Source code for neurocore.logging.setup

"""NeuroCore structured logging — configures structlog for the entire application.

Reuses the same pattern as NeuroWeave: structlog with console (colored, dev)
and JSON (machine-parseable, production) modes. Adds optional file handler
support for persistent log output.

Usage:
    from neurocore.logging import configure_logging, get_logger
    from neurocore.config import load_config

    config = load_config()
    configure_logging(config)

    log = get_logger("my-component")
    log.info("started", version="0.1.0")
"""

from __future__ import annotations

import logging
import sys
from typing import TYPE_CHECKING

import structlog

if TYPE_CHECKING:
    from neurocore.config.schema import NeuroCoreConfig

# Track whether logging has been configured to prevent double-init
_configured: bool = False


[docs] def configure_logging(config: NeuroCoreConfig) -> None: """Configure structlog and stdlib logging from NeuroCoreConfig. Call once at startup. After this, any module can do: from neurocore.logging import get_logger log = get_logger("skills") log.info("skill.loaded", name="neuroweave", version="0.1.0") Args: config: NeuroCoreConfig with logging.level, logging.format, logging.file. """ global _configured log_level = getattr(logging, config.logging.level.value.upper(), logging.INFO) # Shared processors — run for every log event shared_processors: list[structlog.types.Processor] = [ structlog.contextvars.merge_contextvars, structlog.stdlib.add_log_level, structlog.stdlib.add_logger_name, structlog.processors.TimeStamper(fmt="iso", utc=False), structlog.processors.StackInfoRenderer(), structlog.processors.UnicodeDecoder(), ] # Choose renderer based on format from neurocore.config.schema import LogFormat if config.logging.format == LogFormat.JSON: renderer: structlog.types.Processor = structlog.processors.JSONRenderer() else: renderer = structlog.dev.ConsoleRenderer(colors=sys.stderr.isatty()) # Configure structlog structlog.configure( processors=[ *shared_processors, structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=not _configured, # Don't cache on reconfig ) # Configure stdlib root logger so structlog output actually appears formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, renderer, ], ) handler = logging.StreamHandler(sys.stderr) handler.setFormatter(formatter) root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.addHandler(handler) root_logger.setLevel(log_level) # Optional file handler if config.logging.file is not None: file_path = config.resolve_path(config.logging.file) file_path.parent.mkdir(parents=True, exist_ok=True) # File output always uses JSON for machine parseability file_formatter = structlog.stdlib.ProcessorFormatter( processors=[ structlog.stdlib.ProcessorFormatter.remove_processors_meta, structlog.processors.JSONRenderer(), ], ) file_handler = logging.FileHandler(str(file_path), encoding="utf-8") file_handler.setFormatter(file_formatter) root_logger.addHandler(file_handler) # Quiet noisy third-party loggers for name in ("uvicorn", "uvicorn.access", "httpx", "httpcore", "asyncio"): logging.getLogger(name).setLevel(max(log_level, logging.WARNING)) _configured = True
[docs] def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger: """Get a structlog logger, optionally bound to a component name. Args: name: Component name (e.g. "skills", "config", "runtime"). Added as 'component' key in log events. Returns: A bound structlog logger. """ log = structlog.get_logger() if name: log = log.bind(component=name) return log
[docs] def reset_logging() -> None: """Reset logging configuration. Primarily for testing.""" global _configured structlog.reset_defaults() root_logger = logging.getLogger() root_logger.handlers.clear() root_logger.setLevel(logging.WARNING) _configured = False