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, pillan a mucha gente, y las tres fallan en silencio.

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.

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" 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.

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%20BASE64
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 y OTEL_EXPORTER_OTLP_CLIENT_KEY
Una CA privadaOTEL_EXPORTER_OTLP_CERTIFICATE

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.

Un span por petición MCP, más un span hijo por cada llamada a la API de GitLab que provoque.

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ó incidencias como si borró una rama. El atributo de acción es lo único que las distingue.

InstrumentoUnidadQué responde
mcp.server.operation.durationscuánto tardan las peticiones, por método y acción
mcp.client.operation.durationscuánto espera este servidor al cliente
http.client.request.durationscuánto tarda GitLab
mcp.server.session.durationscuánto sigue conectado un cliente
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 razón

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.

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. gitlab_mcp.credential_pool.eviction.reason toma uno de siete valores: size_pressure, size_pressure_busy, idle, stale_credential, rejected_credential, invalid_credential y rebuild. Los siete se exportan desde el arranque del proceso, ceros incluidos, para que un panel nunca esté vacío por el motivo ambiguo.

size_pressure_busy es el que merece una alerta: es el único camino que termina una suscripción que alguien está esperando, significa que el pool no tenía nada tranquilo que llevarse, y tiene una línea WARN propia con in_use=true y el max_size que hay que subir.

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.

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

Esos cinco 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 “to correlate information for a user in anonymized form” 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 person pseudonym, que “requires long-term storage of the pseudonymisation secrets” y cuyo “risk of unauthorised attribution is comparatively high”. Según el Artículo 4(5) del RGPD esa clave es la “additional information” 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.

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 incidencia 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 y el estado; el span padre ya nombra la acción, que identifica la familia de endpoints de forma más legible que una URL.

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.

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 bajo una clave de 32 bytes generada al arrancar y que no se escribe en ningún sitio. 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

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.