Cap 6: Instrumentación

Por: Artiko
opentelemetryinstrumentacionpythonnodejssdkauto-instrumentacion

Auto-instrumentación vs Manual

graph LR
    AUTO[Auto-instrumentación\nCero código\nLibrerías contrib] -->|cubre| FW[Frameworks\nHTTP, DB, Queue]
    MAN[Manual\nTu código de negocio\nContexto específico] -->|cubre| BIZ[Lógica de negocio\nReglas de dominio]
    AUTO -.->|complementa| MAN

Auto-instrumentación — Las librerías contrib de OTel instrumentan automáticamente frameworks populares (Flask, Express, psycopg2, redis, etc.) sin que modifiques tu código.

Manual — Instrumentas la lógica de negocio con contexto que las librerías no pueden inferir: order.id, payment.tier, feature.flags.

La combinación correcta: auto-instrumentación para el plumbing (HTTP, DB), manual para el dominio.

Python — Setup completo

Instalar dependencias

pip install \
  opentelemetry-sdk \
  opentelemetry-exporter-otlp-proto-grpc \
  opentelemetry-instrumentation-fastapi \
  opentelemetry-instrumentation-sqlalchemy \
  opentelemetry-instrumentation-redis \
  opentelemetry-instrumentation-httpx

Configuración del SDK

# otel_setup.py — ejecutar al inicio de la aplicación

from opentelemetry import trace, metrics
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk._logs import LoggingHandler
import logging
import os

OTEL_ENDPOINT = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
SERVICE_NAME = os.getenv("OTEL_SERVICE_NAME", "my-service")

def setup_telemetry():
    resource = Resource.create({
        "service.name": SERVICE_NAME,
        "service.version": os.getenv("APP_VERSION", "0.0.0"),
        "deployment.environment": os.getenv("ENV", "development"),
    })

    # Traces
    tracer_provider = TracerProvider(resource=resource)
    tracer_provider.add_span_processor(
        BatchSpanProcessor(OTLPSpanExporter(endpoint=OTEL_ENDPOINT))
    )
    trace.set_tracer_provider(tracer_provider)

    # Metrics
    metric_reader = PeriodicExportingMetricReader(
        OTLPMetricExporter(endpoint=OTEL_ENDPOINT),
        export_interval_millis=60_000,
    )
    meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
    metrics.set_meter_provider(meter_provider)

    # Logs
    logger_provider = LoggerProvider(resource=resource)
    logger_provider.add_log_record_processor(
        BatchLogRecordProcessor(OTLPLogExporter(endpoint=OTEL_ENDPOINT))
    )
    set_logger_provider(logger_provider)

    # Conectar con logging estándar
    handler = LoggingHandler(level=logging.DEBUG, logger_provider=logger_provider)
    logging.getLogger().addHandler(handler)

Auto-instrumentación con FastAPI

# main.py
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
from otel_setup import setup_telemetry

setup_telemetry()

app = FastAPI()

# Auto-instrumentar FastAPI — crea spans para cada endpoint
FastAPIInstrumentor.instrument_app(app)

# Auto-instrumentar SQLAlchemy — crea spans para queries SQL
SQLAlchemyInstrumentor().instrument(engine=engine)

# Auto-instrumentar Redis
RedisInstrumentor().instrument()

Con esto, cada request HTTP y query SQL genera spans automáticamente sin más código.

Instrumentación manual de dominio

from opentelemetry import trace
from opentelemetry.trace import StatusCode
import logging

tracer = trace.get_tracer("order-service", "1.0.0")
logger = logging.getLogger("order-service")
meter = metrics.get_meter("order-service", "1.0.0")

orders_counter = meter.create_counter("orders.created", unit="1")
order_value = meter.create_histogram("orders.value", unit="USD")

async def create_order(user_id: str, items: list, db) -> dict:
    with tracer.start_as_current_span("create-order") as span:
        span.set_attribute("enduser.id", user_id)
        span.set_attribute("order.items_count", len(items))

        total = sum(item["price"] for item in items)
        span.set_attribute("order.total_usd", total)

        logger.info("Creating order", extra={
            "enduser.id": user_id,
            "order.items_count": len(items),
            "order.total_usd": total,
        })

        try:
            order = await db.create_order(user_id, items)
            orders_counter.add(1, {"tier": get_user_tier(user_id)})
            order_value.record(total, {"currency": "USD"})
            span.add_event("order persisted", {"order.id": order["id"]})
            return order
        except Exception as e:
            span.record_exception(e)
            span.set_status(StatusCode.ERROR, str(e))
            logger.error("Order creation failed", extra={"error": str(e)})
            raise

Node.js — Setup completo

Instalar dependencias

npm install \
  @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-grpc \
  @opentelemetry/exporter-metrics-otlp-grpc \
  @opentelemetry/semantic-conventions

Configuración (debe cargarse primero)

// instrumentation.ts — importar ANTES que cualquier otro módulo

import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { Resource } from '@opentelemetry/resources';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? 'my-service',
    [ATTR_SERVICE_VERSION]: process.env.APP_VERSION ?? '0.0.0',
    'deployment.environment': process.env.NODE_ENV ?? 'development',
  }),

  // Auto-instrumenta: Express, HTTP, grpc, pg, redis, etc.
  instrumentations: [getNodeAutoInstrumentations()],

  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4317',
  }),

  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4317',
    }),
    exportIntervalMillis: 60_000,
  }),
});

sdk.start();

process.on('SIGTERM', () => sdk.shutdown());

Cargar antes que todo:

// package.json
{
  "scripts": {
    "start": "node --require ./dist/instrumentation.js dist/index.js"
  }
}

Instrumentación manual en Node.js

import { trace, metrics, SpanStatusCode } from '@opentelemetry/api';
import { ATTR_HTTP_RESPONSE_STATUS_CODE } from '@opentelemetry/semantic-conventions';

const tracer = trace.getTracer('order-service', '1.0.0');
const meter = metrics.getMeter('order-service', '1.0.0');

const ordersCounter = meter.createCounter('orders.created', { unit: '1' });
const orderDuration = meter.createHistogram('orders.processing.duration', { unit: 's' });

async function createOrder(userId: string, items: Item[]): Promise<Order> {
  return tracer.startActiveSpan('create-order', async (span) => {
    span.setAttributes({
      'enduser.id': userId,
      'order.items_count': items.length,
    });

    const start = performance.now();

    try {
      const order = await db.createOrder(userId, items);
      ordersCounter.add(1, { 'order.status': 'success' });
      orderDuration.record((performance.now() - start) / 1000);
      span.setStatus({ code: SpanStatusCode.OK });
      return order;
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
      ordersCounter.add(1, { 'order.status': 'error' });
      throw error;
    } finally {
      span.end();
    }
  });
}

Variables de entorno para configuración

Una ventaja de OTel es que mucha configuración se puede hacer sin código:

# Identificación del servicio
export OTEL_SERVICE_NAME=order-service
export OTEL_SERVICE_VERSION=2.1.0

# Exporter
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc

# Sampling — 10% en producción
export OTEL_TRACES_SAMPLER=traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

# Atributos del resource
export OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production,k8s.namespace.name=payments

Librerías contrib disponibles

OTel tiene instrumentación lista para usar para los frameworks más populares:

Python:

Node.js (via @opentelemetry/auto-instrumentations-node):