OpenTelemetry
Este servidor puede exportar trazas, métricas y logs por OTLP a un colector que tú ejecutas. Está desactivado por defecto.
Cómo activarlo
Sección titulada «Cómo activarlo»gitlab-mcp-server --http --gitlab-url=https://gitlab.com --telemetry{ "mcpServers": { "gitlab": { "command": "gitlab-mcp-server", "env": { "GITLAB_URL": "https://gitlab.com", "GITLAB_TOKEN": "glpat-…", "GITLAB_MCP_TELEMETRY": "true", "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4318" } } }}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.
Cuatro trampas de las variables estándar
Sección titulada «Cuatro trampas de las variables estándar»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.
Autenticarse ante tu colector
Sección titulada «Autenticarse ante tu colector»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 colector | Qué configurar |
|---|---|
| Nada (el valor por defecto) | No configures nada. Es una configuración admitida, no un recurso de última hora. |
| Un token bearer | OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20TU_TOKEN |
| Autenticación básica | OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20$(printf 'usuario:clave' | base64) |
| Una clave de API de proveedor | OTEL_EXPORTER_OTLP_HEADERS=api-key=TU_CLAVE |
| Varias a la vez | OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20tok,x-tenant=acme |
| TLS mutuo | OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE=/ruta/cert.pem y OTEL_EXPORTER_OTLP_CLIENT_KEY=/ruta/key.pem |
| Una CA privada | OTEL_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.
Qué se registra
Sección titulada «Qué se registra»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.
| Atributo | Dónde | Significado |
|---|---|---|
mcp.method.name | todos los spans | tools/call, resources/read, prompts/get, … |
gen_ai.tool.name | llamadas a herramienta | la herramienta que nombró el cliente |
gitlab_mcp.action | llamadas a herramienta | la acción canónica del catálogo, como issue.list |
gitlab_mcp.tool_surface | todos los spans | dynamic, meta o individual |
network.transport | todos los spans | pipe en stdio, tcp en HTTP |
gen_ai.prompt.name | prompts/get | el prompt que nombró el cliente |
mcp.protocol.version | cuando se conoce | la revisión de MCP que habla la petición |
mcp.session.id | solo sesiones con estado | ausente en el HTTP sin estado por defecto |
error.type | solo en fallos | un código JSON-RPC, o tool_error |
rpc.response.status_code | cuando hubo código | el código JSON-RPC |
gitlab_mcp.domain | llamadas a herramientas | el 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_reason | rechazos | por 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.
Métricas
Sección titulada «Métricas»| Instrumento | Unidad | Qué responde |
|---|---|---|
mcp.server.operation.duration | s | cuánto tardan las peticiones, por método, dominio y, donde se conserva el nombre de la herramienta, acción |
mcp.client.operation.duration | s | cuánto espera este servidor al cliente en lo que inicia él (elicitación, muestreo, roots/list, notificaciones) |
http.client.request.duration | s | cuánto tarda GitLab |
mcp.server.session.duration | s | cuá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.duration | s | cuá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:
| Motivo | Qué ocurrió |
|---|---|
size_pressure | El pool estaba lleno y soltó la entrada usada menos recientemente que no estaba haciendo nada |
size_pressure_busy | El 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_credential | La 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_credential | GitLab 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_credential | La revalidación periódica encontró que GitLab rechazaba la credencial |
below_minimum | GitLab 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) |
rebuild | Falló 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 subscriptionLas 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_reason | Por qué |
|---|---|
safe_mode | el modo seguro respondió una escritura con una previsualización |
needs_confirmation | una acción destructiva sin confirmar |
invalid_params | los parámetros no encajan con la acción |
unknown_action | no existe ninguna acción con ese nombre en esta superficie |
rate_limited | el propio límite de tasa del despliegue rechazó la llamada |
fine_grained | un 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.
Registros
Sección titulada «Registros»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.
| Ajuste | Qué hace |
|---|---|
GITLAB_MCP_TELEMETRY_IDENTITY_KEY | un 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_ROTATION | cuá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.
Qué implica poner la clave
Sección titulada «Qué implica poner la clave»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.
Qué no se registra nunca
Sección titulada «Qué no se registra nunca»Ni por defecto ni con ningún ajuste, porque no existe tal ajuste:
- Argumentos y resultados de las herramientas. Las convenciones definen
atributos
Opt-Inpara 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
Hostde 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.
Registrar quién hizo la llamada
Sección titulada «Registrar quién hizo la llamada»Desactivado por defecto, en tres escalones:
--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.nameo 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.
Cómo desactivarlo
Sección titulada «Cómo desactivarlo»--telemetry=false # no lo actives; es el valor por defectoOTEL_SDK_DISABLED=true # vétalo desde el entorno, diga lo que diga la banderaOTEL_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.
Cómo saber si está activo
Sección titulada «Cómo saber si está activo»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.
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.
Cuando algo va mal
Sección titulada «Cuando algo va mal»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.
GITLAB_MCP_LOG_LEVEL=debug gitlab-mcp-server --telemetry 2>telemetry.logLos 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.
Estándares que sigue
Sección titulada «Estándares que sigue»| Área | Fuente |
|---|---|
| Variables de configuración | OTLP Exporter Configuration |
| Forma de spans y métricas | Convenciones semánticas de MCP |
| Niveles de requisito de atributos | Attribute Requirement Level |
| Registro de errores | Recording Errors |
| Atributos de identidad | User attributes |
Contexto de traza en _meta | Especificació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.