관측성

관측성 (메트릭, 추적, 로그)

MCP 서버는 Prometheus 메트릭을 노출할 수 있고 OpenTelemetry 분산 추적과 로그 내보내기를 지원해요. OTel MCP 시맨틱 컨벤션을 따르죠.

메트릭은 SSE 또는 streamable-http 전송이 필요해요. 추적과 로그 내보내기는 표준 OTEL_* 환경 변수를 사용하고 어떤 전송에서든 --metrics와 무관하게 동작해요.

참고: mcp-grafana는 현재 추적과 로그 모두 OTLP/gRPC 전송만 지원해요. OTEL_EXPORTER_OTLP_PROTOCOL(그리고 _TRACES_PROTOCOL/_LOGS_PROTOCOL 변형)은 반영되지 않고, 어쨌든 gRPC가 사용돼요.

출처: 문서

본문

무엇을 얻을 수 있을까요

MCP 작업 메트릭(HTTP 전송에서만)을 스크래핑하고, stdio를 포함한 어떤 전송에서든 추적과 로그를 Tempo, Loki, Grafana Cloud로 내보낼 수 있어요.

시작하기 전에

  • SSE 또는 streamable-http로 실행 중인 서버 (메트릭은 stdio에서 사용 불가).

Prometheus 메트릭 활성화하기

SSE 또는 streamable HTTP 전송을 쓸 때 --metrics로 Prometheus 메트릭을 활성화해요:

# Metrics on the main server at /metrics
./mcp-grafana -t streamable-http --metrics
# Metrics on a separate listen address
./mcp-grafana -t streamable-http --metrics --metrics-address :9090

사용 가능한 메트릭:

Metric Type Description
mcp_server_operation_duration_seconds Histogram MCP operation duration (labels: mcp_method_name, gen_ai_tool_name, error_type, network_transport, mcp_protocol_version, and — for tools/call on selected tools — mcp_tool_operation, mcp_tool_resource_type, mcp_tool_phase)
mcp_server_session_duration_seconds Histogram MCP client session duration (labels: network_transport, mcp_protocol_version)
http_server_request_duration_seconds Histogram HTTP server request duration (from otelhttp)

참고: 메트릭은 SSE 또는 streamable HTTP 전송을 쓸 때만 사용할 수 있어요. stdio 전송에서는 사용할 수 없어요.

도구 호출 차원 라벨

tools/call의 경우 mcp_server_operation_duration_seconds는 최대 세 개의 추가 저카디널리티 라벨을 담을 수 있어서, 호출이 무엇을 하고 있었는지에 따라 시간을 잘라낼 수 있어요:

Label Source Notes
mcp_tool_operation the tool’s operation argument Multiplexer tools only (e.g. alerting_manage_rules); one of the tool’s declared operations, else other.
mcp_tool_resource_type the tool’s type argument e.g. the datasource plugin type on create_datasource; a plugin type the server ships a schema for, else other.
mcp_tool_phase the tool’s result _meta Phase of a multi-call flow (e.g. create_datasource schema guidance vs. actual creation).

인자는 원시 클라이언트 입력이라, 인자에서 파생된 두 라벨은 도구별·값별로 허용 목록에 들어가요. 목록에 없는 도구는 둘 다 내보내지 않고, 목록에 없는 값은 하나의 other 버킷으로 합쳐져서 각 라벨의 시리즈 수가 고정돼요. 도구 검증만으로는 이것을 제한할 수 없어요 — 거부된 작업도 계측되고, create_datasource의 type은 자유 텍스트라 알 수 없는 값에서도 성공하니까요. mcp_tool_phase는 예외예요. 도구가 자기 결과에 직접 설정하는 값이라, 도구는 그 값 집합을 작게 유지해야 해요.

호출의 고카디널리티 대상(데이터소스 uid, 없으면 이름)은 절대 메트릭 라벨이 아니에요. mcp.tool.target으로 스팬 전용이고, 엔티티를 지명하지 않는 호출에는 비어 있어요 (아래 참고).

메트릭 이름 발견

Metric Type Description
mcp_metric_names_response_size_bytes Native histogram Bytes consumed from complete successful discovery HTTP responses, after HTTP decompression
mcp_metric_names_count Native histogram Number of metric names decoded successfully before local filtering and pagination

이 메트릭들은 prometheus형 백엔드에서 사용 가능한 메트릭 이름을 발견할 때 받는 응답을 이해하는 데 도움을 줘요. 성공한 응답의 크기만 측정해요. 경계(bounded) 백엔드 라벨은 prometheus(호환 백엔드 포함)와 cloud_monitoring을 구분해요. prometheus 백엔드와 달리 Cloud Monitoring은 이름뿐 아니라 더 많은 것을 담는 전체 디스크립터 응답을 측정해서, 두 백엔드의 응답 바이트 크기 비교는 의미가 없어요.

예를 들어 지난 1시간의 p95 응답 크기는:

histogram_quantile(0.95, sum by (backend) (rate(mcp_metric_names_response_size_bytes[1h])))

이 메트릭들은 Native Histogram을 쓰므로 스크래퍼도 네이티브 히스토그램을 지원해야 해요.

Loki 비용 가드레일 메트릭

Loki 쿼리 비용 가드레일을 켰을 때(--loki-guardrail-mode가 shadow 또는 enforce), 가드레일이 평가하는 모든 query_loki_logs 호출은 네 개 카운터 중 정확히 하나만 증가시켜요:

Metric Type Incremented when
mcp_loki_guardrail_admitted_total Counter The query passed every enabled check
mcp_loki_guardrail_would_block_total Counter The query failed a check in shadow mode and ran anyway
mcp_loki_guardrail_blocked_total Counter The query failed a check in enforce mode and was rejected
mcp_loki_guardrail_fail_open_total Counter The guardrail could not reach a verdict and admitted the query

이 네 개가 가드된 집단을 나누므로, 합이 가드레일이 평가한 호출 수예요. 각각은 점검 횟수가 아니라 쿼리 수예요.

라벨:

Label On Values
reason would_block, blocked selector (no selective label matcher), range (effective time range over the cap), bytes (index/stats estimate over the budget)
cause fail_open unparseable (no stream selector the scanner recognises), estimate_failed (index/stats unavailable)
backend all four loki, victorialogs, unknown

쿼리 하나가 여러 점검을 동시에 걸릴 수 있어요. 가장 먼저 실행된 점검(selector → range → bytes 순)으로 라벨을 붙여 한 번만 세요. 이 순서는 의도적이에요. 선택성 점검은 무조건적이라 selector로 귀속된 쿼리는 --loki-guardrail-max-range나 --loki-guardrail-max-bytes를 올려도 통과되지 않아요. shadow에서 enforce로 승격할 때는 sum(rate(mcp_loki_guardrail_would_block_total[5m]))로 영향받는 집단의 규모를, sum by (reason) (...)로 경계를 조정하면 줄어들지 여부를 읽으세요.

mcp_loki_guardrail_fail_open_total을 함께 보세요. 조용한 would_block 비율은 fail-open 비율도 낮을 때만 "차단될 것이 없다"는 뜻이에요. backend="loki"에서 cause="unparseable" 비율이 높으면 가드레일의 LogQL 스캐너가 사용 중인 쿼리 형태를 인식하지 못한다는 뜻이라서, 경계 조정보다는 스캐너 개선이 필요해요. backend="victorialogs"에서 높은 unparseable 비율은 정상이에요 — 중괄호 없는 LogsQL이 그 곳의 일반적인 형태이고, 바이트 예산 점검은 그 백엔드에서 전혀 실행되지 않아요. 그래서 backend를 따로 접지 않고 라벨로 두는 것이에요.

스트림 셀렉터도 LogQL도 라벨로 내보내지 않아요. 둘 다 무한할 수 있고, 라인 필터가 민감한 리터럴을 담을 수 있거든요. 대신 추출된 셀렉터는 WARN으로, 전체 쿼리는 DEBUG로 로그해요.

OpenTelemetry 추적 활성화하기

OTEL_EXPORTER_OTLP_ENDPOINT(또는 신호별 OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)가 설정되면 서버가 OTLP/gRPC로 추적을 내보내요.

로컬 예시:

# Send traces to a local Tempo instance
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_EXPORTER_OTLP_INSECURE=true \
./mcp-grafana -t streamable-http

Grafana Cloud 예시:

# Send traces to Grafana Cloud with authentication
OTEL_EXPORTER_OTLP_ENDPOINT=https://tempo-us-central1.grafana.net:443 \
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic ..." \
./mcp-grafana -t streamable-http

도구 호출 스팬은 tools/call 같은 이름을 따르고 gen_ai.tool.name, mcp.method.name, mcp.session.id 같은 속성을 포함해요. 서버는 도구 호출 요청의 _meta 필드에서 W3C 트레이스 컨텍스트 전파를 지원해요.

트레이스 컨텍스트 전파

HTTP 전송(SSE 또는 streamable-http)에서는 서버가 양쪽에서 W3C 트레이스 컨텍스트 전파에 참여해서, 호출자와 mcp-grafana, Grafana가 하나로 이어진 트레이스로 보여요:

  • 인바운드: 들어오는 요청(MCP 클라이언트나 상위 프록시의)의 traceparent/tracestate 헤더가 서버 스팬의 부모가 되어, 새 트레이스를 시작하는 대신 호출자의 트레이스를 이어가요.
  • 아웃바운드: Grafana API로 가는 요청이 mcp-grafana 자신의 스팬을 지명하는 traceparent를 담아서, Grafana의 스팬이 우리 스팬에 매달려요.

전파는 항상 활성화돼 있어요. OTEL_EXPORTER_OTLP_ENDPOINT가 필요하지 않아요. 추적 내보내기를 꺼도 스팬은 기록되지 않지만 인바운드 트레이스 컨텍스트는 버려지지 않고 Grafana로 전달돼요.

사용되는 전파자는 표준 OTEL_PROPAGATORS 환경 변수로 구성되며 기본은 tracecontext,baggage예요. 비-W3C 시스템(b3, b3multi, jaeger, xray, ottrace, 쉼표로 구분한 조합)과 상호운용하려면 설정하고, 전파를 완전히 끄려면 none으로 설정해요.

도구 호출 요청은 MCP _meta 필드에 traceparent/tracestate를 담을 수도 있어요. 있으면 그 컨텍스트가 도구 스팬의 부모가 돼요. 이것은 HTTP 요청에서 헤더를 읽을 수 없는 stdio에서 유일한 전파 채널이에요.

GRAFANA_FORWARD_HEADERS에 traceparent나 tracestate를 나열할 필요도 없고 더 이상 효과도 없어요. 호출자의 traceparent를 그대로 전달하면 Grafana의 스팬이 호출자에 매달리고 mcp-grafana가 트레이스 중간에서 빠지게 되므로, 전달된 값이 전파된 값을 절대 덮어쓰지 않아요. OTEL_PROPAGATORS=none으로 전파를 끄면 전달된 추적 헤더는 여전히 적용돼요.

도구 호출 스팬은 도구 호출 차원 mcp.tool.operation, mcp.tool.resource_type, mcp.tool.phase와, 한 엔티티를 건드리는 스팬을 그룹화하기 위한 스팬 전용 mcp.tool.target(데이터소스 uid, 없으면 이름)도 담아요. mcp.tool.target은 호출이 엔티티를 지명하지 않을 때 — 특히 데이터소스가 아직 존재하지 않는 create_datasource의 phase=schema 호출 — 비어 있어서, 그 호출을 나중의 phase=created 호출에 이어 붙이는 것은 공유 트레이스(클라이언트가 _meta로 컨텍스트를 전파할 때)나 mcp.session.id + mcp_tool_resource_type에 의존해요. 메트릭 라벨과 달리 이것들은 모든 도구에 원시 값으로 붙어요 (허용 목록도, other 버킷도 없음) — 트레이스는 설계상 고카디널리티니까요. HTTP 전송(SSE 또는 streamable-http — 풍부하게 만들 서버 스팬이 존재하려면)과 추적 활성화가 필요하지만 --metrics는 필요 없어요. stdio이거나 추적이 꺼져 있으면 보강은 no-op이에요.

OpenTelemetry 로그 활성화하기

OTEL_EXPORTER_OTLP_ENDPOINT(또는 신호별 OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)가 설정되면 서버는 기존의 평문 stderr 출력에 더해 구조화 로그도 OTLP/gRPC로 내보내요. 로그는 활성 스팬의 trace_id와 span_id를 담아서 내보낸 추적과 상관관계를 이뤄요.

추적과 로그는 엔드포인트를 독립적으로 해석하므로 두 신호를 따로 켤 수 있어요:

  • OTEL_EXPORTER_OTLP_TRACES_ENDPOINT만 설정하면 추적만 활성화되고 로그 내보내기는 없어요.
  • OTEL_EXPORTER_OTLP_LOGS_ENDPOINT만 설정하면 로그 내보내기만 되고 추적은 없어요.
  • 일반 OTEL_EXPORTER_OTLP_ENDPOINT를 설정하면 둘 다 활성화돼요.
# Send logs and traces to a local OTel collector
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
OTEL_EXPORTER_OTLP_INSECURE=true \
./mcp-grafana -t streamable-http

stderr 로깅은 그대로 계속돼요. 운영자는 로그만 OTel 컬렉터로 보내고 싶으면 stderr를 /dev/null로 파이프하면 돼요.

로그는 OTEL_EXPORTER_OTLP_LOGS_ENDPOINT(또는 일반 OTEL_EXPORTER_OTLP_ENDPOINT)를 원격 gRPC 엔드포인트에 지정하고 OTEL_EXPORTER_OTLP_LOGS_HEADERS(또는 OTEL_EXPORTER_OTLP_HEADERS)로 인증을 제공하면 Grafana Cloud처럼 OTLP/gRPC를 받는 어떤 관리형 백엔드로든 직접 보낼 수 있어요. 위 추적 예시와 같은 방식이죠. 로컬 OTel 컬렉터는 선택 사항이에요 — fan-out, 배칭, 다중 백엔드 라우팅에 유용하지만 필수는 아니에요.

신호별 변형 OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, OTEL_EXPORTER_OTLP_LOGS_HEADERS, OTEL_EXPORTER_OTLP_LOGS_INSECURE, OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, OTEL_EXPORTER_OTLP_LOGS_COMPRESSION은 반영되어 일반 OTEL_EXPORTER_OTLP_* 대응 항목을 덮어써요 — 전체 목록과 우선순위 규칙은 OTel exporter 사양을 참고하세요.

참고: 구성된 컬렉터에 도달할 수 없으면 로그 레코드는 메모리에 버퍼링되고(기본 큐: 2048) 큐가 차면 가장 오래된 레코드가 버려져요. 프로세스는 서비스를 막지 않고 계속돼요. 장애 중 손실 없는 버퍼링이 필요하면 로컬 OTel 컬렉터를 구성하세요.

로그는 stdio 전송에서도 내보내져서, IDE 클라이언트가 불러낸 로컬 mcp-grafana 인스턴스의 로그를 중앙화하기 쉬워요.

Docker로 실행하기 (메트릭, 추적, 로그)

docker run --rm -p 8000:8000 \
  -e GRAFANA_URL=http://localhost:3000 \
  -e GRAFANA_SERVICE_ACCOUNT_TOKEN= \
  -e OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317 \
  -e OTEL_EXPORTER_OTLP_INSECURE=true \
  grafana/mcp-grafana \
  -t streamable-http --metrics

더 알아보기 (Learn more)