L'adoption des architectures microservices transforme fondamentalement les défis d'observabilité. Ce qui était une application monolithique devient un réseau de services interdépendants.
Sans observabilité adaptée, comprendre ce qui se passe devient rapidement impossible. L'observabilité microservices vise à rendre le système introspectable.
Qu'est-ce que l'Observabilité Microservices ?
L'observabilité désigne la capacité à comprendre l'état interne d'un système distribué à partir de ses outputs externes. Elle repose sur trois piliers fondamentaux.
Les trois piliers
┌─────────────────────────────────────────┐
│ OBSERVABILITÉ │
└─────────────────────────────────────────┘
/ | \
┌─────────┐ ┌─────────┐ ┌─────────┐
│ METRICS │ │ LOGS │ │ TRACES │
│ │ │ │ │ │
│ Quoi │ │ Détail │ │ Comment │
└─────────┘ └─────────┘ └─────────┘
Métriques
Les métriques fournissent des mesures numériques agrégées :
# Latence par service
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket{job="api-gateway"}[5m])
)
# Taux d'erreur
rate(http_requests_total{status=~"5.."}[5m])
/ rate(http_requests_total[5m])
# Requêtes par seconde
rate(http_requests_total[1m])
Elles répondent à la question : quoi se passe ?
Logs
Les logs capturent les événements discrets avec contexte :
{
"timestamp": "2025-12-29T10:30:00Z",
"level": "error",
"service": "order-service",
"trace_id": "abc123",
"span_id": "def456",
"message": "Failed to process order",
"error": "Database connection timeout",
"order_id": "ORD-789",
"user_id": "USR-123"
}
Ils fournissent le détail nécessaire au debugging.
Traces distribuées
Les traces suivent le parcours d'une requête :
[Gateway] ────► [Order Service] ────► [Payment Service]
│ │ │
│ 10ms │ 150ms │ 200ms
│ │ │
└──────────────────┴──────────────────────┘
Total: 360ms
Elles révèlent comment les services collaborent.
Corrélation des signaux
L'observabilité efficace corrèle les trois piliers :
Alerte métrique ──► Traces concernées ──► Logs détaillés
(quoi) (comment) (pourquoi)
Le trace ID permet cette navigation fluide.
Pourquoi l'Observabilité est Critique pour les Microservices
Les problèmes évidents dans un monolithe deviennent obscurs dans un système distribué.
Debugging distribué
Une requête peut traverser dix services :
User Request
│
▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Gateway │───►│ Auth │───►│ User │
└─────────┘ └─────────┘ └─────────┘
│
▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Order │───►│ Payment │───►│ Notify │
└─────────┘ └─────────┘ └─────────┘
│
▼
┌─────────┐ ┌─────────┐
│ Stock │───►│ Ship │
└─────────┘ └─────────┘
Dépendances cachées
Les microservices développent des dépendances implicites :
# Dépendances documentées
order-service:
depends_on:
- payment-service
- stock-service
# Dépendances réelles (découvertes via traces)
order-service:
depends_on:
- payment-service
- stock-service
- user-service # Non documenté !
- promotion-service # Non documenté !
Les traces révèlent ces dépendances réelles.
Performance end-to-end
La latence utilisateur est la somme des latences :
# Exemple de trace analysée
trace = {
"total_duration_ms": 850,
"spans": [
{"service": "gateway", "duration_ms": 50},
{"service": "auth", "duration_ms": 100},
{"service": "order", "duration_ms": 200},
{"service": "payment", "duration_ms": 400}, # Goulot !
{"service": "notification", "duration_ms": 100}
]
}
# Identification automatique des goulots
bottleneck = max(trace["spans"], key=lambda s: s["duration_ms"])
# => payment service (400ms = 47% du temps total)
Détection d'anomalies
Des patterns subtils deviennent visibles :
# Taux d'erreur entre deux services spécifiques
rate(http_requests_total{
source="order-service",
destination="payment-service",
status=~"5.."
}[5m])
Comment Implémenter l'Observabilité Microservices
Instrumentation avec OpenTelemetry
OpenTelemetry est le standard d'instrumentation :
# Python avec OpenTelemetry
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# Configuration
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("order-service")
# Instrumentation d'une fonction
async def process_order(order_id: str):
with tracer.start_as_current_span("process_order") as span:
span.set_attribute("order.id", order_id)
# Les appels HTTP propagent automatiquement le contexte
user = await user_service.get_user(order.user_id)
payment = await payment_service.charge(order.amount)
span.set_attribute("order.total", order.amount)
return order
Propagation du contexte
Le trace ID doit traverser tous les services :
# HTTP - Propagation automatique avec OpenTelemetry
# Les headers W3C Trace Context sont ajoutés automatiquement
# traceparent: 00-abc123-def456-01
# gRPC - Métadonnées
metadata = [
("traceparent", trace_context.get_traceparent()),
]
response = stub.ProcessPayment(request, metadata=metadata)
# Message Queue - Propriétés
message = {
"body": payload,
"properties": {
"traceparent": trace_context.get_traceparent()
}
}
queue.publish(message)
Logs structurés avec contexte
Incluez le trace ID dans les logs :
import structlog
def configure_logging():
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
add_trace_context, # Ajoute trace_id et span_id
structlog.processors.JSONRenderer()
]
)
def add_trace_context(logger, method_name, event_dict):
span = trace.get_current_span()
if span:
ctx = span.get_span_context()
event_dict["trace_id"] = format(ctx.trace_id, "032x")
event_dict["span_id"] = format(ctx.span_id, "016x")
return event_dict
# Usage
logger.info("Order created", order_id="ORD-123", amount=99.99)
# Output: {"trace_id": "abc123...", "span_id": "def456...", "order_id": "ORD-123", ...}
Collecte centralisée
Configurez les backends :
# docker-compose.yml - Stack d'observabilité
services:
# Collecteur OpenTelemetry
otel-collector:
image: otel/opentelemetry-collector:latest
ports:
- "4317:4317" # gRPC
- "4318:4318" # HTTP
volumes:
- ./otel-config.yaml:/etc/otel-config.yaml
# Traces
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686"
# Métriques
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
# Logs
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
# Visualisation unifiée
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
Dashboards unifiés
Créez des vues croisant les signaux :
# Grafana dashboard - Service overview
panels:
- title: "Request Rate"
type: graph
datasource: prometheus
query: rate(http_requests_total{service="order"}[5m])
- title: "Error Rate"
type: graph
datasource: prometheus
query: rate(http_requests_total{service="order",status=~"5.."}[5m])
- title: "Latency P95"
type: graph
datasource: prometheus
query: histogram_quantile(0.95, rate(http_duration_bucket{service="order"}[5m]))
- title: "Recent Traces"
type: table
datasource: jaeger
query: service=order
- title: "Error Logs"
type: logs
datasource: loki
query: '{service="order"} |= "error"'
Bonnes Pratiques d'Observabilité Microservices
Standardisez avec OpenTelemetry
Utilisez OpenTelemetry partout :
# Tous les services suivent la même convention
instrumentation:
tracer: opentelemetry
metrics: opentelemetry
logs: structured-json-with-trace-context
Conventions de nommage
Standardisez les noms :
# Métriques
http_requests_total # Pas request_count
http_request_duration_seconds # Pas latency_ms
# Attributs de span
http.method: GET
http.url: /api/orders
http.status_code: 200
service.name: order-service
service.version: 2.1.0
# Labels de log
level: error
service: order-service
trace_id: abc123
Échantillonnage intelligent
Gérez les volumes de traces :
from opentelemetry.sdk.trace.sampling import ParentBasedTraceIdRatio
# Échantillonnage de base : 10% des traces
sampler = ParentBasedTraceIdRatio(0.1)
# Échantillonnage tail-based avec règles
# Conserver 100% des erreurs et latences élevées
rules = [
{"condition": "status_code >= 500", "sample_rate": 1.0},
{"condition": "duration_ms > 1000", "sample_rate": 1.0},
{"condition": "default", "sample_rate": 0.1}
]
Contexte métier enrichi
Ajoutez des attributs business :
with tracer.start_as_current_span("checkout") as span:
span.set_attribute("user.id", user_id)
span.set_attribute("user.tier", "premium")
span.set_attribute("cart.items_count", len(cart))
span.set_attribute("cart.total", cart.total)
span.set_attribute("payment.method", "credit_card")
Alertes sur symptômes
Alertez sur l'impact utilisateur :
alerts:
# Symptôme : latence élevée (impact utilisateur)
- name: high_latency
expr: histogram_quantile(0.95, rate(http_duration_bucket[5m])) > 0.5
severity: warning
# Symptôme : taux d'erreur (impact utilisateur)
- name: high_error_rate
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.01
severity: critical
# PAS de cause directe comme alerte primaire
# - name: cpu_high # Cause, pas symptôme
Formation des équipes
Investissez dans la formation :
- Navigation dans Grafana
- Lecture des traces Jaeger
- Requêtes PromQL et LogQL
- Corrélation des signaux
Les outils sophistiqués sont inutiles si personne ne sait les utiliser.
Conclusion
L'observabilité constitue un prérequis pour opérer des microservices en production.
Les éléments clés :
- Instrumentation : OpenTelemetry partout
- Corrélation : Trace ID dans métriques, logs et traces
- Visualisation : Dashboards unifiés avec navigation fluide
- Formation : Équipes capables d'utiliser les outils
L'investissement se rentabilise par la réduction du temps de résolution des incidents.