Skip to content

ADR-004: Defer Observability to httpx

Status

Accepted

Date: 2024-08-13 (estimated)

Context

API clients need observability features like:

  • Request/response logging
  • Metrics (latency, throughput, errors)
  • Distributed tracing (OpenTelemetry, Jaeger)
  • Custom event hooks

Options for providing observability:

  1. Built-in Observability: Implement logging, metrics, tracing in the client
  2. Defer to httpx: Use httpx's native event hooks and let users add their own
  3. Hybrid: Basic logging built-in, advanced features via httpx

Considerations:

  • Users have different observability requirements
  • Observability stacks vary (Prometheus, DataDog, New Relic, OpenTelemetry)
  • Built-in features become dependencies and maintenance burden
  • httpx provides comprehensive event hook system

Decision

We will defer observability to httpx's native features and provide documentation on how to use them.

The client will:

  • NOT include built-in metrics collection
  • NOT include built-in tracing
  • NOT include opinionated logging beyond basic errors
  • DOCUMENT how to use httpx event hooks
  • DOCUMENT integration patterns for common tools

Users can add observability via httpx's event hooks:

# Request/Response Logging
import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("httpx").setLevel(logging.DEBUG)

# Custom Event Hooks
def log_request(request):
    print(f">>> {request.method} {request.url}")

client.event_hooks["request"] = [log_request]

# OpenTelemetry Integration
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
HTTPXClientInstrumentor().instrument()

# Prometheus Metrics
from prometheus_client import Counter
requests_total = Counter('katana_requests', 'Total requests')

def track_request(request):
    requests_total.inc()

client.event_hooks["request"] = [track_request]

Consequences

Positive Consequences

  1. Flexibility: Users choose their observability stack
  2. Zero Dependencies: No observability libraries required
  3. No Maintenance Burden: Don't maintain integrations for every tool
  4. Standard Patterns: Uses httpx patterns, no custom API
  5. Future-Proof: Works with any new observability tool
  6. Opt-In: Users only pay for what they use
  7. No Opinions: Don't force observability choices on users

Negative Consequences

  1. Setup Required: Users must configure observability themselves
  2. Documentation Needed: Must document common patterns
  3. No Out-of-Box: No built-in dashboards or metrics
  4. Learning Curve: Users need to learn httpx event hooks

Neutral Consequences

  1. User Responsibility: Users own their observability setup
  2. Flexibility = Complexity: More options means more decisions

Alternatives Considered

Alternative 1: Built-in Observability

Include Prometheus metrics, structured logging, etc:

from katana_public_api_client import KatanaClient

client = KatanaClient(
    enable_metrics=True,
    enable_tracing=True,
    log_level="INFO"
)

# Metrics automatically exported to /metrics endpoint
# Traces automatically sent to configured backend

Pros:

  • Works out of the box
  • Consistent across users
  • Easy to get started

Cons:

  • Forces specific observability stack
  • Adds dependencies (prometheus_client, opentelemetry, etc.)
  • Maintenance burden for integrations
  • Hard to customize
  • Breaks for users with different stacks

Why Rejected: Too opinionated, maintenance burden, limits flexibility.

Alternative 2: Plugin System

Provide plugin system for observability:

from katana_public_api_client.plugins import PrometheusPlugin, OTelPlugin

client = KatanaClient(
    plugins=[
        PrometheusPlugin(),
        OTelPlugin(endpoint="...")
    ]
)

Pros:

  • Opt-in features
  • Extensible
  • Can maintain official plugins

Cons:

  • Need to maintain plugin system
  • Need to maintain multiple plugins
  • Users still need to learn plugin API
  • Duplicates httpx's event hooks

Why Rejected: Reinvents httpx's existing event hook system.

Alternative 3: Minimal Logging Only

Include basic structured logging, nothing else:

# Built-in: structured logs
logger.info("API request", extra={
    "method": "GET",
    "url": "/products",
    "status": 200,
    "duration_ms": 123
})

Pros:

  • Useful for debugging
  • Low maintenance
  • Opt-out with log level

Cons:

  • Still opinionated about log format
  • Doesn't help with metrics/tracing
  • Users may want different log formats

Why Rejected: Partial solution, still requires users to add metrics/tracing.

Implementation Details

Provided Documentation

Document common observability patterns in README:

Request/Response Logging

import logging

logging.basicConfig(level=logging.INFO)
logging.getLogger("httpx").setLevel(logging.DEBUG)

async with KatanaClient() as client:
    # All requests logged automatically
    response = await get_all_products.asyncio_detailed(client=client)

Custom Event Hooks

def log_request(request):
    print(f">>> {request.method} {request.url}")

def log_response(response):
    print(f"<<< {response.status_code}")

async with KatanaClient() as client:
    client.event_hooks["request"] = [log_request]
    client.event_hooks["response"] = [log_response]

OpenTelemetry Integration

from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor

HTTPXClientInstrumentor().instrument()

# All requests now have spans
async with KatanaClient() as client:
    response = await get_all_products.asyncio_detailed(client=client)

Prometheus Metrics

from prometheus_client import Counter, Histogram

requests_total = Counter('katana_requests_total', 'Total requests', ['method', 'endpoint'])
request_duration = Histogram('katana_request_duration_seconds', 'Request duration')

def track_request(request):
    requests_total.labels(method=request.method, endpoint=request.url.path).inc()

def track_response(response):
    request_duration.observe(response.elapsed.total_seconds())

async with KatanaClient() as client:
    client.event_hooks["request"] = [track_request]
    client.event_hooks["response"] = [track_response]

What We Provide

  1. Documentation: Common integration patterns
  2. Examples: Working examples in examples/
  3. Clean API: httpx client is directly accessible for advanced use
  4. Event Hooks: Full access to httpx's event system

What We Don't Provide

  1. ❌ Built-in metrics collection
  2. ❌ Built-in tracing
  3. ❌ Opinionated logging format
  4. ❌ Observability library dependencies

Educational Resources

Link users to:

References