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/