<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Otel on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/otel/</link><description>Recent content in Otel on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Wed, 27 May 2026 11:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/otel/index.xml" rel="self" type="application/rss+xml"/><item><title>Tracing LLM con OpenTelemetry GenAI: la caja negra del avión que el campo estabilizó en 2026</title><link>https://blog.lo0.es/posts/tracing-llm-otel-genai/</link><pubDate>Wed, 27 May 2026 11:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/tracing-llm-otel-genai/</guid><description>&lt;blockquote>
&lt;p>Este post complementa el de &lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">Prompt versioning con Langfuse y MLflow&lt;/a>, donde el &lt;code>prompt_id@version&lt;/code> aparecía como atributo de span sin explicar el pipeline entero, y el de &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs&lt;/a>, que distingue tracing de eval. Aquí entramos al dentro del tracing.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>OpenTelemetry GenAI, las semantic conventions &lt;code>gen_ai.*&lt;/code> y &lt;code>mcp.*&lt;/code>, son &lt;strong>estables desde finales de 2025&lt;/strong> y a mayo de 2026 son el sustrato sobre el que todo el ecosistema —Langfuse, Phoenix, LangSmith, Braintrust, Arize, Honeycomb, Datadog, New Relic— construye su observabilidad LLM. Eso significa que tu equipo instrumenta una sola vez, contra OTel, y elige backend después; no al revés. El pipeline canónico es &lt;strong>aplicación → OTel SDK (OpenLLMetry o openinference) → OTel Collector → backend(s)&lt;/strong>, con un sampling de dos capas (head-based del 1-5 % para mantener volumen viable, tail-based al 100 % sobre errores y latencias altas). Este post desmonta los atributos exactos que hay que rellenar en cada span (&lt;code>gen_ai.system&lt;/code>, &lt;code>request.model&lt;/code>, &lt;code>usage.input_tokens&lt;/code>, etc.), la diferencia operativa entre los tres SDKs habituales, y la anatomía de una traza real de chat-with-RAG-and-tool con todos los spans hijos.&lt;/p>
&lt;h2 id="estás-aquí-observe">Estás aquí: OBSERVE&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#c9a8e9;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#otm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#otm)}&lt;/style>
&lt;defs>&lt;marker id="otm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: OBSERVE · tracing con OpenTelemetry GenAI&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box active"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-la-caja-negra-del-avión">La analogía: la caja negra del avión&lt;/h2>
&lt;p>Una caja negra de avión hace tres cosas a la vez:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Graba todo&lt;/strong> sin necesidad de que alguien lo pida explícitamente. Cada parámetro de vuelo, cada conversación de cabina, cada acción de los pilotos queda registrada con marca temporal precisa y formato estandarizado.&lt;/li>
&lt;li>&lt;strong>Usa un formato que conoce todo el mundo&lt;/strong>. Cualquier investigador de accidentes aéreos del mundo puede leer la caja negra de cualquier fabricante, porque el schema está acordado entre todos. No hay &amp;ldquo;caja negra Boeing&amp;rdquo; y &amp;ldquo;caja negra Airbus&amp;rdquo;: hay una sola caja negra.&lt;/li>
&lt;li>&lt;strong>Sobrevive al evento&lt;/strong>: se diseña asumiendo que la nave puede caer. No depende del propio avión para almacenarse ni para ser leída.&lt;/li>
&lt;/ol>
&lt;p>El tracing LLM con OTel GenAI hace exactamente esas tres cosas:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Graba todo&lt;/strong> automáticamente. Cada llamada al modelo, cada paso de un agente, cada tool call, cada retrieval de RAG, queda como &lt;strong>span&lt;/strong> —una unidad de trabajo con inicio, fin, atributos y relación parent/child con otros spans—.&lt;/li>
&lt;li>&lt;strong>Usa un formato común&lt;/strong>. Los atributos &lt;code>gen_ai.request.model&lt;/code>, &lt;code>gen_ai.usage.input_tokens&lt;/code>, &lt;code>gen_ai.response.finish_reasons&lt;/code>, etc., son &lt;strong>acordados por el grupo de trabajo OpenTelemetry GenAI&lt;/strong> y los respetan todos los backends.&lt;/li>
&lt;li>&lt;strong>Sobrevive al backend&lt;/strong>. Los spans no viajan directamente a Langfuse: viajan a un &lt;strong>OTel Collector&lt;/strong> intermedio que se encarga de buffering, sampling y fan-out a 1, 2 o N backends. Si Langfuse cae, el Collector retiene los spans.&lt;/li>
&lt;/ol>
&lt;p>La analogía no es decorativa: es la única forma de entender por qué OTel GenAI ganó al ecosistema de SDKs propietarios que cada vendor traía en 2023-2024. &lt;strong>La caja negra estandarizada gana sobre la grabadora propietaria, siempre.&lt;/strong>&lt;/p>
&lt;h2 id="por-qué-otel-genai-existe-y-por-qué-ganó">Por qué OTel GenAI existe (y por qué ganó)&lt;/h2>
&lt;p>El estado de las cosas a finales de 2024 era roto: cada vendor traía su propio SDK con sus propios nombres de atributos. LangSmith llamaba &lt;code>model_name&lt;/code>, OpenLLMetry llamaba &lt;code>llm.model&lt;/code>, Helicone llamaba &lt;code>model&lt;/code>. Si querías cambiar de proveedor de observabilidad, &lt;strong>re-instrumentabas la aplicación entera&lt;/strong>. Para equipos serios, eso era inaceptable.&lt;/p>
&lt;p>OpenTelemetry tenía ya un marco para resolver esto: las &lt;strong>semantic conventions&lt;/strong>. Un grupo de trabajo (OpenTelemetry GenAI SIG) propuso un schema. En 2025 alcanzó &amp;ldquo;Stable&amp;rdquo; para los atributos de la modalidad básica (chat completions + tool calls), y en 2026 cubre también modalidades multimodales, embeddings y reasoning.&lt;/p>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 290" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline OTel GenAI">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.bs{fill:#ffe6d6}
.bc{fill:#d6eaff}
.bb{fill:#d9f5d6}
.bm{fill:#fff5b0}
.t{font:700 13px sans-serif;fill:#222}
.s{font:400 11px sans-serif;fill:#555}
.h{font:700 14px sans-serif;fill:#222}
.ar{stroke:#666;stroke-width:1.6;fill:none;marker-end:url(#ot1)}
.aw{stroke:#27ae60;stroke-width:1.8;fill:none;marker-end:url(#ot2)}
&lt;/style>
&lt;defs>
&lt;marker id="ot1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>
&lt;marker id="ot2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#27ae60"/>&lt;/marker>
&lt;/defs>
&lt;text x="380" y="22" text-anchor="middle" class="h">Pipeline OTel GenAI: instrumentas una vez, eliges backend después&lt;/text>
&lt;rect x="30" y="50" width="160" height="80" class="bx bs"/>
&lt;text x="110" y="74" text-anchor="middle" class="t">Aplicación&lt;/text>
&lt;text x="110" y="92" text-anchor="middle" class="s">Python / Node / Go&lt;/text>
&lt;text x="110" y="108" text-anchor="middle" class="s">+ OpenLLMetry&lt;/text>
&lt;text x="110" y="124" text-anchor="middle" class="s">o openinference SDK&lt;/text>
&lt;rect x="230" y="50" width="160" height="80" class="bx bc"/>
&lt;text x="310" y="74" text-anchor="middle" class="t">OTel Collector&lt;/text>
&lt;text x="310" y="92" text-anchor="middle" class="s">buffering + sampling +&lt;/text>
&lt;text x="310" y="108" text-anchor="middle" class="s">batching + retry +&lt;/text>
&lt;text x="310" y="124" text-anchor="middle" class="s">routing por exporter&lt;/text>
&lt;rect x="430" y="20" width="160" height="40" class="bx bb"/>
&lt;text x="510" y="38" text-anchor="middle" class="t">Langfuse&lt;/text>
&lt;text x="510" y="52" text-anchor="middle" class="s">UI LLM-first, eval, prompts&lt;/text>
&lt;rect x="430" y="80" width="160" height="40" class="bx bb"/>
&lt;text x="510" y="98" text-anchor="middle" class="t">Tempo / Jaeger&lt;/text>
&lt;text x="510" y="112" text-anchor="middle" class="s">distributed tracing infra&lt;/text>
&lt;rect x="430" y="140" width="160" height="40" class="bx bm"/>
&lt;text x="510" y="158" text-anchor="middle" class="t">Prometheus&lt;/text>
&lt;text x="510" y="172" text-anchor="middle" class="s">métricas agregadas&lt;/text>
&lt;rect x="430" y="200" width="160" height="40" class="bx bm"/>
&lt;text x="510" y="218" text-anchor="middle" class="t">ClickHouse / OpenSearch&lt;/text>
&lt;text x="510" y="232" text-anchor="middle" class="s">storage de logs y eventos&lt;/text>
&lt;rect x="630" y="80" width="100" height="40" class="bx bb"/>
&lt;text x="680" y="98" text-anchor="middle" class="t">Phoenix&lt;/text>
&lt;text x="680" y="112" text-anchor="middle" class="s">opcional, eval-first&lt;/text>
&lt;rect x="630" y="140" width="100" height="40" class="bx bb"/>
&lt;text x="680" y="158" text-anchor="middle" class="t">Datadog/NR&lt;/text>
&lt;text x="680" y="172" text-anchor="middle" class="s">infra-side&lt;/text>
&lt;path class="ar" d="M190,90 L230,90"/>
&lt;path class="ar" d="M390,90 L430,40"/>
&lt;path class="ar" d="M390,90 L430,100"/>
&lt;path class="ar" d="M390,90 L430,160"/>
&lt;path class="ar" d="M390,100 L430,220"/>
&lt;path class="aw" d="M590,100 L630,100"/>
&lt;path class="aw" d="M590,160 L630,160"/>
&lt;text x="110" y="170" text-anchor="middle" class="s" fill="#c0392b">SDK añade spans automáticamente&lt;/text>
&lt;text x="110" y="184" text-anchor="middle" class="s" fill="#c0392b">con gen_ai.* attributes&lt;/text>
&lt;text x="310" y="170" text-anchor="middle" class="s" fill="#27ae60">cambias backend tocando solo&lt;/text>
&lt;text x="310" y="184" text-anchor="middle" class="s" fill="#27ae60">el exporter del Collector&lt;/text>
&lt;text x="380" y="265" text-anchor="middle" class="s" font-style="italic">Si Langfuse cae, el Collector retiene los spans y los re-emite cuando vuelve.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Lo que cambia en la práctica:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Antes (SDKs propietarios)&lt;/strong>: cambias de Langfuse a Phoenix → re-instrumentas, re-deployas todo, pierdes el histórico.&lt;/li>
&lt;li>&lt;strong>Ahora (OTel)&lt;/strong>: cambias de Langfuse a Phoenix → editas el exporter en el &lt;code>otel-collector-config.yaml&lt;/code>, recargas el Collector, los spans nuevos van al destino nuevo sin tocar una línea de código de la aplicación.&lt;/li>
&lt;/ul>
&lt;h2 id="los-atributos-canónicos-gen_ai">Los atributos canónicos &lt;code>gen_ai.*&lt;/code>&lt;/h2>
&lt;p>A mayo de 2026 los atributos estables —es decir, que &lt;strong>no van a cambiar&lt;/strong>— son los que aparecen en la tabla. Hay más en estado &amp;ldquo;experimental&amp;rdquo; (multimodal, reasoning) y otros en &amp;ldquo;deprecated&amp;rdquo; (que se mantienen por compatibilidad).&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Atributo&lt;/th>
&lt;th>Significado&lt;/th>
&lt;th>Ejemplo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gen_ai.system&lt;/code>&lt;/td>
&lt;td>Familia del proveedor&lt;/td>
&lt;td>&lt;code>openai&lt;/code>, &lt;code>anthropic&lt;/code>, &lt;code>vllm&lt;/code>, &lt;code>huggingface&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.model&lt;/code>&lt;/td>
&lt;td>Modelo solicitado&lt;/td>
&lt;td>&lt;code>gpt-4o&lt;/code>, &lt;code>meta-llama/Llama-3.1-8B-Instruct&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.temperature&lt;/code>&lt;/td>
&lt;td>Sampling temp&lt;/td>
&lt;td>&lt;code>0.7&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.max_tokens&lt;/code>&lt;/td>
&lt;td>Cap de output&lt;/td>
&lt;td>&lt;code>1024&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.request.top_p&lt;/code>&lt;/td>
&lt;td>Nucleus sampling&lt;/td>
&lt;td>&lt;code>0.95&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.response.model&lt;/code>&lt;/td>
&lt;td>Modelo que respondió (puede diferir)&lt;/td>
&lt;td>&lt;code>gpt-4o-2024-08-06&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.response.id&lt;/code>&lt;/td>
&lt;td>ID de la respuesta del proveedor&lt;/td>
&lt;td>&lt;code>chatcmpl-9xY...&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.response.finish_reasons&lt;/code>&lt;/td>
&lt;td>Razones de fin&lt;/td>
&lt;td>&lt;code>[&amp;quot;stop&amp;quot;]&lt;/code>, &lt;code>[&amp;quot;length&amp;quot;]&lt;/code>, &lt;code>[&amp;quot;tool_calls&amp;quot;]&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.input_tokens&lt;/code>&lt;/td>
&lt;td>Tokens entrada&lt;/td>
&lt;td>&lt;code>1247&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.usage.output_tokens&lt;/code>&lt;/td>
&lt;td>Tokens salida&lt;/td>
&lt;td>&lt;code>412&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.operation.name&lt;/code>&lt;/td>
&lt;td>Tipo de operación&lt;/td>
&lt;td>&lt;code>chat&lt;/code>, &lt;code>text_completion&lt;/code>, &lt;code>embeddings&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.prompt.id&lt;/code>&lt;/td>
&lt;td>ID del prompt versionado&lt;/td>
&lt;td>&lt;code>customer_support_v3&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.prompt.version&lt;/code>&lt;/td>
&lt;td>Versión específica&lt;/td>
&lt;td>&lt;code>14&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.prompt.label&lt;/code>&lt;/td>
&lt;td>Etiqueta semántica&lt;/td>
&lt;td>&lt;code>production&lt;/code>, &lt;code>staging&lt;/code>, &lt;code>canary&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.conversation.id&lt;/code>&lt;/td>
&lt;td>ID de conversación multiturn&lt;/td>
&lt;td>&lt;code>session_abc123&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Y para tool calls (función desde el modelo), los atributos &lt;code>gen_ai.tool.*&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Atributo&lt;/th>
&lt;th>Significado&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>gen_ai.tool.call.id&lt;/code>&lt;/td>
&lt;td>ID del tool call concreto&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.tool.name&lt;/code>&lt;/td>
&lt;td>Nombre de la herramienta&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.tool.type&lt;/code>&lt;/td>
&lt;td>&lt;code>function&lt;/code>, &lt;code>mcp&lt;/code>, etc.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>gen_ai.tool.description&lt;/code>&lt;/td>
&lt;td>Descripción breve&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Y para tool calls vía MCP (Model Context Protocol), los atributos &lt;code>mcp.*&lt;/code> añaden capa específica (&lt;code>mcp.server.name&lt;/code>, &lt;code>mcp.method&lt;/code>, &lt;code>mcp.session.id&lt;/code>, etc.) que conviven con los &lt;code>gen_ai.*&lt;/code>.&lt;/p>
&lt;p>&lt;strong>La regla práctica&lt;/strong>: si tu instrumentación rellena estos atributos en cada span, el dashboard de Langfuse, el grafo de Tempo y los métricas de Prometheus salen &amp;ldquo;gratis&amp;rdquo; porque los tres saben leer la convención.&lt;/p>
&lt;h2 id="el-pipeline-canónico-sdk--collector--backends">El pipeline canónico: SDK → Collector → backends&lt;/h2>
&lt;p>Tres capas, cada una con su responsabilidad:&lt;/p>
&lt;h3 id="1--el-sdk">1 · El SDK&lt;/h3>
&lt;p>Tres opciones dominantes en 2026:&lt;/p>
&lt;p>&lt;strong>OpenLLMetry (Traceloop)&lt;/strong> — el más extendido. Instrumenta automáticamente OpenAI Python SDK, Anthropic SDK, LangChain, LlamaIndex, Haystack, Cohere, Mistral, vLLM y casi cualquier librería del ecosistema. Una sola línea (&lt;code>Traceloop.init()&lt;/code>) instrumenta todo. Licencia Apache 2.0. Mantiene su propio fork con extensiones que aún no están en OTel core, pero exporta cumpliendo la convención.&lt;/p>
&lt;p>&lt;strong>openinference (Arize)&lt;/strong> — competidor directo, también Apache 2.0. Más cercano a Phoenix (mismo vendor) pero exporta OTel estándar. Mejor instrumentación para LangChain/LlamaIndex/DSPy en algunas versiones; peor para vLLM directo.&lt;/p>
&lt;p>&lt;strong>Langfuse SDK&lt;/strong> — propietario en la forma (la API es de Langfuse), pero por dentro emite spans OTel. Lo natural si Langfuse es el backend principal. Tiene el mejor soporte para &amp;ldquo;session&amp;rdquo; y &amp;ldquo;user&amp;rdquo; linking, conceptos que no están aún en OTel core pero que Langfuse mapea a &lt;code>gen_ai.conversation.id&lt;/code> para compatibilidad.&lt;/p>
&lt;p>Recomendación práctica para mayo 2026: &lt;strong>OpenLLMetry si quieres backend-agnostic&lt;/strong> (cambiarás de proveedor); &lt;strong>Langfuse SDK si Langfuse ya es tu apuesta&lt;/strong> (te ahorras un mapeo). Los dos producen spans válidos que cualquier Collector consume.&lt;/p>
&lt;h3 id="2--el-otel-collector">2 · El OTel Collector&lt;/h3>
&lt;p>El Collector es la &lt;strong>pieza más importante&lt;/strong> del pipeline y la menos hablada. Tres responsabilidades:&lt;/p>
&lt;p>&lt;strong>Buffering&lt;/strong>: si Langfuse o Tempo tienen un problema, el Collector retiene los spans en memoria (o en disco con persistencia activada) y los re-emite cuando el backend vuelve.&lt;/p>
&lt;p>&lt;strong>Sampling&lt;/strong>: aplica las dos capas de sampling (siguiente sección) sin que la aplicación se entere.&lt;/p>
&lt;p>&lt;strong>Routing&lt;/strong>: con la config &lt;code>exporters&lt;/code> apunta a uno o varios backends a la vez. La práctica habitual es &lt;strong>enviar a Langfuse + Tempo + Prometheus simultáneamente&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>Langfuse para UI LLM-first (prompts, evals, sesiones de usuario).&lt;/li>
&lt;li>Tempo (o Jaeger) para el contexto distribuido completo (un span LLM dentro de un request HTTP que ha tocado 12 microservicios).&lt;/li>
&lt;li>Prometheus para métricas agregadas (latencia P95 por modelo, tokens/segundo, error rate).&lt;/li>
&lt;/ul>
&lt;p>Un fragmento de &lt;code>otel-collector-config.yaml&lt;/code> representativo en producción:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">protocols&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">grpc&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">http&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">4318&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">batch&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">send_batch_size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1024&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tail_sampling&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">decision_wait&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">policies&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">errors&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">status_code&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">status_code&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">status_codes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">ERROR] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">slow_traces&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">latency&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">latency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">threshold_ms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5000&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">head_5pct&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">probabilistic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">probabilistic&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">sampling_percentage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlphttp/langfuse&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://langfuse.internal:3000/api/public/otel/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">headers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">Authorization&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Basic ${LANGFUSE_AUTH}&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">otlp/tempo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">tempo:4317&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tls&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">insecure&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">endpoint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.0.0.0&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="m">8889&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">service&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">pipelines&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">traces&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">tail_sampling, batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlphttp/langfuse, otlp/tempo]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">receivers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">otlp]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">processors&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">batch]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">exporters&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">prometheus]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3--los-backends">3 · Los backends&lt;/h3>
&lt;p>&lt;strong>Langfuse&lt;/strong> es el dominante para LLM observability OSS (licencia MIT, suite completa: tracing + evals + prompts + datasets). Su modelo asume &lt;code>trace_id&lt;/code> y &lt;code>span_id&lt;/code> de OTel y los pinta en UI con el contexto LLM expandido (mensajes formateados, tokens contados, costes calculados con un mapeo de modelo→precio).&lt;/p>
&lt;p>&lt;strong>Phoenix&lt;/strong> (Arize, licencia ELv2) tiene un foco más eval-first. Útil si tu equipo viene de NLP clásico y quiere visualización de embeddings UMAP por defecto.&lt;/p>
&lt;p>&lt;strong>LangSmith&lt;/strong> (LangChain) es propietario, no OSS. Excelente integración con LangChain/LangGraph; menos relevante si no usas ese stack.&lt;/p>
&lt;p>&lt;strong>Tempo / Jaeger&lt;/strong> muestran las trazas LLM &lt;strong>dentro del contexto del request distribuido&lt;/strong>. Es lo que necesitas cuando un cliente reporta que &amp;ldquo;la página tarda 8 segundos&amp;rdquo; y quieres ver si los 8 segundos son el modelo, el retrieval o el microservicio aguas abajo.&lt;/p>
&lt;h2 id="sampling-las-dos-capas-que-conviven">Sampling: las dos capas que conviven&lt;/h2>
&lt;p>Sin sampling, un sistema con 10 req/s genera ~26 millones de spans/mes. El backend se ahoga, el coste explota y la mayoría de los spans son redundantes (200 requests parecidas no informan más que 20).&lt;/p>
&lt;p>El patrón canónico en 2026 son &lt;strong>dos capas combinadas&lt;/strong>:&lt;/p>
&lt;h3 id="head-based-sampling-al-entrar">Head-based sampling (al entrar)&lt;/h3>
&lt;p>Se decide &lt;strong>antes&lt;/strong> de que el span exista si vas a trazarlo o no, basado en una decisión probabilística (1-5 % típico) o en una regla determinista (tracear siempre si el usuario es premium, si el modelo es el nuevo en canary, etc.).&lt;/p>
&lt;p>Pros: barato (no se genera el span en absoluto), determinista.&lt;/p>
&lt;p>Contras: &lt;strong>te puedes perder errores poco frecuentes&lt;/strong>. Si un error pasa 1 vez de cada 1000 y trazas el 1 %, lo verás 1 vez de cada 100 000 — ya no es debuggable.&lt;/p>
&lt;h3 id="tail-based-sampling-al-salir">Tail-based sampling (al salir)&lt;/h3>
&lt;p>Se genera el span &lt;strong>completo&lt;/strong> y se decide al final si conservarlo o tirarlo. La decisión puede ser:&lt;/p>
&lt;ul>
&lt;li>Conservar el 100 % de los errores.&lt;/li>
&lt;li>Conservar el 100 % de las trazas con latencia &amp;gt; X ms.&lt;/li>
&lt;li>Conservar el 100 % de las trazas que activaron un guardrail.&lt;/li>
&lt;li>Conservar una muestra aleatoria del resto (1-5 %).&lt;/li>
&lt;/ul>
&lt;p>Pros: garantiza que ves los errores y los outliers de latencia. Sin él, los problemas más interesantes son los que más probabilidad tienen de quedarse fuera.&lt;/p>
&lt;p>Contras: necesitas un &lt;strong>buffer&lt;/strong> porque hasta que la traza no termina no puedes decidir. El procesador &lt;code>tail_sampling&lt;/code> del Collector mantiene los spans 5-30 segundos en memoria hasta tomar la decisión.&lt;/p>
&lt;p>La regla práctica en 2026 es &lt;strong>combinar ambos&lt;/strong>: head-based al 5 % + tail-based capturando el 100 % de errores y latencias &amp;gt; 5s. Eso te da ~5 % de tráfico baseline + el 100 % de lo interesante.&lt;/p>
&lt;h2 id="anatomía-de-una-traza-real">Anatomía de una traza real&lt;/h2>
&lt;p>Vamos a desmontar la traza de una sola pregunta de usuario a un sistema típico chat-with-RAG-and-tool. La pregunta es &amp;ldquo;¿cuál es el saldo de mi cuenta principal?&amp;rdquo;.&lt;/p>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Anatomía de una traza LLM completa">
&lt;style>
.tr{fill:#f4f4f4;stroke:#999;stroke-width:1}
.s1{fill:#d6eaff;stroke:#222;stroke-width:1}
.s2{fill:#ffe9d6;stroke:#222;stroke-width:1}
.s3{fill:#d9f5d6;stroke:#222;stroke-width:1}
.s4{fill:#fff5b0;stroke:#222;stroke-width:1}
.s5{fill:#e9d6f5;stroke:#222;stroke-width:1}
.lb{font:600 11px sans-serif;fill:#222}
.sm{font:400 10px sans-serif;fill:#444}
.h{font:700 13px sans-serif;fill:#222}
.tm{font:400 10px sans-serif;fill:#888}
&lt;/style>
&lt;text x="380" y="20" text-anchor="middle" class="h">Trace_id = abc123 · "¿cuál es el saldo de mi cuenta principal?"&lt;/text>
&lt;text x="380" y="38" text-anchor="middle" class="tm">duración total: 2 847 ms · 7 spans · 1 trace&lt;/text>
&lt;line x1="60" y1="55" x2="700" y2="55" stroke="#ccc"/>
&lt;text x="60" y="68" class="tm">0 ms&lt;/text>
&lt;text x="700" y="68" text-anchor="end" class="tm">2847 ms&lt;/text>
&lt;rect x="60" y="80" width="640" height="22" class="s1"/>
&lt;text x="70" y="96" class="lb">HTTP POST /chat — span root (app)&lt;/text>
&lt;rect x="80" y="112" width="100" height="20" class="s2"/>
&lt;text x="86" y="126" class="sm">guardrail.input · 38ms&lt;/text>
&lt;rect x="190" y="112" width="220" height="20" class="s3"/>
&lt;text x="196" y="126" class="sm">rag.retrieve · 218ms · BGE-M3 + Qdrant&lt;/text>
&lt;rect x="190" y="136" width="80" height="18" class="s3" opacity="0.7"/>
&lt;text x="195" y="149" class="sm">embed · 28ms&lt;/text>
&lt;rect x="280" y="136" width="125" height="18" class="s3" opacity="0.7"/>
&lt;text x="285" y="149" class="sm">vector_search · 80ms&lt;/text>
&lt;rect x="420" y="112" width="280" height="20" class="s4"/>
&lt;text x="426" y="126" class="lb">gen_ai.chat · 2104ms · llama-3.1-70b-instruct&lt;/text>
&lt;rect x="510" y="136" width="80" height="18" class="s5" opacity="0.85"/>
&lt;text x="515" y="149" class="sm">tool.get_balance · 60ms&lt;/text>
&lt;rect x="600" y="112" width="60" height="20" class="s2"/>
&lt;text x="606" y="126" class="sm">guardrail.output · 28ms&lt;/text>
&lt;line x1="60" y1="170" x2="700" y2="170" stroke="#ccc"/>
&lt;text x="60" y="190" class="h">Atributos clave por span&lt;/text>
&lt;text x="60" y="208" class="sm">root: &lt;tspan font-family="monospace">http.method=POST, user.id=42, conversation.id=session_abc&lt;/tspan>&lt;/text>
&lt;text x="60" y="224" class="sm">guardrail.input: &lt;tspan font-family="monospace">gen_ai.tool.name=llm_guard, gen_ai.tool.type=guardrail, gen_ai.guardrail.decision=allow&lt;/tspan>&lt;/text>
&lt;text x="60" y="240" class="sm">rag.retrieve: &lt;tspan font-family="monospace">gen_ai.operation.name=embeddings, gen_ai.request.model=bge-m3, db.system=qdrant&lt;/tspan>&lt;/text>
&lt;text x="60" y="256" class="sm">vector_search: &lt;tspan font-family="monospace">db.operation=query, db.collection=docs_prod, db.qdrant.top_k=20, db.qdrant.score=0.83&lt;/tspan>&lt;/text>
&lt;text x="60" y="272" class="sm">gen_ai.chat: &lt;tspan font-family="monospace">gen_ai.system=vllm, request.model=llama-3.1-70b, usage.input=1247, usage.output=412&lt;/tspan>&lt;/text>
&lt;text x="60" y="288" class="sm"> prompt.id=customer_support_v3, prompt.version=14, prompt.label=production&lt;/text>
&lt;text x="60" y="304" class="sm">tool.get_balance: &lt;tspan font-family="monospace">gen_ai.tool.name=get_balance, gen_ai.tool.type=function, gen_ai.tool.call.id=call_xy&lt;/tspan>&lt;/text>
&lt;text x="60" y="320" class="sm">guardrail.output: &lt;tspan font-family="monospace">gen_ai.tool.name=llama_guard_4, gen_ai.guardrail.decision=allow&lt;/tspan>&lt;/text>
&lt;text x="60" y="344" class="sm" font-style="italic">Todos los spans comparten trace_id=abc123. Langfuse los pinta como árbol; Tempo como timeline plana.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Siete spans, una sola traza. La información que se puede explotar:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Latencia descompuesta&lt;/strong>: 38 ms guardrail + 218 ms retrieval + 2104 ms LLM + 60 ms tool + 28 ms guardrail = 2 448 ms (el resto es overhead de orquestación). El cuello de botella es el LLM (74 % del tiempo). Si el usuario se queja de lentitud, sabes dónde mirar.&lt;/li>
&lt;li>&lt;strong>Tokens y coste&lt;/strong>: 1 247 input + 412 output. Con Llama 3.1 70B on-prem a ~12 W/token de coste energético equivalente, ~3.5 c€ por respuesta. Multiplicado por volumen, da factura de inferencia.&lt;/li>
&lt;li>&lt;strong>&lt;code>prompt.id+version&lt;/code> viajando&lt;/strong>: si en un mes notas que la calidad ha caído, filtras por &lt;code>prompt.version&lt;/code> y ves si coincide con un cambio del prompt.&lt;/li>
&lt;li>&lt;strong>Guardrail decisions trazadas&lt;/strong>: si un guardrail bloqueó la respuesta, queda en la traza con &lt;code>gen_ai.guardrail.decision=block&lt;/code> y razón. Auditoría ENS satisfecha.&lt;/li>
&lt;li>&lt;strong>Tool calls correlacionados&lt;/strong>: &lt;code>tool.get_balance&lt;/code> es un span hijo del &lt;code>gen_ai.chat&lt;/code>. Si la herramienta falló, ves directamente el error en su span, no aparece sólo en logs separados.&lt;/li>
&lt;/ul>
&lt;h2 id="implicaciones-en-hardware-on-premise">Implicaciones en hardware on-premise&lt;/h2>
&lt;p>El tracing &lt;strong>no es gratis&lt;/strong>, pero su overhead bien dimensionado es despreciable.&lt;/p>
&lt;h3 id="coste-por-span">Coste por span&lt;/h3>
&lt;p>A nivel SDK, un span LLM añade ~50-200 µs de overhead (creación, atributos, serialización). Para un LLM call que dura 2 segundos, eso es &amp;lt; 0.01 % de la latencia.&lt;/p>
&lt;p>A nivel red, un span típico OTLP comprimido pesa ~1-3 KB. Para un sistema con 100 req/s con 7 spans/request, son 700 spans/s × 2 KB ≈ 1.4 MB/s al Collector. Trivial.&lt;/p>
&lt;h3 id="storage">Storage&lt;/h3>
&lt;p>Es donde se va el dinero. Sin sampling, un mes a 100 req/s ≈ 18 GB de spans. Con head-based 5 % + tail-based de errores ≈ 1-2 GB/mes. Manejable en cualquier ClickHouse o Loki autohospedado.&lt;/p>
&lt;h3 id="en-una-rtx-4090-24-gb--collector-y-langfuse-on-prem">En una RTX 4090 (24 GB) + Collector y Langfuse on-prem&lt;/h3>
&lt;p>Para un servicio de demo o un single-tenant pequeño: Langfuse + Postgres + Clickhouse + Collector en el mismo host del modelo. Consume ~8 GB RAM y &amp;lt; 1 % de CPU continuo. La 4090 sirve el modelo, el resto vive en CPU. Setup mínimo viable para una startup o un equipo pequeño.&lt;/p>
&lt;h3 id="en-un-cluster-genérico-4h100-sxm--observabilidad-central">En un cluster genérico 4×H100 SXM + observabilidad central&lt;/h3>
&lt;p>Pod dedicado para el OTel Collector (DaemonSet en cada nodo + Gateway central). Langfuse + ClickHouse + Tempo + Prometheus + Grafana en un namespace &lt;code>observability&lt;/code> separado del namespace &lt;code>serving&lt;/code>. Es la arquitectura canónica de cualquier producción ENS/NIS2 seria: la observabilidad &lt;strong>no comparte recursos con el modelo&lt;/strong>.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Métricas LLM (Prometheus): los counters y histograms canónicos&lt;/strong> — &lt;code>gen_ai_client_request_duration_seconds&lt;/code>, &lt;code>gen_ai_client_input_tokens_total&lt;/code>, etc. Cómo se construyen dashboards Grafana sobre esa base.&lt;/li>
&lt;li>&lt;strong>Guardrails como spans&lt;/strong>: cómo modelar Llama Guard 4, NeMo Guardrails y LLM Guard como spans hijos del span LLM, y qué atributos llevan.&lt;/li>
&lt;li>&lt;strong>Tracing distribuido con MCP&lt;/strong> — propagación &lt;code>traceparent&lt;/code> desde el cliente LLM hasta el servidor MCP, atributos &lt;code>mcp.*&lt;/code>, problemas conocidos.&lt;/li>
&lt;li>&lt;strong>eBPF para tracing automático sin SDK&lt;/strong> — Tetragon y Hubble extrayendo trazas LLM sin instrumentación explícita, para casos donde no se puede modificar el código.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/prompt-versioning-langfuse-mlflow/">Prompt versioning con Langfuse y MLflow&lt;/a> — el &lt;code>prompt_id@version&lt;/code> que viaja como &lt;code>gen_ai.prompt.id&lt;/code> en los spans de aquí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs: la capa después del tracing&lt;/a> — el patrón &amp;ldquo;el judge corre sobre una muestra de las trazas&amp;rdquo; se apoya en este pipeline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/llm-as-judge-fundamentos/">LLM-as-judge: el corrector de oposiciones&lt;/a> — el judge consume trazas para evaluar la calidad continua. Las trazas conservadas por tail-sampling son su input.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — las trazas son el origen del dataset de fine-tuning. Cuando un usuario regenera, ese span con &lt;code>gen_ai.response.finish_reasons=[&amp;quot;stop&amp;quot;]&lt;/code> rejected entra al pipeline DPO.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro donde OBSERVE es la etapa 5.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>OpenTelemetry GenAI Semantic Conventions: &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">https://opentelemetry.io/docs/specs/semconv/gen-ai/&lt;/a>.&lt;/li>
&lt;li>OpenLLMetry (Traceloop): &lt;a href="https://github.com/traceloop/openllmetry">https://github.com/traceloop/openllmetry&lt;/a>.&lt;/li>
&lt;li>openinference (Arize): &lt;a href="https://github.com/Arize-ai/openinference">https://github.com/Arize-ai/openinference&lt;/a>.&lt;/li>
&lt;li>Langfuse OTel integration: &lt;a href="https://langfuse.com/docs/opentelemetry/get-started">https://langfuse.com/docs/opentelemetry/get-started&lt;/a>.&lt;/li>
&lt;li>OTel Collector tail-sampling processor: &lt;a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor">https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor&lt;/a>.&lt;/li>
&lt;li>Phoenix (Arize) docs: &lt;a href="https://docs.arize.com/phoenix">https://docs.arize.com/phoenix&lt;/a>.&lt;/li>
&lt;li>Model Context Protocol observability (&lt;code>mcp.*&lt;/code> semconv): &lt;a href="https://modelcontextprotocol.io/docs/observability">https://modelcontextprotocol.io/docs/observability&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>