OpenTelemetry
에이전트가 이미 OpenTelemetry를 내보내고 있다면, mcpd는 엔드포인트를 바꾸는 일입니다. 이 페이지는 OTLP 경로 전체입니다. 엔드포인트 셋, 인코딩, 그리고 mcpd가 읽는 모든 속성입니다.
엔드포인트 셋
mcpd는 질의 API와 인터페이스를 내주는 바로 그 포트에서 OTLP HTTP 엔드포인트도 내줍니다. 앞에 세워 둘 컬렉터도, 두 번째 컨테이너도, 따로 쓰는 수신 포트도 없습니다.
| 엔드포인트 | 받는 것 | 답하는 것 |
|---|---|---|
POST /v1/traces | ExportTraceServiceRequest | partialSuccess.rejectedSpans |
POST /v1/logs | ExportLogsServiceRequest | partialSuccess.rejectedLogRecords |
POST /v1/metrics | ExportMetricsServiceRequest | partialSuccess.rejectedDataPoints |
성공한 내보내기는 모두 개수를 실은 OTLP 자신의 partialSuccess
모양으로 답합니다. 컬렉터가 무언가를 버렸다고 보낸 쪽에 알리는 방식이 그것이고,
익스포터는 그 값을 읽습니다. 빈 본문을 돌려주면 보낸 쪽은 얻지 못한 성공을
성공으로 보고하게 됩니다.
익스포터를 이쪽으로 향하게 하기
변수 두 개면 됩니다. 에이전트를 돌리는 프로세스의 환경에 두십시오. 그 밖에 바뀌는 것은 없습니다.
익스포터
# The base URL. Your exporter appends /v1/traces itself.
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# Optional. Send gzip if your payloads are large.
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip
# Only when mcpd runs with --ingest-key.
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"
아무것도 오지 않는 이유를 디버깅하기 전에 알아 둘 것이 둘 있습니다. 첫째,
OTEL_EXPORTER_OTLP_ENDPOINT는 기준 URL이고 익스포터가 거기에
시그널 경로를 덧붙입니다. 경로까지 포함해 전체 URL을 주고 싶다면
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT를 대신 쓰십시오. 둘째,
OpenTelemetry에서 4317번 포트는 관례상 gRPC 포트이고 4318번이 HTTP
포트입니다. mcpd는 모든 것을 한 포트에서 내주기 때문에 4317에서 HTTP를
받습니다. 그래서 기본 포트를 그대로 둔 익스포터는 4318로 보내고, 아무것도
도착하지 않습니다. 포트를 명시하십시오.
트레이스는 두 인코딩 모두
/v1/traces는 content-type 헤더를 읽고 두 인코딩을
모두 받습니다. protobuf가 들어 있으면 OTLP/protobuf이고, 그렇지
않으면 OTLP/JSON입니다. protobuf가 OpenTelemetry의 기본값이므로, 손대지 않은
SDK를 mcpd로 향하게 하면 아무 설정 없이도 트레이스가 도착합니다.
/v1/logs와 /v1/metrics는 JSON만 받습니다. 거기로 온
protobuf 본문은 body is not valid JSON으로 거절됩니다. 세 시그널을
모두 보낸다면 프로토콜을 전역으로 한 번 정하십시오.
모든 시그널을 json으로
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json content-type이 아예 오지 않으면 mcpd는
application/json으로 가정합니다.
압축과 크기
요청 본문은 content-encoding: gzip으로 오거나,
identity로 오거나, 인코딩 헤더 없이 올 수 있습니다. 그 밖의 것은
이름을 짚어 거절합니다. content-encoding "br" is not supported; use gzip
or send the body uncompressed입니다. 응답은 콘텐츠 협상으로 압축되므로,
느린 회선으로 큰 트레이스를 되읽을 때도 압축됩니다.
한 요청에 한계 두 개가 걸립니다. 본문 한계는 32 MiB이고 도착한 것을 기준으로
재므로, gzip이 여유를 만들어 줍니다. 배치 한계는 시그널 10,000개이며 모든 수집
문에서 디코딩한 뒤에 셉니다. /v1/traces의 스팬,
/v1/logs의 레코드, /v1/metrics의 데이터 포인트,
그리고 /v1/ingest의 요청에 들어 있는 전부입니다. 더 큰 배치는 두
숫자를 함께 짚으며 413 payload_too_large로 거절됩니다. 두 한계 다
설정 항목입니다. --max-body-bytes와
--max-batch-signals이며
설정에
적혀 있습니다.
도착한 뒤 저장되기까지
OTLP 스팬과 네이티브 스팬은 같은 길을 갑니다. 파이프라인을 공유하는 이유가 그것입니다. 아래의 순서가 두 문 사이에서 어긋날 수 없습니다.
-
Decode
OTLP or native, gzip or plain
-
Sample
Decided once, at the root
-
Redact
Before anything is written
-
Price
From the configured price book
-
Store
One transaction per batch
Redaction is stage three, not a cleanup job. A value that a rule matches never reaches storage at all.
OTLP 스팬이 비용 수치를 갖게 되는 것은 수집 시점에 값을 매기기 때문입니다. mcpd는 스팬이 도착한 그 순간, 이 서버가 돌고 있는 가격표로 값을 매깁니다. 그래서 나중에 요율을 바꾸어도 지난달의 비용이 조용히 다시 쓰이는 일은 없습니다. 비용 산정을 보십시오.
두 시맨틱 규약 모두
AI 스팬을 위한 속성 이름은 서로 겨루는 두 벌이 쓰이고 있습니다.
OpenTelemetry 자신의 gen_ai.*와, OpenInference의
llm.* 및 openinference.span.kind입니다. mcpd는 둘 다
읽고, 그 위에 자기 mcpd.* 이름도 읽습니다. 이름을 바꿔 줄 것은
없습니다.
아래의 각 행은 순서대로 읽히고, 먼저 맞는 것이 이깁니다. 어느 행도 이름 짓지 않은 속성도 그대로 보관됩니다. 평탄화된 속성 맵 전체가 스팬에 저장되므로, 보낸 것이 매핑되지 않았다는 이유로 사라지는 일은 없습니다.
모델, 페이로드, 사용량
| mcpd 필드 | 읽는 곳, 순서대로 |
|---|---|
model | gen_ai.request.model, gen_ai.response.model, llm.model_name, mcpd.model |
provider | gen_ai.system, llm.provider, mcpd.provider |
input | input.value, gen_ai.prompt, mcpd.input |
output | output.value, gen_ai.completion, mcpd.output |
entity_name | gen_ai.agent.name, mcpd.entity.name |
usage.input_tokens | gen_ai.usage.input_tokens, gen_ai.usage.prompt_tokens, llm.token_count.prompt, mcpd.usage.input_tokens |
usage.output_tokens | gen_ai.usage.output_tokens, gen_ai.usage.completion_tokens, llm.token_count.completion, mcpd.usage.output_tokens |
usage.total_tokens | llm.token_count.total, gen_ai.usage.total_tokens |
input_details.cache_read | gen_ai.usage.cache_read_input_tokens, llm.token_count.prompt_details.cache_read, mcpd.usage.cache_read_tokens |
input_details.cache_write | gen_ai.usage.cache_creation_input_tokens, llm.token_count.prompt_details.cache_write, mcpd.usage.cache_write_tokens |
input_details.audio | llm.token_count.prompt_details.audio |
output_details.reasoning | llm.token_count.completion_details.reasoning, gen_ai.usage.reasoning_tokens |
output_details.audio | llm.token_count.completion_details.audio |
토큰 개수 셋 중 어느 것도 없는 스팬은 0으로 채운 사용량 객체 대신 아예 사용량 객체를 싣지 않습니다. 입력 토큰이 0이라는 사실과 입력 토큰을 모른다는 사실은 서로 다르고, 두 번째를 뜻하는데 첫 번째로 만든 비용 보고서는 아무도 알아채지 못하는 방식으로 틀립니다.
상관 정보
상관 필드는 한 사용자의 실행이나 한 세션의 실행을 물어볼 수 있게 해 주는 것입니다. 스팬 속성을 먼저 읽고, 그다음 리소스 속성을 읽습니다.
| mcpd 필드 | 읽는 곳, 순서대로 |
|---|---|
project | 스팬의 mcpd.project, 그다음 리소스의 같은 값, 그다음 데몬의 --project |
environment | mcpd.environment, 리소스 deployment.environment.name, 리소스 deployment.environment |
service_name | 리소스 service.name |
service_version | 리소스 service.version |
session_id | gen_ai.conversation.id, session.id, mcpd.session_id |
thread_id | mcpd.thread_id, thread.id |
user_id | user.id, enduser.id, mcpd.user_id |
request_id | mcpd.request_id, http.request.id |
release | mcpd.release, 리소스 service.version |
run_id, resource_id, experiment_id | mcpd.run_id, mcpd.resource_id, mcpd.experiment_id |
tags | mcpd.tags, 문자열 배열로 |
상태와 오류
OTLP 상태 코드 1은 ok가 되고 2는
error가 됩니다. 상태가 설정되지 않았는데 종료 시각이 있으면
ok로 기록합니다. 불평 없이 끝난 스팬은 실제로 잘 끝난 것이기
때문입니다. 상태가 설정되지 않았고 종료 시각도 없으면 unset으로
남습니다. 그 스팬은 아직 돌고 있고, mcpd는 결과를 지어내는 대신 그대로
저장합니다.
오류 스팬에서 메시지는 status.message, 그다음
exception.message에서 오고, 둘 다 없으면 문자열 그대로
unspecified error가 됩니다. 오류 종류는
exception.type에서, 스택은 exception.stacktrace에서
옵니다. 오류 스팬은 언제나 메시지를 싣습니다. 글 없이 무언가 실패했다는 말은
손쓸 수 있는 것이 아니기 때문입니다.
스팬 종류
종류는 그 스팬이 워터폴에 어떻게 그려질지를 정합니다. mcpd는 구체적인 것부터
차례로 추론하고, 언제나 안전한 generic으로 물러납니다. 스팬에
잘못된 이름표를 붙이는 것은 이름표를 붙이지 않는 것보다 나쁩니다.
mcpd.span.kind가 mcpd가 아는 종류를 가리킬 때, 그 값.openinference.span.kind.gen_ai.operation.name.- 한 종류의 스팬만 싣는 속성이 있는지 여부.
| 속성 값 | mcpd 종류 |
|---|---|
openinference.span.kind=LLM | model_generation |
openinference.span.kind=CHAIN | workflow_run |
openinference.span.kind=TOOL | tool_call |
openinference.span.kind=AGENT | agent_run |
openinference.span.kind=RETRIEVER | retrieval |
openinference.span.kind=EMBEDDING | rag_embedding |
openinference.span.kind=RERANKER | rag_action |
openinference.span.kind=GUARDRAIL | guardrail |
openinference.span.kind=EVALUATOR | scorer_run |
gen_ai.operation.name=chat | model_generation |
gen_ai.operation.name=text_completion | model_generation |
gen_ai.operation.name=generate_content | model_generation |
gen_ai.operation.name=embeddings | rag_embedding |
gen_ai.operation.name=execute_tool | tool_call |
gen_ai.operation.name=invoke_agent | agent_run |
gen_ai.operation.name=create_agent | agent_run |
gen_ai.system, llm.model_name, llm.provider 중 하나라도 있음 | model_generation |
mcp.tool.name 있음 | mcp_tool_call |
http.request.method 또는 http.method 있음 | http_request |
| 그 밖의 모든 것 | generic |
openinference.span.kind나 gen_ai.operation.name
아래에서 mcpd가 알아보지 못하는 스팬 종류는 거절되지 않고
generic으로 내려갑니다. 종류의 전체 목록은
데이터 모델에
있습니다.
로그와 메트릭
로그 레코드는 명세가 정한 대로 심각도 번호로 매핑됩니다. 1에서 4는 trace,
5에서 8은 debug, 9에서 12는 info, 13에서 16은 warn, 17에서 20은 error이고, 그
위는 모두 fatal입니다. 심각도 번호가 없으면 레코드는 info로 다룹니다. 메시지는
body.stringValue에서, 로거 이름은 스코프 이름에서 오고,
traceId와 spanId는 유효할 때 그대로 보관됩니다. 로그
한 줄을 그것을 낸 스팬 옆에 놓아 주는 것이 바로 이것입니다.
메트릭에서는 합이 카운터가 되고 게이지는 게이지가 됩니다. 히스토그램은 버킷 경계를 보존한 채 합과 개수로 기록합니다. mcpd는 미리 집계된 히스토그램에서 정확한 분위수를 되살릴 수 있는 척하지 않습니다. exemplar는 보관하며, 차트 위의 급등에서 그것을 일으킨 실행을 열 수 있게 해 주는 것이 이것입니다. 대부분의 도구는 이 값을 버립니다.
id, 그리고 W3C traceparent
mcpd는 OpenTelemetry의 식별자 모양을 그대로 씁니다. 트레이스 id는 128비트로 소문자 16진수 32자, 스팬 id는 64비트로 16자입니다. 전송 형식을 똑같이 두는 것이 변환 계층을 없애 줍니다. 모두 0인 id는 트레이스가 없음을 뜻하도록 예약되어 있어, mcpd가 스스로 만드는 id를 포함해 어디에 나타나든 거절됩니다.
OTLP/JSON은 id 필드에 16진수를 규정하지만, 몇몇 SDK는 base64를 내보냅니다. mcpd는 둘 다 받아서 16진수로 저장합니다. id가 없거나 길이가 틀리거나 모두 0인 스팬은 건너뛰고 배치의 나머지는 그대로 씁니다. 나쁜 스팬 하나 때문에 멀쩡한 스팬 만 개를 거절하는 컬렉터는 사람들이 꺼 버리는 컬렉터이기 때문입니다.
타임스탬프는 Unix epoch 이후의 나노초입니다. 253을 넘으므로
OTLP/JSON은 문자열로 보내고, mcpd도 문자열로 파싱합니다. 이것을 부동소수점으로
읽으면 정밀도를 잃고, 그것은 실제로는 그렇지 않은데도 몇 마이크로초 차이로
시작한 것처럼 보이는 스팬으로 드러납니다. endTimeUnixNano가
0이거나 없으면 그 스팬은 아직 끝나지 않은 것입니다.
traceparent 헤더는
version-traceid-parentid-flags입니다. mcpd는 명세가 정한 규칙대로
파싱합니다. 버전 ff는 거절하고, 버전 00은 정확히 네
필드여야 하며, 이후 버전은 더 실어도 되고, id는 검증을 통과해야 하고, 플래그
바이트의 0번 비트가 샘플링 플래그입니다. 형식이 틀린 헤더는 결코 요청을 거절할
이유가 되지 않습니다. mcpd는 대신 새 트레이스를 시작합니다.
이것이 중요해지는 곳은 이렇습니다. mcpd SDK들은 들어오는
traceparent를 읽고 나가는 호출에 하나를 실어 보내므로, 트레이스가
서비스 경계를 넘어 살아남습니다. OTLP 엔드포인트는 id를 페이로드에서
가져옵니다. 익스포터가 이미 스팬에 넣어 두었기 때문입니다. mcpd로 계측하지 않는
서비스에서 트레이스가 시작된다면 그 traceparent를 함께 넘기십시오.
그러면 양쪽이 하나의 트레이스 id 아래에 놓입니다.
요청 하나, 통째로
위의 어느 것에도 SDK는 필요 없습니다. 아래는 터미널에 그대로 붙여 넣을 수 있는 완전하고 유효한 내보내기입니다.
OTLP/JSON 원본
curl -X POST http://localhost:4317/v1/traces \
-H 'content-type: application/json' \
-d '{
"resourceSpans": [{
"resource": { "attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-agent" } }
]},
"scopeSpans": [{
"spans": [{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"name": "chat gpt-4o",
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000001500000000",
"status": { "code": 1 },
"attributes": [
{ "key": "gen_ai.system", "value": { "stringValue": "openai" } },
{ "key": "gen_ai.request.model", "value": { "stringValue": "gpt-4o" } },
{ "key": "gen_ai.operation.name", "value": { "stringValue": "chat" } },
{ "key": "gen_ai.usage.input_tokens", "value": { "intValue": "1200" } },
{ "key": "gen_ai.usage.output_tokens", "value": { "intValue": "350" } }
]
}]
}]
}]
}'
# {"partialSuccess":{"rejectedSpans":0,"errorMessage":""}} OTLP가 실을 수 없는 것
OTLP에는 스팬과 로그와 메트릭을 위한 모양이 있습니다. 기계가 낸 점수, 사람이 누른 부정 평가, 관점 노트를 위한 모양은 없고, 요즘 공급자가 청구하는 토큰 세부 항목을 위한 어휘도 없습니다. 그런 것을 보내는 길은 둘입니다.
-
POST /v1/ingest. mcpd의 네이티브 배치 엔드포인트이며, 요청 하나에 데이터 모델 전체를 싣습니다. -
POST /v1/api/scores와POST /v1/api/feedback. 이미 OTLP로 보내 둔 트레이스에 붙습니다.
둘을 섞어 쓰는 것은 흔한 일이고 그러라고 있는 것입니다. 이미 돌리고 있는 OpenTelemetry SDK로 스팬을 내보내고, 평가 잡에서 점수를 올리십시오. 둘은 트레이스 id에서 만납니다. 점수와 피드백과 HTTP API를 보십시오.
포트를 열기 전에
--ingest-key를 두지 않으면 4317에 닿을 수 있는 누구든 거기에 쓸 수
있고, --read-key가 없으면 저장된 모든 프롬프트를 읽을 수 있습니다.
mcpd는 둘 중 하나라도 열린 채로 루프백이 아닌 주소에 바인드하면 시작할 때 경고를
남깁니다.
설정으로 키를
두고, Authorization: Bearer로 보내십시오. 위의
OTEL_EXPORTER_OTLP_HEADERS가 하는 일이 그것입니다.
키와 함께 실행하기
mcpd --ingest-key $INGEST_KEY --read-key $READ_KEY