OpenTelemetry en .NET desde cero: tu primera traza distribuida en 30 minutos
Instrumentar una API .NET y ver la traza completa de una petición, de extremo a extremo, en media hora. Con el código exacto.
En treinta minutos vas a tener una API .NET emitiendo trazas reales y un tablero donde puedes hacer clic en una petición y ver exactamente cuánto tardó cada tramo: el controlador, la consulta a base de datos, la llamada al servicio externo. Sin licencias, sin agentes propietarios y sin tocar tu lógica de negocio.
Este es el tutorial que hubiéramos querido encontrar la primera vez. Va directo al código que funciona, con las decisiones ya tomadas y las trampas señaladas donde están. Si quieres primero el panorama —qué son métricas, trazas y logs y por qué importan— empieza por la introducción a observabilidad en .NET y regresa aquí.
SDK de .NET 8 o superior, Docker corriendo en tu máquina, y una API que ya exista —aunque sea la plantilla de ejemplo—. No hace falta que sea un proyecto grande: entre más simple, mejor para el primer intento.
Los siete pasos
- 01Levantar el visor de trazas
- 02Instalar los paquetes
- 03Configurar en Program.cs
- 04Ver tu primera traza
- 05Completar el recorrido
- 06Agregar tus propios tramos
- 07Prepararlo para producción
Levantar el visor de trazas
Antes de instrumentar nada, ten a dónde mandar los datos. Dos opciones, ambas de un solo comando y sin configuración.
docker run --rm -it -p 18888:18888 -p 4317:18889 \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
# El panel queda en http://localhost:18888
# La consola imprime la URL con el token de acceso: úsala tal cual
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 -p 4317:4317 -p 4318:4318 \
jaegertracing/all-in-one:latest
# El panel queda en http://localhost:16686
Los dos escuchan OTLP en el puerto 4317, que es el estándar. Si empiezas al revés —instrumentando sin destino— vas a pasar veinte minutos dudando si el problema es tu código o la conexión. Con el visor arriba, cualquier error posterior tiene una sola causa posible.
Instalar los paquetes
Cinco paquetes. El primero es el núcleo; los demás son las instrumentaciones automáticas de cada tecnología.
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.AspNetCore
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package OpenTelemetry.Instrumentation.Runtime
# Si usas SQL Server, Azure SQL o cualquier cliente ADO.NET:
dotnet add package OpenTelemetry.Instrumentation.SqlClient --prerelease
--prerelease
La instrumentación de SqlClient ha vivido en versiones preliminares bastante más tiempo que las demás. Es estable en la práctica y se usa ampliamente en producción, pero revisa la versión disponible el día que lo instales y fíjala en el archivo del proyecto en lugar de dejarla flotando.
Configurar en Program.cs
Este es el bloque completo. Va antes de builder.Build() y es todo lo que necesitas para el primer resultado.
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r
.AddService(serviceName: "api-pedidos", serviceVersion: "1.0.0")
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName
}))
.WithTracing(t => t
.AddAspNetCoreInstrumentation(o =>
{
o.RecordException = true;
// no ensuciar con los sondeos de salud
o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health");
})
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.AddOtlpExporter())
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter());
var app = builder.Build();
Sin más configuración, el exportador manda a http://localhost:4317, que es justo donde está escuchando el contenedor del paso 1. Por eso no hay que decirle nada todavía.
AddService es el nombre con el que tu aplicación va a aparecer en el tablero: ponle el nombre real del servicio, no «MiApi». El atributo deployment.environment te permite después separar producción de pruebas en el mismo destino, y agradecerás tenerlo desde el día uno.
Ver tu primera traza
Corre la aplicación, haz tres o cuatro peticiones a cualquier endpoint y abre el tablero. Deberías ver el servicio listado y, al entrar, una traza por petición.
dotnet run
# En otra terminal, genera tráfico:
curl http://localhost:5000/api/pedidos
curl http://localhost:5000/api/pedidos/42
Tres causas cubren prácticamente todos los casos. Uno: el contenedor no está corriendo o el puerto 4317 está ocupado —verifica con docker ps—. Dos: estás en Linux o WSL y la aplicación corre dentro de otro contenedor, así que localhost no apunta al visor; usa el nombre del servicio o host.docker.internal. Tres: el exportador manda por lotes cada pocos segundos, así que espera unos instantes antes de concluir que falló.
Completar el recorrido
Hasta aquí ves la petición entrante como un solo bloque. Lo interesante aparece cuando se instrumentan las salidas: si tu código llama a otro servicio con HttpClient o consulta la base de datos, cada llamada se convierte en un tramo anidado con su propia duración.
Para HttpClient hay un detalle importante: la instrumentación solo aplica si el cliente viene de IHttpClientFactory. Un new HttpClient() a mano queda invisible, y además tiene otros problemas.
// En Program.cs
builder.Services.AddHttpClient("pagos", c =>
{
c.BaseAddress = new Uri("https://api.proveedor-pagos.mx/");
});
// En el servicio que lo usa
public class ServicioPagos(IHttpClientFactory factory)
{
public async Task<bool> CobrarAsync(decimal monto)
{
var http = factory.CreateClient("pagos");
var r = await http.PostAsJsonAsync("v1/cargos", new { monto });
return r.IsSuccessStatusCode;
}
}
Genera una petición que toque base de datos y servicio externo. Vas a ver la barra completa dividida: 12 ms tu código, 34 ms la consulta, 890 ms el proveedor de pagos. Esa imagen es la que convence a un equipo entero de que valió la pena.
La instrumentación de SQL permite capturar el texto completo de cada consulta. Es utilísimo para depurar y es un riesgo real de fuga: si tus consultas llevan valores en línea, vas a mandar datos de clientes al sistema de observabilidad. Actívalo solo en desarrollo, o asegúrate de que todo vaya parametrizado.
Agregar tus propios tramos
La instrumentación automática cubre la infraestructura. Lo que vale oro es marcar los pasos de tu negocio: validar inventario, calcular impuestos, aplicar reglas de descuento. Se hace con ActivitySource, que es parte de .NET, no de OpenTelemetry.
using System.Diagnostics;
using OpenTelemetry.Trace;
public class ProcesadorPedidos
{
// Una sola instancia estática por componente
private static readonly ActivitySource Fuente =
new("Pedidos.Procesador", "1.0.0");
public async Task<Resultado> ProcesarAsync(Pedido pedido)
{
using var actividad = Fuente.StartActivity("procesar-pedido");
// Atributos de negocio: baja cardinalidad, alto valor
actividad?.SetTag("pedido.canal", pedido.Canal);
actividad?.SetTag("pedido.lineas", pedido.Lineas.Count);
actividad?.SetTag("pedido.requiere_factura", pedido.RequiereFactura);
try
{
var resultado = await EjecutarAsync(pedido);
actividad?.SetStatus(ActivityStatusCode.Ok);
return resultado;
}
catch (Exception ex)
{
actividad?.SetStatus(ActivityStatusCode.Error, ex.Message);
actividad?.RecordException(ex);
throw;
}
}
}
Falta un paso que se olvida siempre: registrar la fuente. Si no la declaras, tus tramos no se exportan y no hay ningún error que te avise.
.WithTracing(t => t
.AddSource("Pedidos.Procesador") // ← el nombre debe coincidir exacto
.AddAspNetCoreInstrumentation(/* ... */)
// ... el resto igual
)
Etiqueta con valores de baja cardinalidad: canal, tipo de cliente, forma de pago, sucursal. El identificador del pedido está bien en una traza —son individuales por naturaleza—, pero nunca como dimensión de una métrica. Ahí es donde las facturas de observabilidad se descontrolan.
Prepararlo para producción
Lo que llevas funciona, pero tiene el destino escrito en el código y exporta el 100% de las trazas. Dos cambios y queda listo.
Primero: saca la configuración a variables de entorno. El SDK las lee solo, sin que agregues una línea.
OTEL_SERVICE_NAME=api-pedidos
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.interno:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# Muestreo: 10% de las trazas, respetando la decisión del servicio origen
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
# Atributos comunes a todo lo que emita el servicio
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=produccion,service.namespace=ventas
Segundo: no exportes directo desde cada aplicación al destino final. Manda todo a un Collector intermedio.
Absorbe los picos con su propia cola, te deja cambiar de destino sin volver a desplegar aplicaciones, permite filtrar o depurar atributos sensibles antes de que salgan, y habilita el muestreo por cola —quedarte con el 100% de las trazas con error y con un porcentaje del resto—, que es la única forma sensata de controlar el costo sin perder lo importante.
parentbased
Con varios servicios instrumentados, el muestreo debe decidirse una sola vez, al inicio del recorrido, y respetarse aguas abajo. Si cada servicio decide por su cuenta, terminas con trazas incompletas: el tramo de entrada existe y el del servicio siguiente no. Es de los errores más frustrantes de diagnosticar.
Los errores que vas a cometer
- —Olvidar
AddSource. Tus tramos manuales no aparecen y no hay mensaje de error. Si instrumentaste a mano y no ves nada, revisa esto primero. - —Instanciar
ActivitySourceen cada llamada. Debe serstatic readonly, una por componente. Crearla dentro del método funciona pero desperdicia recursos sin necesidad. - —No filtrar los sondeos de salud. Un
/healthcada cinco segundos genera 17 mil trazas inútiles al día por servicio. - —Usar
new HttpClient(). Queda fuera de la instrumentación y además agota sockets bajo carga. - —Meter identificadores en las métricas. El ID de pedido va en trazas y logs. En métricas, multiplica la cardinalidad y con ella la factura.
Verifica antes de darlo por hecho
Instrumentación completa
Imprimir o guardar como PDF- El servicio aparece en el tablero con su nombre real y su versión No «MiApi» ni «WebApplication1»
- Una petición genera una traza con tramos anidados, no un bloque único Si no hay anidamiento, faltan las instrumentaciones de salida
- Las consultas a base de datos aparecen como tramos con su duración AddSqlClientInstrumentation registrado
- Las llamadas HTTP salientes aparecen y muestran el host destino Y el cliente viene de IHttpClientFactory
- Una excepción marca la traza como fallida y guarda el detalle RecordException = true y SetStatus en el código propio
- Los sondeos de salud están excluidos Revisa el filtro en AddAspNetCoreInstrumentation
- La configuración vive en variables de entorno, no en el código OTEL_SERVICE_NAME y OTEL_EXPORTER_OTLP_ENDPOINT
- El texto de las consultas SQL no viaja con datos de clientes Consultas parametrizadas, o captura de texto apagada
Con estos ocho puntos ya tienes trazabilidad útil. El siguiente paso es correlacionar tus logs con el TraceId para saltar de un error al recorrido completo de esa petición.
Qué sigue
Con esto cubriste la primera etapa: ver. Lo que falta para que la observabilidad se vuelva parte de la operación es correlacionar los logs con el TraceId, definir qué significa «bien» con SLIs medibles, y construir alertas sobre síntomas en lugar de sobre causas. Cada uno tiene su guía en el blog.
Pero no avances hasta que este paso esté firme. Instrumentar dos servicios de verdad, con sus tramos de negocio marcados, vale más que instrumentar quince a medias.
La primera vez que ves dónde se fueron los tres segundos, dejas de discutir sobre rendimiento y empiezas a arreglarlo.
¿Instrumentaste una API y ahora tienes que hacerlo con quince?
Lo hacemos contigo. Definimos el estándar de instrumentación para tu stack, montamos el Collector con el muestreo y el filtrado de datos sensibles ya resueltos, instrumentamos los servicios críticos y dejamos a tu equipo capacitado para replicarlo en el resto.
- —Observabilidad y OpenTelemetry para .NET
- —Instrumentación de aplicaciones .NET Framework legacy
- —Diagnóstico de rendimiento, memoria y concurrencia
- —Mantenimiento y monitoreo de servidores