Skip to content

16 - Centralized Logging

Overview

Tradeboard implements centralized Python logging with configurable levels, colored console output, and optional retained files. General application logs are not stored in logs.db: logs.db is the traffic/security store, while order and analyzer audit rows live in the main database.

Architecture Diagram

┌───────────────────────────────────────────────────────────────────────────────┐
│                       Centralized Logging Architecture                        │
└───────────────────────────────────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────────────────────────────────┐
│                            Application Components                             │
│                                                                               │
│  ┌────────────┐    ┌────────────┐    ┌────────────┐    ┌────────────┐         │
│  │ Flask      │    │ REST API   │    │ WebSocket  │    │ Services   │         │
│  │ Routes     │    │ Endpoints  │    │ Proxy      │    │            │         │
│  └──────┬─────┘    └──────┬─────┘    └──────┬─────┘    └──────┬─────┘         │
│         │                 │                 │                 │               │
│         └─────────────────┴────────┬────────┴─────────────────┘               │
│                                    │                                          │
│                                    ▼                                          │
│                    ┌──────────────────────────────┐                           │
│                    │ setup_logging(): root logger │                           │
│                    └──────────────────────────────┘                           │
└────────────────────────────────────┼──────────────────────────────────────────┘

                    ┌────────────────┴──────────────────────┐
                    │                                       │
                    ▼                                       ▼
   ┌────────────────────────────────┐      ┌────────────────────────────────┐
   │ Console handler                │      │ TimedRotatingFileHandler       │
   │ ColoredFormatter, LOG_COLORS   │      │ when="midnight", interval=1    │
   │ SensitiveDataFilter            │      │ backupCount=LOG_RETENTION      │
   │ stdout, always on              │      │ only if LOG_TO_FILE=True       │
   └────────────────────────────────┘      └────────────────────────────────┘
                    │                                       │
                    ▼                                       ▼
   ┌────────────────────────────────┐      ┌────────────────────────────────┐
   │ log/errors.jsonl               │      │ log/tradeboard_YYYY-MM-DD.log    │
   │ ERROR and above, JSON lines    │      │ one file per day, kept for     │
   │ always on, trimmed at boot     │      │ LOG_RETENTION days             │
   └────────────────────────────────┘      └────────────────────────────────┘

Configuration

Environment Variables

Defaults below are the values utils/logging.py falls back to when the variable is unset.

bash
# Enable/disable file logging (default: False)
LOG_TO_FILE='False'

# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)
LOG_LEVEL='INFO'

# Log directory (default: log)
LOG_DIR='log'

# Log format
LOG_FORMAT='[%(asctime)s] %(levelname)s in %(module)s: %(message)s'

# Days of rotated log files to retain (default: 14)
LOG_RETENTION='14'

# Colored console output (default: True)
LOG_COLORS='True'

Usage

Getting a Logger

python
from utils.logging import get_logger

logger = get_logger(__name__)

# Log at different levels
logger.debug("Debug message")
logger.info("Info message")
logger.warning("Warning message")
logger.error("Error message")
logger.critical("Critical message")

Log Levels

LevelValueUse Case
DEBUG10Detailed debugging information
INFO20General operational messages
WARNING30Something unexpected happened
ERROR40Error occurred, operation failed
CRITICAL50System is unusable

Implementation

Location: utils/logging.py

Handlers are attached once to the root logger by setup_logging(), which runs at module import. get_logger(name) is only a thin wrapper around logging.getLogger(name), so per-module loggers inherit the root configuration.

python
import logging
import os
from logging.handlers import TimedRotatingFileHandler

def setup_logging():
    """Initialize the logging configuration from environment variables."""
    root_logger = logging.getLogger()
    root_logger.setLevel(getattr(logging, log_level, logging.INFO))
    root_logger.handlers = []

    sensitive_filter = SensitiveDataFilter()

    # Console handler - ColoredFormatter honours LOG_COLORS
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(ColoredFormatter(log_format, enable_colors=log_colors))
    console_handler.addFilter(sensitive_filter)
    root_logger.addHandler(console_handler)

    # File handler (only when LOG_TO_FILE=True) - rotates daily at midnight
    if log_to_file:
        cleanup_old_logs(log_path, log_retention)
        log_file = log_path / f"tradeboard_{datetime.now().strftime('%Y-%m-%d')}.log"
        file_handler = TimedRotatingFileHandler(
            filename=str(log_file),
            when="midnight",
            interval=1,
            backupCount=log_retention,
            encoding="utf-8",
        )
        file_handler.setFormatter(logging.Formatter(log_format))
        file_handler.addFilter(sensitive_filter)
        root_logger.addHandler(file_handler)

    # JSON error log - always active, ERROR and above, log/errors.jsonl
    json_handler = logging.FileHandler(filename=str(errors_file), encoding="utf-8")
    json_handler.setLevel(logging.ERROR)
    json_handler.setFormatter(JSONErrorFormatter())
    json_handler.addFilter(sensitive_filter)
    root_logger.addHandler(json_handler)

def get_logger(name: str) -> logging.Logger:
    """Get a logger instance for a module."""
    return logging.getLogger(name)

# Initialize logging on import
setup_logging()

Filters And Noise Suppression

ClassEffect
SensitiveDataFilterRedacts credentials and tokens from every handler
ColoredFormatterLevel-based console colors, controlled by LOG_COLORS
JSONErrorFormatterStructured ERROR+ records for log/errors.jsonl
WerkzeugErrorFilterDrops known development-server noise
WebSocketHandshakeFilterDrops short-lived WebSocket handshake errors

setup_logging() also raises the level of werkzeug, urllib3, requests, httpx, httpcore, hpack, apscheduler, websockets and telegram loggers so third-party chatter stays out of the console.

Error Log

log/errors.jsonl is written unconditionally, independent of LOG_TO_FILE. It captures ERROR and above as one JSON object per line and is truncated to the last 1000 entries at startup so it cannot grow without bound.

Log Categories

Application Logs

CategoryLogger NameDescription
Authblueprints.authLogin/logout events
Ordersrestx_api.place_orderOrder placement
WebSocketwebsocket_proxyWS connections
Strategyblueprints.strategyStrategy execution

Example Log Output

[2024-01-15 09:30:15] INFO in auth: User admin logged in successfully
[2024-01-15 09:30:20] INFO in place_order: Order placed - SBIN BUY 100 MIS
[2024-01-15 09:30:21] DEBUG in broker_api: Broker response: {"orderid": "123456"}
[2024-01-15 09:31:00] WARNING in session: Session expiring in 5 minutes
[2024-01-15 15:30:00] INFO in squareoff: Auto square-off triggered for MIS positions

Startup Banner

python
from utils.logging import log_startup_banner

def log_startup_banner(logger_instance, title: str, url: str,
                       separator_char: str = "=", width: int = 60):
    """Log a highlighted startup banner with a single URL."""

log_startup_banner(logger, "Tradeboard is running", "http://127.0.0.1:5000")

The banner is three logged lines between two separator lines of separator_char, not a drawn box. Colors come from colorama and are suppressed when LOG_COLORS is false unless FORCE_COLOR is set. app.py imports the helper but does not currently call it.

Sample output at the default separator_char="=" and width=60:

============================================================
Tradeboard is running
Access the application at: http://127.0.0.1:5000
============================================================

File Rotation

log/
├── tradeboard_2026-08-23.log    # Today, written while LOG_TO_FILE=True
├── tradeboard_2026-08-22.log    # Rolled at midnight
├── ...
├── tradeboard_2026-08-09.log    # Oldest kept, backupCount=LOG_RETENTION
├── errors.jsonl               # ERROR and above, always on
└── strategies/                # Per-strategy subprocess logs

Rotation is time based, not size based. TimedRotatingFileHandler rolls at midnight and keeps LOG_RETENTION days of history.

Rotation Settings

SettingValue in codeDescription
HandlerTimedRotatingFileHandlerTime based, not size based
Whenmidnight, interval=1One rotation per day
Backup CountLOG_RETENTION (default 14)Number of rotated files to keep
Encodingutf-8File handler encoding
Startup cleanupcleanup_old_logs(log_dir, retention_days)Deletes files older than the retention window
CompressionNoneRotated files are not compressed

Viewing Logs

File Logs

bash
# View today's log
cat log/tradeboard_$(date +%F).log

# Follow log in real-time
tail -f log/tradeboard_$(date +%F).log

# View last 100 lines
tail -100 log/tradeboard_$(date +%F).log

# Search for errors
grep ERROR log/tradeboard_$(date +%F).log

# Structured error records (always written)
tail -f log/errors.jsonl

UI Log Viewer

blueprints/log.py serves the order-log viewer at /logs (with /logs/export), and blueprints/logging.py serves the consolidated dashboard at /logging, which links live logs, analyzer logs, traffic, latency and security views.

Key Files Reference

FilePurpose
utils/logging.pysetup_logging(), get_logger(), formatters and filters
blueprints/log.pyOrder log viewer at /logs and /logs/export
blueprints/logging.pyConsolidated logging dashboard at /logging
database/apilog_db.pyOrder API audit rows in the main database
database/analyzer_db.pyAnalyzer audit rows in the main database
database/traffic_db.pyRequest traffic rows in logs.db
log/Dated log files plus errors.jsonl