Ir al contenido

OpenTelemetry

Este servidor puede exportar trazas, métricas y logs por OTLP a un colector que tú ejecutas. Está desactivado por defecto.

Ventana de terminal
gitlab-mcp-server --http --gitlab-url=https://gitlab.com --telemetry

Esa es toda la superficie que este proyecto posee. El endpoint, las credenciales, el muestreo, el agrupamiento y los atributos de recurso vienen de las variables de entorno OTEL_* estándar que los exportadores de OpenTelemetry leen por su cuenta. Reinventar esa superficie significaría mantener una segunda copia peor de una configuración que ya conoces, y rompería el caso corriente de una máquina que exporta OTEL_EXPORTER_OTLP_ENDPOINT una vez para todos sus servicios.

No son cosa de este servidor, y pillan a mucha gente. Tres de ellas dejan la telemetría funcionando con algo distinto de lo que querías; la cuarta, http/json, se rechaza al arrancar con una línea ERROR, telemetry disabled: it could not be started, y el servidor sigue sirviendo sin telemetría.

Este servidor emula esa desviación señal por señal en vez de leer el esquema de la URL, así que el aviso de texto plano nombra las señales que de verdad quedan afectadas. No rechaza la configuración: el endpoint es cosa tuya.

No hay opción de usuario y contraseña, y no falta ninguna: la especificación no define esa variable. Todos los esquemas son cabeceras.

Lo que pide tu colectorQué configurar
Nada (el valor por defecto)No configures nada. Es una configuración admitida, no un recurso de última hora.
Un token bearerOTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20TU_TOKEN
Autenticación básicaOTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20$(printf 'usuario:clave' | base64)
Una clave de API de proveedorOTEL_EXPORTER_OTLP_HEADERS=api-key=TU_CLAVE
Varias a la vezOTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20tok,x-tenant=acme
TLS mutuoOTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/ruta/cert.pem y OTEL_EXPORTER_OTLP_CLIENT_KEY=/ruta/key.pem
Una CA privadaOTEL_EXPORTER_OTLP_CERTIFICATE=/ruta/ca.pem

Una cabecera configurada para una señal cuyo endpoint va en texto plano y está en otro host, sea un endpoint http:// o uno https:// que OTEL_EXPORTER_OTLP_INSECURE degrada como se describe arriba, cruza la red en claro en cada exportación. Este servidor no rechaza esa configuración, porque un colector en una red privada de confianza alcanzado en claro es un despliegue real y el endpoint es cosa tuya. Lo que sí hace es decirlo una vez al arrancar, nombrando las señales afectadas:

{"level":"WARN","msg":"a collector credential is configured against a plaintext endpoint on another host; it crosses the network in the clear on every export","component":"telemetry","signals":["traces"]}

El bucle local queda exento: una credencial que nunca sale de la máquina no se puede observar en una red, así que un colector adyacente no genera ningún aviso.

Este servidor nunca transforma esa variable, y nunca la registra. Lo que configures es lo que recibe tu colector, y una prueba de extremo a extremo verifica que la credencial no aparece nunca en los logs del propio servidor, ni siquiera cuando falla una exportación.

Sí la lee, una vez, y solo para mantenerla fuera de esa salida. El analizador de cabeceras del SDK imprime lo que no supo interpretar, y el exportador de logs entrega la variable entera al manejador de errores, así que un solo par malformado que no sea la credencial basta para imprimir al lado una credencial perfectamente formada. Este servidor lee al arrancar las mismas variables que leen los exportadores y redacta esos valores, y sus formas decodificadas, de cada diagnóstico del SDK que emite. El nombre de la variable se deja tal cual, porque quien tiene una configuración mal formada necesita saber cuál corregir.

Un span por petición MCP, más un span hijo por cada llamada a la API de GitLab que provoque. El modo HTTP añade un span por cada petición HTTP entrante, que se describe después de la tabla.

AtributoDóndeSignificado
mcp.method.nametodos los spanstools/call, resources/read, prompts/get, …
gen_ai.tool.namellamadas a herramientala herramienta que nombró el cliente
gitlab_mcp.actionllamadas a herramientala acción canónica del catálogo, como issue.list
gitlab_mcp.tool_surfacetodos los spansdynamic, meta o individual
network.transporttodos los spanspipe en stdio, tcp en HTTP
gen_ai.prompt.nameprompts/getel prompt que nombró el cliente
mcp.protocol.versioncuando se conocela revisión de MCP que habla la petición
mcp.session.idsolo sesiones con estadoausente en el HTTP sin estado por defecto
error.typesolo en fallosun código JSON-RPC, o tool_error
rpc.response.status_codecuando hubo códigoel código JSON-RPC
gitlab_mcp.domainllamadas a herramientasel dominio del catálogo, como issue; acotado, así que sobrevive en métricas donde el id de acción son demasiados valores
gitlab_mcp.refusal_reasonrechazospor qué se declinó la llamada sin llegar a GitLab

gitlab_mcp.action es el que conviene entender. En la superficie dinámica, que es la de por defecto, cada llamada nombra las mismas dos herramientas, así que gen_ai.tool.name vale gitlab_execute_action tanto si el servidor listó issues como si borró una rama. El atributo de acción es lo único que las distingue.

El span hijo de una llamada a GitLab registra http.request.method, el host y el puerto a los que se envió y el estado de la respuesta y, cuando la ruta es una de las que contiene la tabla de permisos de grano fino del servidor, url.template: la ruta tal como GitLab la declara, por ejemplo /api/v4/projects/:id/issues, que también da nombre al span (GET /api/v4/projects/:id/issues). La plantilla lleva los marcadores y nunca los valores con que una petición los rellenó. Una instancia instalada bajo una ruta propia (una raíz de URL relativa, https://host/gitlab) recibe la misma plantilla: la ruta que precede a /api/ es de la instancia y se deja fuera. Una petición a una ruta que la tabla no contiene, /api/v4/version entre ellas, no lleva plantilla, y su span se nombra solo por el método. La plantilla es solo un atributo del span: no es una dimensión de http.client.request.duration, cuyas series multiplicaría por cada ruta que sirve GitLab.

En modo HTTP cada petición HTTP entrante recibe además un span de servidor propio, abierto antes de la validación del host, de CORS y de la comprobación de la credencial. El span MCP empieza después de la autenticación, así que este es el que registra una petición rechazada antes de llegar a MCP, con su código de estado. Se nombra por el método y lleva http.request.method, url.scheme (leído de la conexión, nunca de X-Forwarded-Proto), network.protocol.name y http.response.status_code. Un método que no está en la lista de la convención se registra como _OTHER, con el verbo tal como llegó, cortado a 32 caracteres, en http.request.method_original y solo en el span. No registra la ruta, porque en un endpoint publicado la ruta es lo que envíe un escáner. Un 4xx deja sin fijar el estado del span, como exige la convención para un span de servidor, y un 5xx lo marca como error.

InstrumentoUnidadQué responde
mcp.server.operation.durationscuánto tardan las peticiones, por método, dominio y, donde se conserva el nombre de la herramienta, acción
mcp.client.operation.durationscuánto espera este servidor al cliente en lo que inicia él (elicitación, muestreo, roots/list, notificaciones)
http.client.request.durationscuánto tarda GitLab
mcp.server.session.durationscuánto sigue conectado un cliente (stdio y --stateless=false)
gitlab_mcp.credential_pool.entries{entry}cuántas credenciales tiene el pool (modo HTTP)
gitlab_mcp.credential_pool.capacity{entry}cuántas puede tener, que es --max-http-clients
gitlab_mcp.credential_pool.evictions{eviction}entradas soltadas, separadas por gitlab_mcp.credential_pool.eviction.reason
http.server.request.durationscuánto tarda una petición HTTP entrante, con la comprobación de la credencial incluida y también las rechazadas (modo HTTP)
gitlab_mcp.auth.blocks{request}peticiones rechazadas antes de leer su credencial, separadas por gitlab_mcp.auth.block.reason (modo HTTP)

GITLAB_MCP_TELEMETRY_TOOL_NAME (--telemetry-tool-name) decide dos dimensiones de métrica a la vez, gen_ai.tool.name y gitlab_mcp.action, porque en la superficie individual cada herramienta es una acción y conservar cualquiera de las dos conserva tantas series como conservar ambas. auto conserva ambas en las superficies dinámica y meta y quita ambas en la individual, off quita ambas en todas las superficies y on conserva ambas. gitlab_mcp.domain se mantiene con cualquier valor, así que un panel basado en él sobrevive, y uno basado en la acción muestra una serie por dominio en cuanto la acción se quita. Los spans llevan siempre ambas, sea cual sea el ajuste.

mcp.server.session.duration no se registra a propósito en el HTTP sin estado por defecto, donde cada POST es su propia sesión: el histograma sería una copia de mcp.server.operation.duration con un nombre que promete otra cosa.

http.server.request.duration lo mide el span HTTP de arriba, y sus dimensiones son el método, el esquema, el nombre del protocolo y el código de estado de ese span, sin el verbo original. gitlab_mcp.auth.blocks cuenta cada rechazo bajo el presupuesto que lo respondió, como gitlab_mcp.auth.block.reason: failure_lockout y distinct_tokens son los dos presupuestos de autenticación, y transport_source es uno más grueso que se carga a la dirección de la que llegó la conexión y no a la que nombra una cabecera de proxy de confianza, y que solo existe cuando se confía en esa cabecera. Los tres se exportan desde el arranque, ceros incluidos. La dirección rechazada no es una dimensión, a propósito: haría que la serie identificara a personas y crecería sin límite justo con el tráfico que mide.

Los tres instrumentos del pool de credenciales llevan el espacio de nombres propio de este servidor en lugar del mcp. de la convención, porque un pool de credenciales es un concepto de este despliegue y no del protocolo, y un nombre bajo mcp. reclamaría parte de un espacio de nombres que es de la convención semántica. Solo los publica el modo HTTP, y capacity se publica junto a entries para que “cuánto falta para el límite” se pueda responder sin teclear el valor de la opción en la consulta de un cuadro de mando.

gitlab_mcp.credential_pool.eviction.reason toma uno de ocho valores, uno por cada camino de retirada, y todos se exportan desde el arranque del proceso se hayan producido o no, ceros incluidos, para que un panel nunca esté vacío por el motivo ambiguo:

MotivoQué ocurrió
size_pressureEl pool estaba lleno y soltó la entrada usada menos recientemente que no estaba haciendo nada
size_pressure_busyEl pool estaba lleno, todas las entradas servían una suscripción (un observador de recurso o un flujo subscriptions/listen abierto), y la más antigua salió igualmente
idle--pool-idle-timeout recuperó una entrada que nadie había usado
stale_credentialLa credencial no se había comprobado contra GitLab dentro del techo de antigüedad de una hora, así que la entrada se reconstruye en vez de darla por buena
rejected_credentialGitLab rechazó la credencial en una llamada: un 401 que nombra el token, o un 401 que la sonda de credenciales confirmó después, nunca un rechazo por permisos. Un 401 de GraphQL a un token que no lleva ni api ni read_api también nombra el token (Llamadas rechazadas)
invalid_credentialLa revalidación periódica encontró que GitLab rechazaba la credencial
below_minimumGitLab dijo de una credencial que el pool ya sirve que es auténtica y que no lleva ni read_api ni api, el mínimo de admisión: una confirmación o una revalidación recibió 403 insufficient_scope, o la propia descripción del token, consultada de nuevo porque su tipo era desconocido cuando se construyó la entrada, solo nombraba ámbitos por debajo (Verificación de credenciales)
rebuildFalló el registro del catálogo de la forma de configuración, y se llevó sus entradas con él

size_pressure_busy es el que merece una alerta. Toda retirada termina las suscripciones de la credencial que retira, pero esta es la única que se lleva la entrada de un suscriptor por falta de sitio, con su credencial todavía buena, y significa que el pool no tenía nada tranquilo que llevarse: consulta la palanca para el valor de --max-http-clients que lo deja fuera de alcance. Tiene una línea WARN propia con in_use=true y el max_size que hay que subir:

server pool: evicted an entry that was serving a subscription

Las entradas soltadas al apagar no se cuentan a propósito: nadie observa una métrica cuando el proceso ha terminado.

Falta un atributo que la convención pide y que no se puede aportar: jsonrpc.request.id. El SDK de Go no da a un middleware receptor ningún acceso al identificador JSON-RPC del mensaje que atiende, así que no hay nada que registrar. Está registrado aguas arriba.

Segundos, no milisegundos. Lo fija la convención, y es lo contrario del campo duration de los registros de log de este servidor.

Dos de esas dimensiones llevan un nombre que eligió el llamante, gen_ai.tool.name y gen_ai.prompt.name. En una métrica van acotadas: una llamada que nombró algo que este servidor no tiene se registra como _OTHER y no bajo el nombre que envió, así que el número de series almacenadas depende de lo que hay registrado y no de lo que teclee un cliente. El span conserva el nombre literal, que es donde hay que mirar cuando un cliente informa de que sus llamadas fallan.

Un rechazo no es una avería, y por eso tiene un atributo propio: error.type no distingue una llamada que este servidor declinó de una que se rompió. La llamada llegó bien formada y este servidor decidió no ejecutarla:

gitlab_mcp.refusal_reasonPor qué
safe_modeel modo seguro respondió una escritura con una previsualización
needs_confirmationuna acción destructiva sin confirmar
invalid_paramslos parámetros no encajan con la acción
unknown_actionno existe ninguna acción con ese nombre en esta superficie
rate_limitedel propio límite de tasa del despliegue rechazó la llamada
fine_grainedun token de grano fino no puede ejecutar esa acción

Esos seis son el conjunto completo, y eso es lo que hace que el atributo sea asumible también como dimensión de métrica: un despliegue que rechaza una de cada tres llamadas es indistinguible de uno sano si solo se mira el histograma de duración.

Algunos llevan además error.type, y no es una contradicción. Un rechazo que quien llama puede corregir se responde con un resultado de error, para que el modelo sepa sin ambigüedad que su llamada falló; el modo seguro responde con una previsualización, que es un resultado correcto y no fija ningún error.type. Para quedarte con las llamadas declinadas, agrupa por el motivo de rechazo y no por error.type.

rate_limited es el que conviene vigilar, porque es el único rechazo que no depende de quien llama. Su línea de log se limita a una cada diez segundos, para que un cliente en bucle de reintento no inunde el terminal; la métrica las cuenta todas.

La tercera señal son los registros estructurados del propio servidor: las mismas líneas que escribe en stderr, exportadas desde INFO hacia arriba. GITLAB_MCP_LOG_LEVEL sigue gobernando el terminal; el umbral de exportación es aparte, así que ejecutar en debug no manda a tu colector un registro por cada viaje a GitLab además del span que ya lo describe.

Cada registro escrito mientras se atiende una petición lleva el identificador de traza y de span de esa petición, así que desde un span lento se puede saltar directamente a las líneas que se escribieron dentro. Los registros de arranque, apagado y tareas de fondo no llevan ninguno, porque no hay span al que pertenecer.

La política de identidad gobierna también estos registros, y las dos patas se comportan distinto a propósito. Tu terminal conserva user y user_id diga lo que diga la política: estás leyendo la salida de tu propio servidor, y un ajuste sobre lo que sale del proceso no tiene por qué editarla. La copia exportada lleva lo que la política permite, con los mismos nombres user.* que usan los spans, para que una sola consulta los cruce.

Ese último párrafo no era cierto hasta hace poco, y lo enseñó un despliegue en marcha: con la política en pseudonymous, que no nombra a nadie, sus spans llevaban un digest y sus logs el nombre de usuario en claro. La política se había aplicado donde se escribió y nunca al puente de logs.

Por defecto el digest se calcula con un secreto generado al arrancar y que no se escribe en ningún sitio, así que identifica a quien llama dentro de un proceso y en ningún otro lado. Un despliegue con varias réplicas le da a la misma persona un digest distinto en cada una, a la vez, y un reinicio renumera a todo el mundo. Para una instancia única eso suele ser lo que quieres: nada que guardar, nada que filtrar.

Dos ajustes lo cambian, y cuál te toca se deduce de cómo ejecutas el servidor.

AjusteQué hace
GITLAB_MCP_TELEMETRY_IDENTITY_KEYun secreto del que cada réplica deriva sus claves, para que quien llama lleve un único digest en todo el despliegue y entre reinicios
GITLAB_MCP_TELEMETRY_IDENTITY_ROTATIONcuánto vive una clave generada, por ejemplo 24h; vacío la mantiene lo que dure el proceso

Pon la clave si ejecutas más de una réplica, o si el recuento de usuarios distintos tiene que sobrevivir a un despliegue. La rotación entonces no aplica: una clave que pusiste tú es tuya y la rotas en tu calendario, y este servidor lo dice al arrancar en vez de rotarla por debajo.

Pon el intervalo de rotación si ejecutas una sola instancia y quieres que el seudónimo deje de correlacionar pasado un tiempo. Viene desactivado porque las réplicas arrancan en instantes distintos, así que rotarían desfasadas y el recuento de usuarios se movería sin que nadie lo haya pedido.

Ninguna de las dos respuestas está prescrita. La convención define user.hash como un valor “para correlacionar información de un usuario de forma anonimizada” y no dice nada sobre cuánto debe durar, y ni la especificación ni ENISA dan guía sobre la vida de un secreto de seudonimización. Lo que sí hay son dos diseños coherentes, y Matomo lleva los dos a la vez: una sal de la instalación que no rota donde el seudónimo debe persistir, y una semilla que se descarta cada día donde no debe.

Sin adornos, porque cambia de verdad lo que es el export. Un seudónimo estable es lo que el EDPB llama seudónimo de persona (person pseudonym), que “exige almacenar a largo plazo los secretos de seudonimización” y cuyo “riesgo de atribución no autorizada es comparativamente alto”. Según el Artículo 4(5) del RGPD esa clave es la “información adicional” que permite la atribución, así que hay que guardarla por separado de los datos que protege.

En concreto: los ids de usuario de GitLab son enteros pequeños, así que cualquiera que tenga la clave y un export recupera la correspondencia por enumeración, en unos dos minutos y en un solo núcleo. La clave no puede vivir donde aterriza la telemetría. Este servidor la lee de una variable de entorno, con la advertencia que arrastra todo secreto en el entorno: es visible para cualquier cosa que pueda leer el entorno del proceso.

La clave se expande con HKDF-SHA256 en dos claves independientes, una para quien llama y otra para las URIs de recurso, así que el valor que pones nunca se usa como clave directamente y un digest de usuario no se puede comparar con uno de recurso.

Con GODEBUG=fips140=only, HKDF no acepta una clave de menos de 112 bits, así que una clave de menos de 14 bytes se rechaza al arrancar: el servidor deja el rechazo en el log y no registra nada sobre quien llama, como siempre que no puede derivar sus claves de seudonimización. La 3.1.0 detenía el proceso con un panic ante esa misma clave.

Ni por defecto ni con ningún ajuste, porque no existe tal ajuste:

  • Argumentos y resultados de las herramientas. Las convenciones definen atributos Opt-In para ambos; este servidor los declina, porque llevan rutas de proyecto, cuerpos de issues y consultas de búsqueda.
  • El contenido de los recursos. El URI que nombró el recurso sigue la política de identidad: un digest con clave por proceso por defecto, y el URI literal solo bajo full, que ya exporta el nombre real de quien llama.
  • Tu token de GitLab, ni ninguna cabecera que enviara un cliente.
  • Cuerpos y mensajes de error de GitLab. Un fallo registra una clasificación como -32603, nunca el texto, que puede nombrar rutas privadas.
  • URL completas de las llamadas a GitLab. El span hijo registra el método, el host, el estado y, cuando la tabla del servidor la contiene, la plantilla de la ruta, que nombra el endpoint con marcadores donde la URL llevaría rutas de proyecto, identificadores y valores de consulta. El span padre ya nombra la acción.
  • credential_hash, el identificador con el que se registra la credencial de un cliente HTTP: un resumen con clave de ella, bajo una clave que se genera al arrancar el proceso y no se escribe en ningún sitio, así que no lleva ninguno de los caracteres del token. Se sigue escribiendo en stderr, donde es lo que un operador usa para correlacionar un rechazo en su propio terminal, y se elimina de la copia exportada de cada registro de log a cualquier profundidad: no autentica nada y correlaciona todo lo que hace un mismo cliente, y esa distinción no la puede hacer por ti un backend de telemetría.
  • La cabecera Host de una petición que rechazó la guarda de host. Se queda en stderr, donde es lo que lees para averiguar qué host está reenviando un proxy mal configurado, y se elimina de la copia exportada de la misma forma: en un endpoint publicado el valor es lo que un llamante haya querido enviar. Llegó a los colectores en la 3.0.0, sin que nadie lo notara porque la prueba que dice que no puede llegar ganaba su carrera contra el lote de logs, y se elimina desde la 3.1.0.

Una prueba de extremo a extremo lanza tráfico real con una ruta de proyecto distintiva, una consulta de búsqueda y un token, y luego busca los tres en cada payload exportado; otra envía una cabecera Host falsificada, un X-Forwarded-For falsificado y una ruta de sondeo, y hace lo mismo.

Desactivado por defecto, en tres escalones:

Ventana de terminal
--telemetry-identity none # el valor por defecto: nada sobre quien llama
--telemetry-identity pseudonymous # un resumen por proceso: correlacionable, no legible
--telemetry-identity full # user.id y user.name

o GITLAB_MCP_TELEMETRY_IDENTITY.

La política gobierna una cosa más de lo que sugiere su nombre: qué recurso nombró la petición. Un URI de recurso aquí lleva ids de proyecto y de grupo, así que dice en qué está trabajando quien llama, que es la misma clase de revelación que decir quién es. Con none y pseudonymous el span lleva gitlab_mcp.resource.ref, un digest con clave por proceso que correlaciona lecturas y sondeos de un mismo recurso sin nombrarlo. Con full lleva mcp.resource.uri, el atributo de la propia convención, con el URI dentro.

Una bandera y no dos, porque un despliegue que registrara rutas de proyecto mientras dice no registrar a nadie sería el resultado previsible de dejar que las dos configuraciones se separen.

none es el valor por defecto porque registrar la identidad de una persona es una decisión tuya sobre tus propios usuarios, y un valor por defecto que la tome por ti es equivocado apunte hacia donde apunte. Las convenciones coinciden: estos atributos son Opt-In, y su regla dice que “las instrumentaciones DEBERÍAN rellenar el atributo si y solo si el usuario configura la instrumentación para hacerlo”.

pseudonymous emite user.hash, un resumen HMAC-SHA256. Por defecto su clave sale de 32 bytes aleatorios generados al arrancar y que no se escriben en ningún sitio; con GITLAB_MCP_TELEMETRY_IDENTITY_KEY definida se deriva de esa clave (Registros recoge los dos ajustes). Le da a un endpoint compartido lo único que necesita de verdad, distinguir el tráfico de un llamante del de otro, para poder atribuir una ráfaga y seguir una sesión, sin nombrar a nadie.

full emite user.id y user.name. Lo que quiere una organización que audita a sus propios usuarios, y a lo que tiene derecho: son su gente y su colector.

Ninguno de estos llega nunca a una métrica, con ninguna política. Un span que lleva un identificador de usuario cuesta un span; una dimensión de métrica que lo lleva es una serie temporal por persona, que crece sin límite con el número de personas que usan el despliegue.

Ventana de terminal
--telemetry=false # no lo actives; es el valor por defecto
OTEL_SDK_DISABLED=true # vétalo desde el entorno, diga lo que diga la bandera

OTEL_SDK_DISABLED es el interruptor de apagado de la propia especificación, y este servidor lo honra donde nada por debajo lo hace: la cadena no aparece en ningún módulo de OpenTelemetry para Go. Es un veto y no un interruptor, porque su valor por defecto significa “activado” mientras que aquí la telemetría está apagada hasta que se pide. Ponerlo a true fuerza proveedores inertes aunque se haya pasado --telemetry, que es lo que permite desactivar la telemetría en toda una flota sin editar cada fichero de servicio.

Su gramática booleana es la de la especificación y es más estricta que la de Go: solo la cadena true, sin distinguir mayúsculas, desactiva. 1, t y yes no lo hacen, y un valor vacío cuenta como no definido.

Lo indica la ficha enumerante, en la ruta .well-known. La ficha SEP-2127 de /server-card solo declara identidad y datos de conexión, y no lleva bloques de capacidades.

Ventana de terminal
curl -s http://localhost:8080/.well-known/mcp/server-card.json | jq .telemetry
{
"enabled": true,
"signals": ["traces", "metrics", "logs"],
"protocol": "http/protobuf",
"conventions": "OpenTelemetry, following the MCP semantic convention",
"recorded": "the method called, the tool and catalog action, the outcome and the duration",
"not_recorded": "tool arguments, tool results, resource contents, queries, tokens, and GitLab response bodies"
}

El bloque no aparece cuando la telemetría está apagada, en vez de aparecer diciendo false: no deberías tener que interpretar una negación para saber que no se está registrando nada. La dirección del colector no se publica ahí, a propósito, porque nombra tu infraestructura y la tarjeta se sirve a quien la pida.

Un fallo de telemetría nunca afecta a una petición. Un colector caído, que rechaza conexiones o que rechaza tu credencial produce líneas de log en stderr y no cambia nada más: el mismo estado, el mismo cuerpo, la misma latencia. La especificación deja esta decisión abierta (“la API o el SDK PUEDEN fallar rápido … pero NO DEBEN hacer que la aplicación falle más tarde en ejecución”), y aquí la decisión es que un servidor que puede hablar con GitLab siga haciéndolo cuando no puede hablar con un colector.

Ventana de terminal
GITLAB_MCP_LOG_LEVEL=debug gitlab-mcp-server --telemetry 2>telemetry.log

Los fallos de exportación, los pares de cabecera mal formados, las duraciones que no parsean y los avisos de protocolo llegan ahí como registros estructurados.

ÁreaFuente
Variables de configuraciónOTLP Exporter Configuration
Forma de spans y métricasConvenciones semánticas de MCP
Niveles de requisito de atributosAttribute Requirement Level
Registro de erroresRecording Errors
Atributos de identidadUser attributes
Contexto de traza en _metaEspecificación de MCP

La convención semántica de MCP está en estado Development, que su propia tabla de madurez describe como “NO DEBERÍA usarse en producción” y “PUEDE retirarse sin aviso previo”. Se adopta igualmente, por un motivo: afecta a los operadores y no a los clientes MCP, así que un cambio de convención cuesta editar un cuadro de mando y no cuesta nada a quien consume este servidor. Vive en un repositorio aparte y no en opentelemetry.io, cuyo /docs/specs/semconv/mcp/ devuelve 404.