Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Python observability patterns including structured logging, metrics, and distributed tracing. Use when adding logging, implementing metrics collection, setting up tracing, or debugging production systems.
.claude/skills/dicklesworthstone-python-observability/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 74% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 91% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 102% | 0% |
Instrument Python applications with structured logs, metrics, and traces. When something breaks in production, you need to answer "what, where, and why" without deploying new code.
Emit logs as JSON with consistent fields for production environments. Machine-readable logs enable powerful queries and alerts. For local development, consider human-readable formats.
Track latency, traffic, errors, and saturation for every service boundary.
Thread a unique ID through all logs and spans for a single request, enabling end-to-end tracing.
Keep metric label values bounded. Unbounded labels (like user IDs) explode storage costs.
pythonimport structlog structlog.configure( processors=[ structlog.processors.TimeStamper(fmt="iso"), structlog.processors.JSONRenderer(), ], ) logger = structlog.get_logger() logger.info("Request processed", user_id="123", duration_ms=45)
Configure structlog for JSON output with consistent fields.
pythonimport logging import structlog def configure_logging(log_level: str = "INFO") -> None: """Configure structured logging for the application.""" structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.JSONRenderer(), ], wrapper_class=structlog.make_filtering_bound_logger( getattr(logging, log_level.upper()) ), context_class=dict, logger_factory=structlog.PrintLoggerFactory(), cache_logger_on_first_use=True, ) # Initialize at application startup configure_logging("INFO") logger = structlog.get_logger()
Every log entry should include standard fields for filtering and correlation.
pythonimport structlog from contextvars import ContextVar # Store correlation ID in context correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") logger = structlog.get_logger() def process_request(request: Request) -> Response: """Process request with structured logging.""" logger.info( "Request received", correlation_id=correlation_id.get(), method=request.method, path=request.path, user_id=request.user_id, ) try: result = handle_request(request) logger.info( "Request completed", correlation_id=correlation_id.get(), status_code=200, duration_ms=elapsed, ) return result except Exception as e: logger.error( "Request failed", correlation_id=correlation_id.get(), error_type=type(e).__name__, error_message=str(e), ) raise
Use log levels consistently across the application.
| Level | Purpose | Examples | |-------|---------|----------| | DEBUG | Development diagnostics | Variable values, internal state | | INFO | Request lifecycle, operations | Request start/end, job completion | | WARNING | Recoverable anomalies | Retry attempts, fallback used | | ERROR | Failures needing attention | Exceptions, service unavailable |
python# DEBUG: Detailed internal information logger.debug("Cache lookup", key=cache_key, hit=cache_hit) # INFO: Normal operational events logger.info("Order created", order_id=order.id, total=order.total) # WARNING: Abnormal but handled situations logger.warning( "Rate limit approaching", current_rate=950, limit=1000, reset_seconds=30, ) # ERROR: Failures requiring investigation logger.error( "Payment processing failed", order_id=order.id, error=str(e), payment_provider="stripe", )
Never log expected behavior at ERROR. A user entering a wrong password is INFO, not ERROR.
Generate a unique ID at ingress and thread it through all operations.
pythonfrom contextvars import ContextVar import uuid import structlog correlation_id: ContextVar[str] = ContextVar("correlation_id", default="") def set_correlation_id(cid: str | None = None) -> str: """Set correlation ID for current context.""" cid = cid or str(uuid.uuid4()) correlation_id.set(cid) structlog.contextvars.bind_contextvars(correlation_id=cid) return cid # FastAPI middleware example from fastapi import Request async def correlation_middleware(request: Request, call_next): """Middleware to set and propagate correlation ID.""" # Use incoming header or generate new cid = request.headers.get("X-Correlation-ID") or str(uuid.uuid4()) set_correlation_id(cid) response = await call_next(request) response.headers["X-Correlation-ID"] = cid return response
Propagate to outbound requests:
pythonimport httpx async def call_downstream_service(endpoint: str, data: dict) -> dict: """Call downstream service with correlation ID.""" async with httpx.AsyncClient() as client: response = await client.post( endpoint, json=data, headers={"X-Correlation-ID": correlation_id.get()}, ) return response.json()
Track these metrics for every service boundary:
pythonfrom prometheus_client import Counter, Histogram, Gauge # Latency: How long requests take REQUEST_LATENCY = Histogram( "http_request_duration_seconds", "Request latency in seconds", ["method", "endpoint", "status"], buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10], ) # Traffic: Request rate REQUEST_COUNT = Counter( "http_requests_total", "Total HTTP requests", ["method", "endpoint", "status"], ) # Errors: Error rate ERROR_COUNT = Counter( "http_errors_total", "Total HTTP errors", ["method", "endpoint", "error_type"], ) # Saturation: Resource utilization DB_POOL_USAGE = Gauge( "db_connection_pool_used", "Number of database connections in use", )
Instrument your endpoints:
pythonimport time from functools import wraps def track_request(func): """Decorator to track request metrics.""" @wraps(func) async def wrapper(request: Request, *args, **kwargs): method = request.method endpoint = request.url.path start = time.perf_counter() try: response = await func(request, *args, **kwargs) status = str(response.status_code) return response except Exception as e: status = "500" ERROR_COUNT.labels( method=method, endpoint=endpoint, error_type=type(e).__name__, ).inc() raise finally: duration = time.perf_counter() - start REQUEST_COUNT.labels(method=method, endpoint=endpoint, status=status).inc() REQUEST_LATENCY.labels(method=method, endpoint=endpoint, status=status).observe(duration) return wrapper
Avoid labels with unbounded values to prevent metric explosion.
python# BAD: User ID has potentially millions of values REQUEST_COUNT.labels(method="GET", user_id=user.id) # Don't do this! # GOOD: Bounded values only REQUEST_COUNT.labels(method="GET", endpoint="/users", status="200") # If you need per-user metrics, use a different approach: # - Log the user_id and query logs # - Use a separate analytics system # - Bucket users by type/tier REQUEST_COUNT.labels( method="GET", endpoint="/users", user_tier="premium", # Bounded set of values )
Create a reusable timing context manager for operations.
pythonfrom contextlib import contextmanager import time import structlog logger = structlog.get_logger() @contextmanager def timed_operation(name: str, **extra_fields): """Context manager for timing and logging operations.""" start = time.perf_counter() logger.debug("Operation started", operation=name, **extra_fields) try: yield except Exception as e: elapsed_ms = (time.perf_counter() - start) * 1000 logger.error( "Operation failed", operation=name, duration_ms=round(elapsed_ms, 2), error=str(e), **extra_fields, ) raise else: elapsed_ms = (time.perf_counter() - start) * 1000 logger.info( "Operation completed", operation=name, duration_ms=round(elapsed_ms, 2), **extra_fields, ) # Usage with timed_operation("fetch_user_orders", user_id=user.id): orders = await order_repository.get_by_user(user.id)
Set up distributed tracing with OpenTelemetry.
Note: OpenTelemetry is actively evolving. Check the official Python documentation for the latest API patterns and best practices.
pythonfrom 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 def configure_tracing(service_name: str, otlp_endpoint: str) -> None: """Configure OpenTelemetry tracing.""" provider = TracerProvider() processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=otlp_endpoint)) provider.add_span_processor(processor) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__) async def process_order(order_id: str) -> Order: """Process order with tracing.""" with tracer.start_as_current_span("process_order") as span: span.set_attribute("order.id", order_id) with tracer.start_as_current_span("validate_order"): validate_order(order_id) with tracer.start_as_current_span("charge_payment"): charge_payment(order_id) with tracer.start_as_current_span("send_confirmation"): send_confirmation(order_id) return order
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 18,392 | 41,484 | +126% | 1 | 1 | 0% | 3,550 | 6,737 | +90% | 0 | 0 | — |
case-02 | fail→fail | 16,462 | 15,641 | -5% | 1 | 1 | 0% | 3,237 | 6,174 | +91% | 0 | 0 | — |
case-03 | pass→pass | 19,955 | 14,402 | -28% | 1 | 1 | 0% | 2,862 | 5,480 | +91% | 0 | 0 | — |
case-04 | fail→pass | 14,099 | 16,813 | +19% | 1 | 1 | 0% | 2,276 | 5,256 | +131% | 0 | 0 | — |
case-05 | fail→pass | 26,667 | 17,466 | -35% | 1 | 1 | 0% | 3,195 | 5,807 | +82% | 0 | 0 | — |
case-06 | pass→pass | 13,939 | 11,803 | -15% | 1 | 1 | 0% | 2,622 | 5,296 | +102% | 0 | 0 | — |
case-07 | pass→pass | 15,096 | 13,193 | -13% | 1 | 1 | 0% | 2,345 | 5,150 | +120% | 0 | 0 | — |
case-08 | pass→pass | 6,471 | 5,313 | -18% | 1 | 1 | 0% | 1,092 | 3,856 | +253% | 0 | 0 | — |
case-21 | pass→pass | 15,322 | 15,081 | -2% | 1 | 1 | 0% | 2,720 | 6,027 | +122% | 0 | 0 | — |
case-09 | pass→pass | 8,327 | 4,465 | -46% | 1 | 1 | 0% | 1,513 | 3,766 | +149% | 0 | 0 | — |
case-10 | pass→pass | 13,114 | 10,450 | -20% | 1 | 1 | 0% | 2,473 | 5,013 | +103% | 0 | 0 | — |
case-11 | pass→pass | 15,150 | 13,153 | -13% | 1 | 1 | 0% | 2,685 | 5,379 | +100% | 0 | 0 | — |
case-12 | pass→pass | 12,975 | 10,248 | -21% | 1 | 1 | 0% | 2,480 | 4,948 | +100% | 0 | 0 | — |
case-13 | pass→pass | 9,471 | 6,988 | -26% | 1 | 1 | 0% | 1,490 | 3,968 | +166% | 0 | 0 | — |
case-14 | pass→pass | 12,468 | 10,498 | -16% | 1 | 1 | 0% | 2,386 | 4,933 | +107% | 0 | 0 | — |
case-15 | pass→pass | 15,596 | 10,880 | -30% | 1 | 1 | 0% | 2,621 | 4,984 | +90% | 0 | 0 | — |
case-16 | pass→pass | 12,146 | 6,047 | -50% | 1 | 1 | 0% | 2,002 | 4,031 | +101% | 0 | 0 | — |
case-17 | pass→pass | 11,638 | 3,674 | -68% | 1 | 1 | 0% | 1,911 | 3,657 | +91% | 0 | 0 | — |
case-18 | fail→pass | 17,102 | 10,684 | -38% | 1 | 1 | 0% | 2,868 | 4,986 | +74% | 0 | 0 | — |
case-19 | pass→pass | 8,385 | 7,532 | -10% | 1 | 1 | 0% | 1,632 | 4,254 | +161% | 0 | 0 | — |
case-20 | pass→pass | 13,437 | 9,473 | -30% | 1 | 1 | 0% | 2,482 | 4,544 | +83% | 0 | 0 | — |
case-22 | pass→pass | 7,899 | 7,497 | -5% | 1 | 1 | 0% | 1,518 | 4,457 | +194% | 0 | 0 | — |
case-23 | pass→pass | 19,729 | 28,263 | +43% | 1 | 1 | 0% | 3,138 | 8,857 | +182% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +13 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.