The 3 AM Wake-Up Call
Your phone buzzes at 3:14 AM. The trading system reported an anomaly during the Tokyo session. You SSH into the server, scroll through logs that look like this:
2026-04-01 03:14:22 Processing order
2026-04-01 03:14:22 Order validated
2026-04-01 03:14:22 ERROR Something went wrong
2026-04-01 03:14:23 Retrying
2026-04-01 03:14:25 Failed again
Which order? What validation failed? Which retry? You spend 45 minutes reconstructing the sequence from fragmented timestamps before you can even begin to debug.
This scenario is not hypothetical. It is the predictable outcome of writing logs the way we were taught: with print() statements and string interpolation. In high-frequency financial systems where millisecond latency matters and order integrity is non-negotiable, unstructured logging is a liability that delays incident response and obscures the audit trail.
This article is a practical guide to structured logging for Python-based financial APIs. We will move from naive print() patterns through production-grade structlog configurations, JSON serialization, and ELK stack integration. Every code example is production-ready and designed for the specific demands of financial data infrastructure.
The Problem with Unstructured Logging
What "Naive" Logging Looks Like
The typical entry point into logging is the print() statement:
def process_order(order_id, symbol, quantity, price):
print(f"[{datetime.now()}] Processing order {order_id}: {symbol} x {quantity} @ {price}")
if quantity <= 0:
print(f"[ERROR] Invalid quantity {quantity} for order {order_id}")
return None
print(f"[{datetime.now()}] Order {order_id} validated, submitting to exchange")
result = submit_to_exchange(symbol, quantity, price)
print(f"[{datetime.now()}] Order {order_id} result: {result}")
return result
This code is readable. It is also nearly useless for production debugging.
The Four Failures of String Interpolation Logging
| Failure Mode | Symptom | Impact |
|---|---|---|
| Timestamp inconsistency | Manual datetime formatting drifts; log aggregation breaks | Cannot correlate events across services |
| No severity hierarchy | [ERROR] and [DEBUG] require regex parsing |
Alerting systems cannot distinguish critical from noise |
| Flat strings, no context | order_id, symbol, quantity are buried in text |
Cannot filter or search programmatically |
| No structured metadata | Cannot compute metrics (error rate, latency percentiles) from logs | Observability blind spots |
Financial APIs have additional requirements that naive logging cannot satisfy:
- Audit compliance: Regulators may require immutable, queryable records of every order event.
- Cross-service correlation: An order may flow through API gateway → validation service → risk engine → exchange adapter. Without a correlation ID, tracing this flow is archaeological work.
- Sensitive data handling: PII (client IDs, account numbers) must be redacted before logging.
- High throughput: A busy trading API may emit thousands of log entries per second. String concatenation per log line is CPU-expensive.
What Structured Logging Actually Means
The Core Principle
Structured logging is the practice of emitting log entries as key-value pairs rather than formatted strings. Instead of:
2026-04-01 15:30:45 ERROR Failed to submit order AAPL-20260401-001 for client C-88291: insufficient margin
We emit:
{
"timestamp": "2026-04-01T15:30:45.123Z",
"level": "error",
"event": "order_submission_failed",
"order_id": "AAPL-20260401-001",
"client_id": "C-88291",
"symbol": "AAPL",
"reason": "insufficient_margin",
"available_margin": 12500.00,
"required_margin": 18750.00
}
Every field is queryable. Every entry is typed. The same event from different services can be joined on order_id. An ELK query for reason:insufficient_margin AND symbol:AAPL returns precise results in milliseconds.
Why JSON?
JSON is the lingua franca of log aggregation pipelines:
- Parsable: Every major log shipper (Filebeat, Fluentd, Logstash) has native JSON parsers.
- Extensible: Add new fields without breaking existing queries.
- Typed: Numbers remain numbers; booleans remain booleans. This matters for range queries and aggregations in Elasticsearch.
The tradeoff is human readability in development. structlog addresses this with dual rendering: JSON for production, colored console output for development.
Introducing structlog
structlog is a Python library that transforms logging from a string-output problem into a data-output problem. It provides:
- Bound loggers: Attach context that persists across function calls.
- Processors: Transform log entries (add timestamps, redact PII, inject correlation IDs).
- Renderers: Output to JSON, console, or any custom format.
- Standard library compatibility: Can route through Python's built-in
loggingmodule.
Installation
pip install structlog
Basic Usage
import structlog
# Initialize structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
log = structlog.get_logger()
log.info(
"order_submitted",
order_id="AAPL-20260401-001",
symbol="AAPL",
quantity=100,
price=185.50,
client_id="C-88291"
)
Output:
{
"order_id": "AAPL-20260401-001",
"symbol": "AAPL",
"quantity": 100,
"price": 185.5,
"client_id": "C-88291",
"event": "order_submitted",
"level": "info",
"timestamp": "2026-04-01T15:30:45.123456Z"
}
Production-Grade Configuration for Financial APIs
The Financial API Processor Chain
A production financial API requires a more sophisticated processor chain than the basic example. Here is a configuration designed for high-throughput trading systems:
import structlog
import logging
import sys
import os
from datetime import datetime
from typing import Any
# Detect environment
ENVIRONMENT = os.environ.get("ENVIRONMENT", "development")
IS_PRODUCTION = ENVIRONMENT == "production"
def add_service_context(logger, method_name, event_dict):
"""Inject service-level metadata into every log entry."""
event_dict["service"] = os.environ.get("SERVICE_NAME", "unknown")
event_dict["version"] = os.environ.get("SERVICE_VERSION", "0.0.0")
event_dict["environment"] = ENVIRONMENT
return event_dict
def redact_sensitive_fields(logger, method_name, event_dict):
"""Redact PII and financial identifiers before logging."""
sensitive_keys = {"client_id", "account_number", "ssn", "credit_card", "api_key"}
for key in sensitive_keys:
if key in event_dict:
original = str(event_dict[key])
# Preserve first and last 4 characters for traceability
if len(original) > 8:
event_dict[key] = f"{original[:4]}...{original[-4:]}"
else:
event_dict[key] = "***REDACTED***"
return event_dict
def add_correlation_id(logger, method_name, event_dict):
"""Ensure every log entry has a correlation ID for request tracing."""
correlation_id = event_dict.pop("correlation_id", None)
if correlation_id is None:
correlation_id = os.environ.get("_CORRELATION_ID", "no-correlation-id")
event_dict["correlation_id"] = correlation_id
return event_dict
def ensure_order_context(logger, method_name, event_dict):
"""Attach order-specific context if present in the call stack."""
# Check for order context set via bound logger
if hasattr(logger, "_context") and "order_id" in logger._context:
for key in ["order_id", "symbol", "side", "quantity"]:
if key not in event_dict and key in logger._context:
event_dict[key] = logger._context[key]
return event_dict
# Define processors based on environment
if IS_PRODUCTION:
processors = [
structlog.stdlib.filter_by_level,
add_service_context,
add_correlation_id,
ensure_order_context,
redact_sensitive_fields,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.JSONRenderer(),
]
else:
# Development: human-readable console output
processors = [
structlog.stdlib.filter_by_level,
add_service_context,
add_correlation_id,
structlog.stdlib.add_log_level,
structlog.dev.ConsoleRenderer(colors=True),
]
structlog.configure(
processors=processors,
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
def get_logger(name: str = None) -> structlog.stdlib.BoundLogger:
"""Factory function for getting configured loggers."""
return structlog.get_logger(name)
Creating Bound Loggers for Order Flow
The bound logger pattern is essential for financial APIs where an order traverses multiple services:
def process_order(order_id: str, symbol: str, quantity: int, side: str, client_id: str):
"""
Process an order through the trading pipeline.
Demonstrates bound logger pattern for maintaining context
across service boundaries.
"""
log = get_logger("order_processor").bind(
order_id=order_id,
symbol=symbol,
side=side,
client_id=client_id # Will be redacted in production
)
log.info("order_processing_started", quantity=quantity)
try:
# Step 1: Risk validation
risk_log = log.bind(phase="risk_validation")
risk_log.debug("evaluating_risk_parameters")
risk_result = validate_risk(order_id, symbol, quantity, client_id)
if not risk_result.approved:
risk_log.warning(
"risk_validation_rejected",
reason=risk_result.reason,
margin_used=risk_result.margin_used,
margin_available=risk_result.margin_available
)
return {"status": "rejected", "reason": risk_result.reason}
risk_log.info("risk_validation_passed")
# Step 2: Exchange submission
exchange_log = log.bind(phase="exchange_submission")
exchange_log.info("submitting_to_exchange")
exchange_result = submit_to_exchange(symbol, quantity, side)
if exchange_result.status == "filled":
exchange_log.info(
"order_filled",
fill_price=exchange_result.fill_price,
fill_quantity=exchange_result.fill_quantity,
latency_ms=exchange_result.latency_ms
)
else:
exchange_log.error(
"exchange_rejected",
rejection_code=exchange_result.code,
rejection_reason=exchange_result.reason
)
return {
"status": exchange_result.status,
"fill_price": exchange_result.fill_price,
"exchange_order_id": exchange_result.exchange_order_id
}
except Exception as exc:
log.error(
"order_processing_failed",
exception_type=type(exc).__name__,
exception_message=str(exc),
# Include traceback via structlog's exception handling
)
raise
Middleware for HTTP Request Correlation
For REST APIs, inject correlation IDs at the request boundary:
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
import uuid
class StructuredLoggingMiddleware(BaseHTTPMiddleware):
"""
Middleware that:
1. Generates a correlation ID for each request
2. Injects it into the log context
3. Returns it in response headers for client-side tracing
"""
async def dispatch(self, request: Request, call_next):
# Extract or generate correlation ID
correlation_id = request.headers.get("X-Correlation-ID")
if not correlation_id:
correlation_id = str(uuid.uuid4())
# Store in environment for subprocess access
os.environ["_CORRELATION_ID"] = correlation_id
# Bind to logger
log = get_logger("http").bind(
correlation_id=correlation_id,
method=request.method,
path=request.url.path,
client_ip=self._extract_client_ip(request)
)
log.info("request_received")
start_time = time.time()
try:
response = await call_next(request)
duration_ms = (time.time() - start_time) * 1000
log.info(
"request_completed",
status_code=response.status_code,
duration_ms=round(duration_ms, 2)
)
# Include correlation ID in response for client tracing
response.headers["X-Correlation-ID"] = correlation_id
return response
except Exception as exc:
duration_ms = (time.time() - start_time) * 1000
log.error(
"request_failed",
exception_type=type(exc).__name__,
duration_ms=round(duration_ms, 2)
)
raise
@staticmethod
def _extract_client_ip(request: Request) -> str:
forwarded = request.headers.get("X-Forwarded-For")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host if request.client else "unknown"
ELK Stack Integration
Architecture Overview
The ELK stack (Elasticsearch, Logstash, Kibana) is the standard for centralized log analysis in distributed systems. Here is how structured logs flow through the pipeline:
Application → structlog (JSON) → stdout → Filebeat → Logstash → Elasticsearch → Kibana
Filebeat Configuration
# filebeat.yml
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
json.keys_under_root: true
json.add_error_key: true
json.message_key: message
fields:
service: tickdb-api
environment: production
fields_under_root: true
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_cloud_metadata: ~
- add_docker_metadata: ~
output.elasticsearch:
hosts: ["elasticsearch:9200"]
index: "tickdb-logs-%{+yyyy.MM.dd}"
setup.kibana:
host: "kibana:5601"
setup.ilm.enabled: true
setup.ilm.rollover_alias: "tickdb-logs"
setup.ilm.pattern: "{now/d}-000001"
setup.ilm.policy_name: "tickdb-logs-policy"
Elasticsearch Index Template
Define field mappings for financial API logs:
{
"index_patterns": ["tickdb-logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"index.lifecycle.name": "tickdb-logs-policy",
"index.lifecycle.rollover_alias": "tickdb-logs"
},
"mappings": {
"properties": {
"timestamp": { "type": "date" },
"level": { "type": "keyword" },
"event": { "type": "keyword" },
"order_id": { "type": "keyword" },
"symbol": { "type": "keyword" },
"quantity": { "type": "long" },
"price": { "type": "double" },
"side": { "type": "keyword" },
"client_id": { "type": "keyword" },
"correlation_id": { "type": "keyword" },
"service": { "type": "keyword" },
"environment": { "type": "keyword" },
"duration_ms": { "type": "float" },
"status_code": { "type": "integer" },
"reason": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
"exception_type": { "type": "keyword" },
"exception_message": { "type": "text" }
}
}
}
}
Kibana Saved Searches
Create saved searches for common debugging scenarios:
1. Order Tracking by Correlation ID
correlation_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
2. Failed Orders in Time Window
level: error AND event: order_* AND @timestamp: [now-1h TO now]
3. High Latency Requests
duration_ms: > 1000 AND event: request_completed
4. Symbol-Specific Analysis
symbol: AAPL AND @timestamp: [now-24h TO now]
| stats avg(duration_ms), count() by event
Advanced Patterns for Financial Systems
Distributed Tracing Integration
For microservices architectures, propagate trace context:
from opentelemetry import trace
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
propagator = TraceContextTextMapPropagator()
def inject_trace_context(logger):
"""Extract trace context from OpenTelemetry and inject into log context."""
carrier = {}
propagator.inject(carrier)
span = trace.get_current_span()
if span and span.get_span_context().is_valid:
span_context = span.get_span_context()
carrier["trace_id"] = format(span_context.trace_id, "032x")
carrier["span_id"] = format(span_context.span_id, "016x")
return logger.bind(**carrier)
Performance Budget Monitoring
Financial APIs have strict latency SLAs. Log latency with percentile context:
import time
from functools import wraps
from typing import Callable
import asyncio
def log_latency(logger, operation: str, threshold_ms: float = 100):
"""Decorator to log operation latency and alert on threshold breaches."""
def decorator(func: Callable):
@wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = await func(*args, **kwargs)
return result
finally:
duration_ms = (time.perf_counter() - start) * 1000
log_event = logger.info if duration_ms < threshold_ms else logger.warning
log_event(
"operation_completed",
operation=operation,
duration_ms=round(duration_ms, 2),
threshold_ms=threshold_ms,
within_threshold=duration_ms < threshold_ms
)
@wraps(func)
def sync_wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = func(*args, **kwargs)
return result
finally:
duration_ms = (time.perf_counter() - start) * 1000
log_event = logger.info if duration_ms < threshold_ms else logger.warning
log_event(
"operation_completed",
operation=operation,
duration_ms=round(duration_ms, 2),
threshold_ms=threshold_ms,
within_threshold=duration_ms < threshold_ms
)
if asyncio.iscoroutinefunction(func):
return async_wrapper
return sync_wrapper
return decorator
# Usage
log = get_logger("market_data")
@log_latency(log, "fetch_order_book", threshold_ms=50)
def fetch_order_book(symbol: str, depth: int = 10):
"""Fetch order book from exchange with latency monitoring."""
# Implementation
pass
Log Sampling for High-Volume Scenarios
During extreme market volatility, log volume can overwhelm ingestion pipelines. Implement probabilistic sampling:
import random
from typing import Callable, TypeVar
F = TypeVar('F')
def sample_logs(sample_rate: float = 1.0, sample_key: str = "order_id"):
"""
Decorator that probabilistically samples log entries.
Ensures all entries for a given order_id are either
fully sampled or fully dropped (consistent sampling).
"""
def decorator(func: Callable[F]) -> Callable[F]:
sampled_orders = set()
def should_sample(order_id: str) -> bool:
if order_id not in sampled_orders:
if random.random() < sample_rate:
sampled_orders.add(order_id)
return True
return False
return True
@wraps(func)
def wrapper(*args, **kwargs):
order_id = kwargs.get(sample_key) or (args[0] if args else None)
if order_id and not should_sample(order_id):
# Log sampling decision but skip full instrumentation
get_logger(func.__module__).debug(
"log_sampled",
function=func.__name__,
sample_rate=sample_rate
)
return func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper
return decorator
Security Considerations
What Never Goes in Logs
| Category | Examples | Why |
|---|---|---|
| Credentials | API keys, passwords, tokens | Credential stuffing attacks |
| PII | SSN, passport numbers, full names | GDPR, CCPA violations |
| Financial data | Full credit card numbers, bank account details | PCI-DSS violations |
| Authentication tokens | JWT contents, session cookies | Session hijacking |
The Redaction Processor in Detail
import re
from typing import Any
class FinancialDataRedactor:
"""
Production-grade redaction for financial API logs.
Handles common patterns found in trading systems.
"""
# Patterns for financial identifiers
PATTERNS = {
"api_key": re.compile(r'(api[_-]?key["\']?\s*[:=]\s*["\']?)([a-zA-Z0-9_-]{8,})'),
"jwt": re.compile(r'(Bearer\s+)([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)'),
"credit_card": re.compile(r'\b(\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4})\b'),
"account_number": re.compile(r'(account[_-]?num(ber)?["\']?\s*[:=]\s*["\']?)(\d{6,})', re.IGNORECASE),
}
# Fields to redact entirely
FULL_REDACT_FIELDS = {
"password", "secret", "token", "authorization",
"credit_card", "cvv", "ssn", "api_secret"
}
# Fields to partially redact (first/last 4)
PARTIAL_REDACT_FIELDS = {
"client_id", "account_id", "order_id", "user_id"
}
@classmethod
def redact_string(cls, value: str) -> str:
"""Apply regex-based redaction to a string value."""
result = value
# Redact API keys
result = cls.PATTERNS["api_key"].sub(r'\1[REDACTED]', result)
# Redact JWTs
result = cls.PATTERNS["jwt"].sub(r'\1[REDACTED_JWT]', result)
# Redact credit card numbers
result = cls.PATTERNS["credit_card"].sub('[REDACTED_CC]', result)
# Redact account numbers
result = cls.PATTERNS["account_number"].sub(r'\1[REDACTED]', result, flags=re.IGNORECASE)
return result
@classmethod
def redact_dict(cls, data: dict) -> dict:
"""Recursively redact sensitive fields from a dictionary."""
if not isinstance(data, dict):
return data
result = {}
for key, value in data.items():
key_lower = key.lower()
if key_lower in cls.FULL_REDACT_FIELDS:
result[key] = "[REDACTED]"
elif key_lower in cls.PARTIAL_REDACT_FIELDS and isinstance(value, str):
result[key] = cls._partial_redact(value)
elif isinstance(value, str):
redacted = cls.redact_string(value)
result[key] = redacted
elif isinstance(value, dict):
result[key] = cls.redact_dict(value)
elif isinstance(value, list):
result[key] = [
cls.redact_dict(item) if isinstance(item, dict)
else cls.redact_string(str(item)) if isinstance(item, str)
else item
for item in value
]
else:
result[key] = value
return result
@staticmethod
def _partial_redact(value: str) -> str:
"""Partially redact a string value, keeping first/last 4 chars."""
if len(value) <= 8:
return "[REDACTED]"
return f"{value[:4]}...{value[-4:]}"
Validation and Testing
Log Output Testing
Verify that your logging configuration produces valid, queryable output:
import pytest
import json
from io import StringIO
@pytest.fixture
def log_output():
"""Capture structured log output as JSON lines."""
stream = StringIO()
structlog.configure(
processors=[
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(stream=stream),
cache_logger_on_first_use=False,
)
yield stream
stream.close()
def test_order_log_contains_required_fields(log_output):
"""Verify order logs contain all fields required for ELK indexing."""
log = structlog.get_logger()
log.info(
"order_submitted",
order_id="TEST-001",
symbol="AAPL",
quantity=100,
price=185.50,
side="buy"
)
log_output.seek(0)
log_entry = json.loads(log_output.readline())
# Verify required fields exist
assert "level" in log_entry
assert "timestamp" in log_entry
assert log_entry["order_id"] == "TEST-001"
assert log_entry["symbol"] == "AAPL"
assert log_entry["quantity"] == 100
assert log_entry["price"] == 185.50
def test_sensitive_data_is_redacted(log_output):
"""Verify sensitive fields are redacted in production output."""
log = structlog.get_logger()
log.info(
"client_authenticated",
client_id="C-88291",
api_key="sk_live_abcdef123456789",
event="authentication"
)
log_output.seek(0)
log_entry = json.loads(log_output.readline())
# API key should be redacted
assert "sk_live" not in json.dumps(log_entry)
# client_id partial redaction (first 4 chars preserved)
assert log_entry["client_id"] == "C-88...8291"
def test_latency_is_logged_as_float():
"""Verify latency measurements are numeric for aggregation."""
log = get_logger("test")
# ... emit log with duration_ms ...
log_output.seek(0)
log_entry = json.loads(log_output.readline())
assert isinstance(log_entry.get("duration_ms"), (int, float))
Putting It Together: A Complete Example
Here is the full implementation pattern for a financial API endpoint:
"""
TickDB Market Data API - Structured Logging Implementation
https://tickdb.ai
"""
import structlog
import os
import time
from functools import wraps
from typing import Any, Callable
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
import uvicorn
# ============================================================
# CONFIGURATION
# ============================================================
ENVIRONMENT = os.environ.get("ENVIRONMENT", "development")
SERVICE_NAME = os.environ.get("SERVICE_NAME", "tickdb-market-data")
SERVICE_VERSION = os.environ.get("SERVICE_VERSION", "1.0.0")
# ============================================================
# STRUCTURED LOGGING SETUP
# ============================================================
def configure_logging():
"""Configure structlog for production use."""
shared_processors = [
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
]
if ENVIRONMENT == "production":
# Production: JSON output for ELK
structlog.configure(
processors=shared_processors + [
_add_service_context,
_add_correlation_id,
_redact_sensitive_fields,
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
else:
# Development: colored console output
structlog.configure(
processors=shared_processors + [
_add_service_context,
_add_correlation_id,
structlog.dev.ConsoleRenderer(colors=True)
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
def _add_service_context(logger, method_name, event_dict):
event_dict["service"] = SERVICE_NAME
event_dict["version"] = SERVICE_VERSION
event_dict["environment"] = ENVIRONMENT
return event_dict
def _add_correlation_id(logger, method_name, event_dict):
correlation_id = os.environ.get("_CORRELATION_ID", "no-correlation")
event_dict["correlation_id"] = correlation_id
return event_dict
def _redact_sensitive_fields(logger, method_name, event_dict):
REDACTED_FIELDS = {"api_key", "password", "secret", "token"}
for field in REDACTED_FIELDS:
if field in event_dict:
event_dict[field] = "[REDACTED]"
# Partial redaction for client identifiers
PARTIAL_FIELDS = {"client_id", "account_id"}
for field in PARTIAL_FIELDS:
if field in event_dict and isinstance(event_dict[field], str):
val = event_dict[field]
if len(val) > 8:
event_dict[field] = f"{val[:4]}...{val[-4:]}"
return event_dict
# Initialize logging
configure_logging()
log = structlog.get_logger()
# ============================================================
# MIDDLEWARE
# ============================================================
class CorrelationMiddleware:
"""Inject correlation ID into each request context."""
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] == "http":
request = Request(scope)
correlation_id = request.headers.get(
"X-Correlation-ID",
request.headers.get("X-Request-ID", f"req-{int(time.time()*1000)}")
)
os.environ["_CORRELATION_ID"] = correlation_id
# Log request
log.info(
"http_request_started",
method=request.method,
path=request.url.path,
correlation_id=correlation_id
)
await self.app(scope, receive, send)
# ============================================================
# API ENDPOINTS
# ============================================================
app = Starlette(debug=ENVIRONMENT != "production", middleware=[CorrelationMiddleware])
@app.route("/v1/market/kline")
async def get_kline(request: Request):
"""
Get OHLCV kline data for a symbol.
Query parameters:
symbol: Trading symbol (e.g., AAPL.US)
interval: Kline interval (1m, 5m, 1h, 1d)
limit: Number of klines to return (max 1000)
"""
symbol = request.query_params.get("symbol")
interval = request.query_params.get("interval", "1h")
limit = min(int(request.query_params.get("limit", 100)), 1000)
request_log = log.bind(symbol=symbol, interval=interval, limit=limit)
request_log.info("kline_request_received")
start_time = time.perf_counter()
try:
# Validate symbol
if not symbol:
request_log.warning("kline_request_invalid", reason="missing_symbol")
return JSONResponse(
{"error": "symbol parameter is required", "code": "INVALID_PARAMETER"},
status_code=400
)
# Fetch data (placeholder for actual TickDB API call)
# In production, this would use the TickDB SDK
data = await fetch_kline_data(symbol, interval, limit)
duration_ms = (time.perf_counter() - start_time) * 1000
request_log.info(
"kline_request_completed",
duration_ms=round(duration_ms, 2),
record_count=len(data.get("klines", []))
)
return JSONResponse(data)
except Exception as exc:
duration_ms = (time.perf_counter() - start_time) * 1000
request_log.error(
"kline_request_failed",
duration_ms=round(duration_ms, 2),
error_type=type(exc).__name__,
error_message=str(exc)
)
return JSONResponse(
{"error": "Internal server error", "code": "INTERNAL_ERROR"},
status_code=500
)
async def fetch_kline_data(symbol: str, interval: str, limit: int) -> dict:
"""
Fetch kline data from TickDB.
This is a placeholder implementation.
Production code would use the TickDB Python SDK:
from tickdb import TickDB
client = TickDB(api_key=os.environ["TICKDB_API_KEY"])
data = client.market.kline.get(
symbol=symbol,
interval=interval,
limit=limit
)
"""
# Simulate data retrieval
await asyncio.sleep(0.05) # Simulate network latency
return {
"symbol": symbol,
"interval": interval,
"klines": [
{
"timestamp": "2026-04-01T09:30:00Z",
"open": 185.50,
"high": 186.20,
"low": 185.10,
"close": 186.00,
"volume": 1250000
}
]
}
# ============================================================
# STARTUP
# ============================================================
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8000))
uvicorn.run(
"main:app",
host="0.0.0.0",
port=port,
log_level="info"
)
Summary
The journey from print() to structlog is not cosmetic. It is a fundamental shift in how we treat log data: from human-readable strings to machine-queriable events.
For financial APIs specifically, structured logging delivers measurable benefits:
- Incident response time: A correlation ID query in Kibana traces an order across all services in under a second.
- Audit compliance: JSON logs with field-level typing satisfy regulatory requirements for immutable, timestamped records.
- Performance monitoring: Latency fields enable percentile calculations without post-processing.
- Security: Redaction processors prevent PII from entering log streams.
- Scalability: JSON serialization is CPU-efficient at high throughput; sampling prevents pipeline overload during volatile markets.
The configuration shown in this article is production-ready for a financial data API. It includes correlation ID propagation, sensitive data redaction, latency monitoring, and ELK integration. Adapt the processors to your specific compliance requirements and deployment constraints.
Logging is infrastructure. Treat it with the same rigor you apply to your trading logic.
Next Steps
If you're building a trading system and need reliable market data alongside observability tooling:
- Sign up at tickdb.ai for API access (free tier available)
- Explore the documentation for WebSocket depth channels and historical kline endpoints
- Clone the structured logging example from this article and adapt the redaction rules to your compliance requirements
If you're instrumenting an existing Python service:
- Install structlog:
pip install structlog - Copy the processor chain from the production configuration section
- Replace string interpolation with structured key-value pairs
- Deploy Filebeat with the provided configuration to ship JSON logs to your ELK cluster
If you need historical data for backtesting your logging-aware strategies:
Reach out to enterprise@tickdb.ai for institutional data plans covering 10+ years of cleaned US equity OHLCV data.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Structured logging is an engineering practice applicable to any software system, regardless of the domain.