Proyectos de Software Grandes: Recolección de Trazas
Posted on October 28, 2025 • 6 minutes • 1213 words • Other languages: English
Este post es parte de mi serie de blogs sobre Proyectos de Software Grandes .
- Código Fuente
- El Foco del Post: Los Traces
- Librerías OpenTelemetry
- Instrumentación en Next.js: Registrando OpenTelemetry
- Configuración de Variables de Entorno OTel
- Configuración del OTel Collector
- Definir OTel Collector y Zipkin
- Visualización de Traces con Zipkin
- Resolviendo el Error de Producción
- ¿Qué Sigue?
Código Fuente
Todos los snippets de código que aparecen en este post están disponibles en la rama dedicada a este artículo en el repo de GitHub del proyecto:
https://github.com/franBec/tas/tree/feature/2025-10-28
El Foco del Post: Las Trazas
Nos vamos a enfocar en implementar la recolección de trazas:

Librerías OpenTelemetry
Necesitamos instalar los siguientes paquetes:
@vercel/otel: Esta es la librería oficial de distribución de OpenTelemetry de Vercel para Next.js, simplificando la creación de tracing y spans dentro del framework.@opentelemetry/sdk-logs/@opentelemetry/api-logs/@opentelemetry/instrumentation: Los componentes fundamentales de OpenTelemetry necesarios para armar el ecosistema de tracing y logging.
Para instalarlos, ejecutá pnpm add @vercel/otel @opentelemetry/sdk-logs @opentelemetry/api-logs @opentelemetry/instrumentation.
Instrumentación en Next.js: Registrando OpenTelemetry
En el mismo src/instrumentation.ts donde inicializamos el logger en el post anterior
, también vamos a registrar OpenTelemetry.
// Based of https://github.com/adityasinghcodes/nextjs-monitoring/blob/main/instrumentation.ts
// Node.js-specific imports are moved into dynamic imports within runtime checks
// Prevent Edge runtime from trying to import Node.js-specific modules
declare global {
var metrics:
| {
registry: any;
}
| undefined;
var logger: any | undefined;
}
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
const { Registry, collectDefaultMetrics } = await import("prom-client");
const pino = (await import("pino")).default;
const pinoLoki = (await import("pino-loki")).default;
const { registerOTel } = await import("@vercel/otel");
//prometheus initialization
const prometheusRegistry = new Registry();
collectDefaultMetrics({
register: prometheusRegistry,
});
globalThis.metrics = {
registry: prometheusRegistry,
};
//loki initialization
globalThis.logger = pino(
pinoLoki({
host: "http://localhost:3100", // Connects to the loki container via localhost:3100
batching: true,
interval: 5,
labels: { app: "next-app" }, // Crucial label for querying in Grafana
})
);
//otel initialization
registerOTel();
}
}
Configuración de Variables de Entorno OTel
Para que la instrumentación de OpenTelemetry sepa a dónde enviar sus datos y cómo etiquetarlos, necesitamos configurar variables de entorno específicas.
Dado que estamos ejecutando la aplicación Next.js fuera de Docker, estas variables deben definirse en el ambiente de la máquina host donde se inicia el proceso de Next.js.
Configurando el Ambiente Host
-
Configuración de Ejecución/Debug del IDE: Si usás un IDE como JetBrains WebStorm , podés agregar estas variables directamente en las opciones de configuración de Run/Debug:
Establecé la siguiente cadena de entorno:
OTEL_LOG_LEVEL=info;OTEL_SERVICE_NAME="next-app"
Tip: Es súper recomendable guardar todas tus variables de entorno de desarrollo no sensibles en un archivo de texto (ej.,
src/resources/dev/env-dev.txt) para que los nuevos desarrolladores puedan simplemente copiarlas y pegarlas en la configuración de su IDE. -
Archivo
.envdel Proyecto: Usamos el archivo.envdel proyecto para referenciar estas variables de entorno, haciéndolas accesibles para el proceso de build y runtime de Next.js.
# OTel Configuration
OTEL_LOG_LEVEL="${OTEL_LOG_LEVEL}"
OTEL_SERVICE_NAME="${OTEL_SERVICE_NAME}"
Configuración del OTel Collector
El tracing es gestionado por OpenTelemetry (OTel). Nuestra app Next.js, a través de @vercel/otel, envía datos de trace usando el protocolo OTLP a un servicio intermediario: el OpenTelemetry Collector.
El Collector actúa como un hub central, recibiendo los datos, procesándolos (como agruparlos eficientemente) y luego dirigiéndolos al backend final—en este caso, Zipkin.
La configuración para el collector está en src/resources/dev/monitoring/otel-collector-config.yml:
# Based of https://github.com/adityasinghcodes/nextjs-monitoring/blob/main/otel-collector-config.yml
# Receivers configuration - defines how the collector receives telemetry data
receivers:
# OpenTelemetry Protocol (OTLP) receiver configuration
otlp:
protocols:
# gRPC endpoint for receiving OTLP data
grpc:
endpoint: "0.0.0.0:4317"
# HTTP endpoint for receiving OTLP data
http:
endpoint: "0.0.0.0:4318"
# Processors configuration - defines how telemetry data is processed
processors:
# Batch processor aggregates data before exporting
batch:
timeout: 1s # Maximum time to wait before sending a batch
send_batch_size: 1024 # Maximum number of spans to include in a batch
# Exporters configuration - defines where telemetry data is sent
exporters:
# Zipkin exporter configuration
zipkin:
endpoint: "http://zipkin:9411/api/v2/spans" # Zipkin server endpoint (using the service name 'zipkin')
format: proto # Use protobuf format for data
# Debug exporter for troubleshooting
debug:
verbosity: detailed # Maximum verbosity level for debugging
# Extensions configuration - additional collector functionality
extensions:
health_check: # Enables health checking endpoint
pprof: # Enables profiling endpoint
endpoint: :1888
zpages: # Enables diagnostic pages
endpoint: :55679
# Service configuration - ties together all the components
service:
extensions: [pprof, zpages, health_check] # Enable all configured extensions
pipelines:
# Traces pipeline configuration
traces:
receivers: [otlp] # Use OTLP receiver
processors: [batch] # Process with batch processor
exporters: [zipkin, debug] # Export to Zipkin and debug
Definir OTel Collector y Zipkin
En el mismo Docker Compose que usamos para definir Loki en el post anterior , también definiremos el OTel Collector y Zipkin.
src/resources/dev/monitoring/docker-compose.yml
# Based of https://github.com/adityasinghcodes/nextjs-monitoring/blob/main/docker-compose.yml
services:
grafana:
container_name: grafana
image: grafana/grafana:11.4.0
ports:
- "3001:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin_user
- GF_SECURITY_ADMIN_PASSWORD=admin_password
volumes:
- grafana-storage:/var/lib/grafana
networks:
- monitoring
prometheus:
container_name: prometheus
image: prom/prometheus:v3.0.1
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-storage:/prometheus
networks:
- monitoring
loki:
container_name: loki
image: grafana/loki:2.9.2
ports:
- "3100:3100"
volumes:
- ./loki-config.yml:/etc/loki/local-config.yml
command: -config.file=/etc/loki/local-config.yml
networks:
- monitoring
otel-collector:
container_name: otel-collector
image: otel/opentelemetry-collector:0.115.0
restart: always
command: ["--config=/etc/otel-collector-config.yml"]
volumes:
- ./otel-collector-config.yml:/etc/otel-collector-config.yml
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "8888:8888" # Prometheus metrics exposed by collector
- "8889:8889" # Prometheus exporter metrics
- "13133:13133" # Health check extension
- "55679:55679" # zPages extension
networks:
- monitoring
zipkin:
container_name: zipkin
image: openzipkin/zipkin:3.4.2
ports:
- "9411:9411"
networks:
- monitoring
networks:
monitoring:
name: monitoring
driver: bridge
volumes:
grafana-storage:
prometheus-storage:
Visualización de Traces con Zipkin
Asegurate de que tu motor Docker (tipo Docker Desktop ) esté corriendo en segundo plano.
- Levantá el Stack:
docker-compose -f src/resources/dev/monitoring/docker-compose.yml up -d - Iniciá la App: Ejecutá el script de inicio de tu aplicación Next.js en la máquina host.
Para ver las trazas:
- Andá a la UI de Zipkin: http://localhost:9411/zipkin/
- En la esquina superior izquierda, hacé clic en el botón rojo “+” y seleccioná el Service Name
next-app. Luego hacé clic en RUN QUERY.
Verás spans para todas las solicitudes recientes, incluidas las generadas por Prometheus scrapeando el endpoint /api/metrics.

Resolviendo el Error de Producción
Volvamos a nuestro problema original: la pantalla de producción en blanco. Vamos a recrear el escenario con un componente que se rompe a propósito.
Creá una ruta simple /route-with-error con lógica rota:
export const dynamic = "force-dynamic";
async function getData() {
const res = await fetch("https://httpbin.org/status/500");
return res.json();
}
export default async function RouteWithError() {
const data = await getData();
return (
<div className="flex flex-col gap-4">
<p>
The data is: <strong>{JSON.stringify(data)}</strong>
</p>
</div>
);
}
Si visitás http://localhost:3000/route-with-error en un build de producción, obtendrás la temida página en blanco sin ninguna indicación de qué sucedió.

Sin embargo, al revisar Zipkin, la historia es completamente diferente:

Si hacemos clic en el trace, encontramos los detalles exactos:

A partir de esta única traza, sabemos la ruta exacta, el tipo exacto de error (Unexpected end of JSON input) y la causa exacta (fetch get https://httpbin.org/status/500). Podemos saltar inmediatamente al código correspondiente y fixear el bug.
¿Qué Sigue?
Hemos establecido un stack de monitoreo local y robusto utilizando herramientas estándar de la industria. El siguiente paso obvio es desplegar esta misma estrategia de monitoreo en nuestro entorno VPS de producción, lidiando con los desafíos de los hostnames externos, el almacenamiento persistente y la autenticación.