| """Structured logging utilities for the trading system. |
| |
| Provides JSON-formatted logging with correlation IDs for tracing |
| requests across services. |
| """ |
| import logging |
| import json |
| import time |
| import uuid |
| import threading |
| from functools import wraps |
|
|
| |
| _context = threading.local() |
|
|
|
|
| def get_correlation_id(): |
| """Get current correlation ID, or generate a new one.""" |
| if not hasattr(_context, 'correlation_id') or _context.correlation_id is None: |
| _context.correlation_id = str(uuid.uuid4())[:8] |
| return _context.correlation_id |
|
|
|
|
| def set_correlation_id(correlation_id): |
| """Set correlation ID for current thread.""" |
| _context.correlation_id = correlation_id |
|
|
|
|
| def clear_correlation_id(): |
| """Clear correlation ID for current thread.""" |
| _context.correlation_id = None |
|
|
|
|
| class JSONFormatter(logging.Formatter): |
| """JSON log formatter for structured logging.""" |
|
|
| def format(self, record): |
| log_entry = { |
| "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(record.created)), |
| "level": record.levelname, |
| "logger": record.name, |
| "message": record.getMessage(), |
| "correlation_id": get_correlation_id(), |
| } |
|
|
| |
| if hasattr(record, 'component'): |
| log_entry["component"] = record.component |
|
|
| |
| if hasattr(record, 'extra_data') and record.extra_data: |
| log_entry.update(record.extra_data) |
|
|
| |
| if record.exc_info: |
| log_entry["exception"] = self.formatException(record.exc_info) |
|
|
| |
| if record.levelno >= logging.ERROR: |
| log_entry["source"] = { |
| "file": record.filename, |
| "line": record.lineno, |
| "function": record.funcName |
| } |
|
|
| return json.dumps(log_entry) |
|
|
|
|
| def setup_logging(component_name, level=logging.INFO, json_format=True): |
| """Configure logging for a service component. |
| |
| Args: |
| component_name: Name of the service (e.g., "matcher", "dashboard") |
| level: Logging level |
| json_format: Use JSON formatting (True) or standard format (False) |
| |
| Returns: |
| Logger instance |
| """ |
| logger = logging.getLogger(component_name) |
| logger.setLevel(level) |
|
|
| |
| logger.handlers = [] |
|
|
| |
| handler = logging.StreamHandler() |
| handler.setLevel(level) |
|
|
| if json_format: |
| handler.setFormatter(JSONFormatter()) |
| else: |
| handler.setFormatter(logging.Formatter( |
| '%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
| )) |
|
|
| logger.addHandler(handler) |
|
|
| return logger |
|
|
|
|
| class StructuredLogger: |
| """Convenience wrapper for structured logging.""" |
|
|
| def __init__(self, component_name, json_format=True): |
| self.logger = setup_logging(component_name, json_format=json_format) |
| self.component = component_name |
|
|
| def _log(self, level, message, **extra): |
| """Internal logging method with extra data.""" |
| record = self.logger.makeRecord( |
| self.logger.name, level, "", 0, message, (), None |
| ) |
| record.component = self.component |
| record.extra_data = extra if extra else None |
| self.logger.handle(record) |
|
|
| def info(self, message, **extra): |
| self._log(logging.INFO, message, **extra) |
|
|
| def debug(self, message, **extra): |
| self._log(logging.DEBUG, message, **extra) |
|
|
| def warning(self, message, **extra): |
| self._log(logging.WARNING, message, **extra) |
|
|
| def error(self, message, **extra): |
| self._log(logging.ERROR, message, **extra) |
|
|
| def order_received(self, order_id, symbol, side, quantity, price, source=None): |
| """Log order received event.""" |
| self.info("order_received", event="order_received", |
| order_id=order_id, symbol=symbol, side=side, |
| quantity=quantity, price=price, source=source) |
|
|
| def trade_executed(self, trade_id, symbol, price, quantity, buy_id, sell_id): |
| """Log trade execution event.""" |
| self.info("trade_executed", event="trade_executed", |
| trade_id=trade_id, symbol=symbol, price=price, |
| quantity=quantity, buy_id=buy_id, sell_id=sell_id) |
|
|
| def order_cancelled(self, order_id, reason=None): |
| """Log order cancellation event.""" |
| self.info("order_cancelled", event="order_cancelled", |
| order_id=order_id, reason=reason) |
|
|
|
|
| def with_correlation_id(func): |
| """Decorator to set correlation ID for request handling.""" |
| @wraps(func) |
| def wrapper(*args, **kwargs): |
| |
| try: |
| from flask import request |
| correlation_id = request.headers.get('X-Correlation-ID') |
| if correlation_id: |
| set_correlation_id(correlation_id) |
| else: |
| set_correlation_id(str(uuid.uuid4())[:8]) |
| except: |
| set_correlation_id(str(uuid.uuid4())[:8]) |
|
|
| try: |
| return func(*args, **kwargs) |
| finally: |
| clear_correlation_id() |
|
|
| return wrapper |
|
|