Skip to main content

Logging Guide

Overview

The application uses Python’s standard logging module with a centralized configuration in app/utils/logger.py. It supports two output formats — plain text for development and structured JSON for production log aggregators (Datadog, ELK, CloudWatch).

Quick Start

# Preferred — uses the centralized config
from app.utils.logger import get_logger
logger = get_logger(__name__)

# Also works — inherits root handler config via hierarchy
import logging
logger = logging.getLogger(__name__)
Both patterns work. get_logger(__name__) namespaces under app.* which makes filtering easier in aggregators.

Configuration

All logging configuration is driven by environment variables:
VariableDefaultDescription
APP_ENVDEVControls default log level and format
LOG_LEVELDEBUG (dev/test), INFO (prod)Override log level
LOG_FORMATtext (dev/test), json (prod)Output format: text or json
SERVICE_NAMEsyntropy-journalsService identifier in structured logs

Format Selection

APP_ENVDefault LOG_FORMATDefault LOG_LEVEL
DEVtextDEBUG
TESTtextDEBUG
PRODjsonINFO
Override anytime: LOG_FORMAT=json make dev to test JSON output locally.

Output Formats

Text (development)

2025-01-15 14:30:22 - app.syntropy_health.states.chat.chat - INFO - send_message called. user_input='hello'

JSON (production)

{
  "timestamp": "2025-01-15T14:30:22.123456+00:00",
  "level": "INFO",
  "logger": {"name": "app.syntropy_health.states.chat.chat"},
  "message": "send_message called. user_input='hello'",
  "service": "syntropy-journals"
}
Error logs include exception details:
{
  "timestamp": "2025-01-15T14:30:22.123456+00:00",
  "level": "ERROR",
  "logger": {"name": "app.syntropy_health.states.chat.chat"},
  "message": "Exception during streaming: Connection refused",
  "service": "syntropy-journals",
  "source": {"file": "app/syntropy_health/states/chat/chat.py", "line": 389, "function": "stream_bot_response"},
  "error": {
    "kind": "ConnectionError",
    "message": "Connection refused",
    "stack": ["Traceback (most recent call last):", "..."]
  }
}

Datadog Integration

The JSON format follows Datadog’s default attribute conventions:
  • timestamp — parsed automatically as the log date
  • level — mapped to Datadog severity
  • service — used for service filtering in Log Explorer
  • error.kind, error.message, error.stack — populate the error panel

Setup with Datadog Agent

  1. Set environment variables in your deployment:
   APP_ENV=PROD
   SERVICE_NAME=syntropy-journals
  1. Configure the Datadog Agent to tail stdout (containerized) or the log file:
    # datadog.yaml or container label
    logs:
      - type: file  # or "docker" for containers
        path: /app/logs/app.log
        service: syntropy-journals
        source: python
    
  2. For containerized deployments (Railway, Docker), Datadog auto-discovers JSON logs from stdout — no additional pipeline configuration needed.

Useful Datadog Queries

# All errors for a service
service:syntropy-journals level:ERROR

# Chat streaming errors
service:syntropy-journals logger.name:app.syntropy_health.states.chat.chat level:ERROR

# LLM client issues
service:syntropy-journals logger.name:app.syntropy_health.functions.llm.* level:ERROR

# Slow operations (search by message pattern)
service:syntropy-journals "Loaded * messages for session"

File Structure

app/
├── utils/
│   └── logger.py          # Centralized config (JSONFormatter, get_logger)
└── logs/
    └── app.log            # Local file output (auto-created)

Logger Naming Convention

PatternUsed ByNamespace
get_logger(__name__)Reflex states, UI functions, db_utilsapp.<module.path>
logging.getLogger(__name__)LLM modules, API services, hydra_config<module.path>
Both inherit the root handler configuration. For new code, prefer get_logger(__name__) for the consistent app.* prefix which simplifies Datadog facet filtering.

Best Practices

  1. Use lazy formattinglogger.info("User %s sent %d messages", user_id, count) not f-strings
  2. Include exc_info=True for errorslogger.error("Failed: %s", e, exc_info=True) to get stack traces in JSON output
  3. Log at boundaries — Log when entering/exiting external calls (DB, LLM APIs), not inside tight loops
  4. Never log secrets — Don’t log API keys, tokens, or full connection strings
  5. Use structured context — For repeated context (session_id, user_id), include them in the message: logger.info("session=%s action=send_message", session_id)