2025-11-21 07:52:27 +05:30
|
|
|
import logging
|
2025-10-03 13:20:24 +05:30
|
|
|
import os
|
|
|
|
|
import sys
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
import loguru
|
|
|
|
|
|
2026-03-05 13:51:57 +05:30
|
|
|
from api.constants import (
|
|
|
|
|
ENVIRONMENT,
|
|
|
|
|
LOG_COMPRESSION,
|
|
|
|
|
LOG_FILE_PATH,
|
|
|
|
|
LOG_LEVEL,
|
|
|
|
|
LOG_RETENTION,
|
|
|
|
|
LOG_ROTATION_SIZE,
|
|
|
|
|
SERIALIZE_LOG_OUTPUT,
|
|
|
|
|
)
|
2025-09-09 14:37:32 +05:30
|
|
|
from api.enums import Environment
|
|
|
|
|
from api.utils.worker import get_worker_id, is_worker_process
|
2026-03-05 13:51:57 +05:30
|
|
|
from pipecat.utils.run_context import run_id_var
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
# Track if logging has been initialized
|
|
|
|
|
_logging_initialized = False
|
|
|
|
|
|
|
|
|
|
|
2025-11-21 07:52:27 +05:30
|
|
|
class InterceptHandler(logging.Handler):
|
|
|
|
|
"""
|
|
|
|
|
Intercept standard library logging calls and redirect them to loguru.
|
|
|
|
|
This allows us to capture uvicorn and other library logs through loguru.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def emit(self, record):
|
|
|
|
|
# Get corresponding Loguru level if it exists
|
|
|
|
|
try:
|
|
|
|
|
level = loguru.logger.level(record.levelname).name
|
|
|
|
|
except ValueError:
|
|
|
|
|
level = record.levelno
|
|
|
|
|
|
|
|
|
|
# Use the original record's information instead of trying to find the caller
|
|
|
|
|
# This preserves the logger name (e.g., "uvicorn.access") in the logs
|
|
|
|
|
loguru.logger.patch(lambda r: r.update(name=record.name)).opt(
|
|
|
|
|
exception=record.exc_info
|
|
|
|
|
).log(level, record.getMessage())
|
|
|
|
|
|
|
|
|
|
|
2025-09-09 14:37:32 +05:30
|
|
|
def inject_run_id(record):
|
|
|
|
|
"""Inject run_id from context variable into log record"""
|
|
|
|
|
record["extra"]["run_id"] = run_id_var.get()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_logging():
|
|
|
|
|
"""Set up logging for the main application"""
|
2025-09-26 10:57:51 +05:30
|
|
|
global _logging_initialized
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
# Return early if already initialized
|
|
|
|
|
if _logging_initialized:
|
2025-09-26 10:57:51 +05:30
|
|
|
return
|
2025-09-09 14:37:32 +05:30
|
|
|
|
2026-03-05 13:51:57 +05:30
|
|
|
log_level = LOG_LEVEL
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
# Don't setup logging in test environment
|
|
|
|
|
if ENVIRONMENT == Environment.TEST.value:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Remove default loguru handler
|
|
|
|
|
try:
|
|
|
|
|
loguru.logger.remove(0)
|
|
|
|
|
except ValueError:
|
|
|
|
|
# Handler might already be removed
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Patch loguru to inject run_id
|
|
|
|
|
patched = loguru.logger.patch(inject_run_id)
|
|
|
|
|
|
2026-03-05 13:51:57 +05:30
|
|
|
log_format = "{time:YYYY-MM-DD HH:mm:ss.SSS} | <level>{level}</level> | [run_id={extra[run_id]}] | {file.name}:{line} | {message}"
|
2025-09-09 14:37:32 +05:30
|
|
|
|
2025-09-26 10:57:51 +05:30
|
|
|
# Add handler - either file or console
|
2025-09-09 14:37:32 +05:30
|
|
|
if LOG_FILE_PATH:
|
|
|
|
|
# Determine the actual log file path
|
|
|
|
|
actual_log_path = LOG_FILE_PATH
|
|
|
|
|
|
|
|
|
|
# If we're in a worker process, append worker ID to the filename
|
|
|
|
|
if is_worker_process():
|
|
|
|
|
worker_id = get_worker_id()
|
|
|
|
|
# Split the path to insert worker ID before extension
|
|
|
|
|
base_path, ext = os.path.splitext(LOG_FILE_PATH)
|
|
|
|
|
actual_log_path = f"{base_path}-worker-{worker_id}{ext}"
|
|
|
|
|
|
|
|
|
|
patched.add(
|
|
|
|
|
actual_log_path,
|
|
|
|
|
level=log_level,
|
2025-10-09 17:54:31 +05:30
|
|
|
serialize=SERIALIZE_LOG_OUTPUT, # Use JSON serialization for structured logs
|
2025-09-09 14:37:32 +05:30
|
|
|
enqueue=True, # Thread-safe writing
|
2025-09-28 18:13:28 +05:30
|
|
|
backtrace=True, # Include full traceback in exceptions
|
|
|
|
|
diagnose=False, # Don't include local variables in traceback for security
|
2026-03-05 13:51:57 +05:30
|
|
|
rotation=LOG_ROTATION_SIZE, # Rotate when file reaches size limit
|
|
|
|
|
retention=LOG_RETENTION, # Keep old logs for this duration
|
|
|
|
|
compression=LOG_COMPRESSION, # Compress rotated logs
|
2025-09-09 14:37:32 +05:30
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# Console handler (existing behavior)
|
|
|
|
|
patched.add(
|
|
|
|
|
sys.stdout,
|
|
|
|
|
format=log_format,
|
|
|
|
|
level=log_level,
|
|
|
|
|
colorize=True,
|
|
|
|
|
)
|
|
|
|
|
|
2025-09-26 10:57:51 +05:30
|
|
|
loguru.logger = patched
|
2025-11-21 07:52:27 +05:30
|
|
|
|
|
|
|
|
# Intercept standard library logging (uvicorn, etc.) and redirect to loguru
|
|
|
|
|
# Set level to INFO to avoid debug logs from libraries
|
|
|
|
|
logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO, force=True)
|
|
|
|
|
|
|
|
|
|
# Specifically intercept uvicorn loggers with INFO level
|
|
|
|
|
for logger_name in ["uvicorn", "uvicorn.error", "uvicorn.access"]:
|
|
|
|
|
logging_logger = logging.getLogger(logger_name)
|
|
|
|
|
logging_logger.handlers = [InterceptHandler()]
|
|
|
|
|
logging_logger.setLevel(logging.INFO)
|
|
|
|
|
logging_logger.propagate = False
|
|
|
|
|
|
2025-09-26 10:57:51 +05:30
|
|
|
_logging_initialized = True
|