Observabilité — 04 — OpenTelemetry : SDK TypeScript et Python, auto-instrumentation

Instrumenter une API TypeScript et Python avec OpenTelemetry : auto-instrumentation, traces manuelles, métriques custom, export OTLP vers Jaeger et Prometheus.

04 — OpenTelemetry : SDK TypeScript et Python, auto-instrumentation

Ce que tu vas apprendre

  • Ce qu'est OpenTelemetry et pourquoi c'est devenu le standard
  • L'auto-instrumentation : zéro code pour les frameworks courants
  • L'instrumentation manuelle : spans custom et métriques métier
  • L'export OTLP vers Jaeger, Prometheus et Grafana
  • Configuration complète TypeScript (Node.js) et Python (FastAPI)

Prérequis


Qu'est-ce qu'OpenTelemetry

OpenTelemetry (OTel) est un projet CNCF né en 2019 de la fusion d'OpenTracing et OpenCensus. C'est un standard ouvert pour collecter logs, métriques et traces — indépendamment du backend.

Avant OTel : chaque vendor (Datadog, Jaeger, Zipkin, New Relic) avait son propre SDK. Changer de backend imposait de réécrire toute l'instrumentation.

Avec OTel : tu instrumentes une fois avec le SDK OTel, tu changes le backend dans la config.

Code instrumenté
    │ SDK OpenTelemetry
    ▼
OTel Collector (optionnel — routing, sampling, transformation)
    │
    ├──► Jaeger (traces)
    ├──► Prometheus (métriques)
    └──► Loki (logs)

TypeScript : setup complet

Installation

bashnpm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-prometheus \
  @opentelemetry/sdk-metrics

Configuration (à charger avant tout le reste)

typescript// instrumentation.ts — charger avant index.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { PrometheusExporter } from "@opentelemetry/exporter-prometheus";
import { Resource } from "@opentelemetry/resources";
import { SEMRESATTRS_SERVICE_NAME, SEMRESATTRS_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
import { TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-node";

const sdk = new NodeSDK({
  resource: new Resource({
    [SEMRESATTRS_SERVICE_NAME]: "order-api",
    [SEMRESATTRS_SERVICE_VERSION]: process.env.APP_VERSION ?? "unknown",
    "deployment.environment": process.env.NODE_ENV ?? "development",
  }),

  // Traces → Jaeger via OTLP
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/traces",
  }),

  // Métriques → Prometheus scrape
  metricReader: new PrometheusExporter({
    port: 9464, // /metrics sur ce port
  }),

  // Sampling : 100% en dev, 10% en prod
  sampler: process.env.NODE_ENV === "production"
    ? new TraceIdRatioBasedSampler(0.1)
    : undefined, // Par défaut : 100%

  // Auto-instrumentation : HTTP, Express, pg, redis, fetch...
  instrumentations: [
    getNodeAutoInstrumentations({
      "@opentelemetry/instrumentation-fs": { enabled: false }, // Trop verbeux
      "@opentelemetry/instrumentation-http": {
        ignoreIncomingRequestHook: (req) =>
          req.url === "/metrics" || req.url === "/health", // Ne pas tracer les health checks
      },
    }),
  ],
});

sdk.start();
console.log("OpenTelemetry initialized");

// Graceful shutdown
process.on("SIGTERM", () => {
  sdk.shutdown().then(() => process.exit(0));
});

Charger avant tout le reste dans package.json :

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

Spans manuels pour la logique métier

L'auto-instrumentation couvre HTTP, BDD, Redis. Pour la logique métier, ajouter des spans manuels :

typescriptimport { trace, context, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("order-service", "1.0.0");

export async function processOrder(orderId: string): Promise<void> {
  // Créer un span enfant du span courant (propagé par OTel automatiquement)
  return tracer.startActiveSpan("order.process", async (span) => {
    span.setAttribute("order.id", orderId);

    try {
      const order = await db.findOrder(orderId); // Automatiquement instrumenté (sous-span pg)

      span.setAttribute("order.status", order.status);
      span.setAttribute("order.total_cents", order.totalCents);

      await validateInventory(order);
      await chargePayment(order);

      span.setStatus({ code: SpanStatusCode.OK });
    } catch (err) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: err instanceof Error ? err.message : String(err),
      });
      span.recordException(err as Error);
      throw err;
    } finally {
      span.end();
    }
  });
}

Métriques custom avec OTel

typescriptimport { metrics } from "@opentelemetry/api";

const meter = metrics.getMeter("order-service", "1.0.0");

const ordersCreated = meter.createCounter("orders_created_total", {
  description: "Nombre total de commandes créées",
});

const orderProcessingDuration = meter.createHistogram("order_processing_duration_seconds", {
  description: "Durée de traitement d'une commande",
  unit: "s",
  advice: { explicitBucketBoundaries: [0.1, 0.5, 1, 2, 5, 10] },
});

const activeOrders = meter.createObservableGauge("active_orders", {
  description: "Nombre de commandes en cours de traitement",
});

activeOrders.addCallback((result) => {
  result.observe(orderQueue.size(), { queue: "main" });
});

// Usage
ordersCreated.add(1, { status: "created", customer_type: "premium" });

const start = performance.now();
await processOrder(orderId);
orderProcessingDuration.record((performance.now() - start) / 1000, { status: "success" });

Python : setup complet avec FastAPI

Installation

bashpip install opentelemetry-sdk \
  opentelemetry-instrumentation-fastapi \
  opentelemetry-instrumentation-httpx \
  opentelemetry-instrumentation-sqlalchemy \
  opentelemetry-exporter-otlp-proto-http \
  opentelemetry-exporter-prometheus

Configuration

python# otel_setup.py
import os
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import start_http_server

def setup_opentelemetry() -> None:
    resource = Resource.create({
        SERVICE_NAME: "order-api",
        SERVICE_VERSION: os.getenv("APP_VERSION", "unknown"),
        "deployment.environment": os.getenv("ENVIRONMENT", "development"),
    })

    # Traces
    sampler = (
        TraceIdRatioBased(0.1)
        if os.getenv("ENVIRONMENT") == "production"
        else TraceIdRatioBased(1.0)
    )

    tracer_provider = TracerProvider(resource=resource, sampler=sampler)
    tracer_provider.add_span_processor(
        BatchSpanProcessor(
            OTLPSpanExporter(
                endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces")
            )
        )
    )
    trace.set_tracer_provider(tracer_provider)

    # Métriques → Prometheus
    start_http_server(port=9464, addr="0.0.0.0")
    reader = PrometheusMetricReader()
    meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
    metrics.set_meter_provider(meter_provider)

Initialisation dans FastAPI

python# main.py
from fastapi import FastAPI
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from .otel_setup import setup_opentelemetry

setup_opentelemetry()

app = FastAPI()

# Auto-instrumentation
FastAPIInstrumentor.instrument_app(
    app,
    excluded_urls="/metrics,/health",
)
HTTPXClientInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument(engine=engine)

Spans et métriques manuels

python# order_service.py
from opentelemetry import trace, metrics

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

orders_created = meter.create_counter(
    "orders_created_total",
    description="Nombre total de commandes créées",
)

order_duration = meter.create_histogram(
    "order_processing_duration_seconds",
    description="Durée de traitement",
    unit="s",
)

async def process_order(order_id: str) -> None:
    with tracer.start_as_current_span("order.process") as span:
        span.set_attribute("order.id", order_id)

        try:
            import time
            start = time.perf_counter()

            order = await db.find_order(order_id)
            span.set_attribute("order.total_cents", order.total_cents)

            await validate_inventory(order)
            await charge_payment(order)

            duration = time.perf_counter() - start
            order_duration.record(duration, {"status": "success"})
            orders_created.add(1, {"status": "created"})

            span.set_status(trace.StatusCode.OK)

        except Exception as exc:
            span.set_status(trace.StatusCode.ERROR, str(exc))
            span.record_exception(exc)
            raise

Docker Compose pour le développement local

yaml# docker-compose.observability.yml
services:
  jaeger:
    image: jaegertracing/all-in-one:1.56
    ports:
      - "16686:16686"  # UI Jaeger
      - "4318:4318"    # OTLP HTTP

  prometheus:
    image: prom/prometheus:v2.50.0
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:10.3.0
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    ports:
      - "3000:3000"
yaml# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: order-api
    static_configs:
      - targets: ["order-api:9464"]

Résumé

Composant TypeScript Python
SDK @opentelemetry/sdk-node opentelemetry-sdk
Auto-instrumentation @opentelemetry/auto-instrumentations-node opentelemetry-instrumentation-*
Export traces OTLPTraceExporter OTLPSpanExporter
Export métriques PrometheusExporter PrometheusMetricReader
Spans manuels tracer.startActiveSpan() tracer.start_as_current_span()

Étape suivante : 05 — SLO / SLI / SLA — définir ce que signifie "le service fonctionne" et mesurer l'écart par rapport à cet objectif.


Sources

  • OpenTelemetry Authors. OpenTelemetry Documentation. opentelemetry.io/docs/
  • OpenTelemetry Authors. Getting Started — Node.js. opentelemetry.io/docs/languages/js/getting-started/
  • OpenTelemetry Authors. Getting Started — Python. opentelemetry.io/docs/languages/python/getting-started/
  • OpenTelemetry Authors. Semantic Conventions. opentelemetry.io/docs/concepts/semantic-conventions/
  • CNCF. OpenTelemetry project. cncf.io/projects/opentelemetry/

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.