Skip to content

Index

provide.foundation.logger.setup

Foundation Logger Setup Module.

Handles structured logging configuration, processor setup, and emoji resolution. Provides the core setup functionality for the Foundation logging system.

Functions

get_system_logger

get_system_logger(
    name: str, config: TelemetryConfig | None = None
) -> object

Get a vanilla Python logger without Foundation enhancements.

This provides a plain Python logger that respects FOUNDATION_LOG_LEVEL but doesn't trigger Foundation's initialization. Use this for logging during Foundation's setup phase or when you need to avoid circular dependencies.

Parameters:

Name Type Description Default
name str

Logger name (e.g., "provide.foundation.otel.setup")

required
config TelemetryConfig | None

Optional TelemetryConfig to use for log level and output

None

Returns:

Type Description
object

A StructuredStdlibLogger instance that accepts structlog-style kwargs

Note

"Vanilla" means plain/unmodified Python logging, without Foundation's features like emoji prefixes or structured logging.

Source code in provide/foundation/logger/setup/coordinator.py
def get_system_logger(name: str, config: TelemetryConfig | None = None) -> object:
    """Get a vanilla Python logger without Foundation enhancements.

    This provides a plain Python logger that respects FOUNDATION_LOG_LEVEL
    but doesn't trigger Foundation's initialization. Use this for logging
    during Foundation's setup phase or when you need to avoid circular
    dependencies.

    Args:
        name: Logger name (e.g., "provide.foundation.otel.setup")
        config: Optional TelemetryConfig to use for log level and output

    Returns:
        A StructuredStdlibLogger instance that accepts structlog-style kwargs

    Note:
        "Vanilla" means plain/unmodified Python logging, without
        Foundation's features like emoji prefixes or structured logging.

    """
    import logging
    import sys

    slog = logging.getLogger(name)

    # Configure only once per logger
    if not slog.handlers:
        log_level = get_foundation_log_level(config)
        slog.setLevel(log_level)

        # Respect FOUNDATION_LOG_OUTPUT setting from config or env
        if config is not None:
            output = config.logging.foundation_log_output.lower()
        else:
            # Load config to get foundation_log_output
            temp_config = TelemetryConfig.from_env()
            output = temp_config.logging.foundation_log_output.lower()

        stream = sys.stderr if output != "stdout" else sys.stdout

        # Check if output stream is a TTY for color support
        is_tty = hasattr(stream, "isatty") and stream.isatty()

        # Use shared formatter to ensure consistency with structlog
        class SharedFormatter(logging.Formatter):
            """Formatter that uses the shared formatting function."""

            def __init__(self, use_colors: bool = False) -> None:
                """Initialize formatter with color support setting."""
                super().__init__()
                self.use_colors = use_colors

            def format(self, record: logging.LogRecord) -> str:
                # Add log emoji prefix to stdlib logger messages

                # Get the base message
                message = record.getMessage()

                # Extract structured logging key-value pairs from record.__dict__
                # These are added via the extra={} parameter
                kvs = []
                # Known record attributes to skip
                skip_attrs = {
                    "name",
                    "msg",
                    "args",
                    "created",
                    "filename",
                    "funcName",
                    "levelname",
                    "levelno",
                    "lineno",
                    "module",
                    "msecs",
                    "message",
                    "pathname",
                    "process",
                    "processName",
                    "relativeCreated",
                    "thread",
                    "threadName",
                    "exc_info",
                    "exc_text",
                    "stack_info",
                    "taskName",
                }

                for key, value in record.__dict__.items():
                    if key not in skip_attrs:
                        if self.use_colors:
                            # Color keys in cyan, values in magenta
                            kvs.append(f"\033[36m{key}\033[0m=\033[35m{value}\033[0m")
                        else:
                            kvs.append(f"{key}={value}")

                if kvs:
                    message = f"{message} {' '.join(kvs)}"

                return format_foundation_log_message(
                    timestamp=record.created,
                    level_name=record.levelname,
                    message=message,
                    use_colors=self.use_colors,
                )

        handler = logging.StreamHandler(stream)
        handler.setLevel(log_level)
        handler.setFormatter(SharedFormatter(use_colors=is_tty))
        slog.addHandler(handler)

        # Don't propagate to avoid duplicate messages
        slog.propagate = False

    return StructuredStdlibLogger(slog)

internal_setup

internal_setup(
    config: TelemetryConfig | None = None,
    is_explicit_call: bool = False,
) -> None

The single, internal setup function that both explicit and lazy setup call. It is protected by the _PROVIDE_SETUP_LOCK in its callers.

Source code in provide/foundation/logger/setup/coordinator.py
def internal_setup(config: TelemetryConfig | None = None, is_explicit_call: bool = False) -> None:
    """The single, internal setup function that both explicit and lazy setup call.
    It is protected by the _PROVIDE_SETUP_LOCK in its callers.
    """
    # This function assumes the lock is already held.
    structlog.reset_defaults()

    # Reset OTLP provider to ensure new LoggerProvider with updated config
    # This is critical when service_name changes, as OpenTelemetry's Resource is immutable
    try:
        from provide.foundation.logger.processors.otlp import reset_otlp_provider

        reset_otlp_provider()
    except ImportError:
        # OTLP not available (missing opentelemetry packages), skip reset
        pass

    # Use __dict__ access to avoid triggering proxy initialization
    foundation_logger.__dict__["_is_configured_by_setup"] = False
    foundation_logger.__dict__["_active_config"] = None
    _LAZY_SETUP_STATE.update({"done": False, "error": None, "in_progress": False})

    current_config = config if config is not None else TelemetryConfig.from_env()
    core_setup_logger = create_foundation_internal_logger(globally_disabled=current_config.globally_disabled)

    if not current_config.globally_disabled:
        core_setup_logger.debug(
            "🔧 Logger configuration initialized",
            service_name=current_config.service_name,
            log_level=current_config.logging.default_level,
            formatter=current_config.logging.console_formatter,
        )

        # Log OpenTelemetry/OTLP configuration
        if current_config.otlp_endpoint:
            try:
                from provide.foundation.integrations.openobserve.config import OpenObserveConfig

                oo_config = OpenObserveConfig.from_env()
                if oo_config.is_configured():
                    # OpenObserve auto-configured OTLP
                    core_setup_logger.debug(
                        "📡 OpenObserve integration enabled - OTLP log export active",
                        otlp_endpoint=current_config.otlp_endpoint,
                        openobserve_org=oo_config.org,
                        openobserve_stream=oo_config.stream,
                    )
                else:
                    # Manually configured OTLP
                    core_setup_logger.debug(
                        "📡 OpenTelemetry OTLP log export active",
                        otlp_endpoint=current_config.otlp_endpoint,
                        otlp_traces_endpoint=current_config.otlp_traces_endpoint,
                    )
            except ImportError:
                # OpenObserve not available, just log basic OTLP config
                core_setup_logger.debug(
                    "📡 OpenTelemetry OTLP log export active",
                    otlp_endpoint=current_config.otlp_endpoint,
                    otlp_traces_endpoint=current_config.otlp_traces_endpoint,
                )

    if current_config.globally_disabled:
        core_setup_logger.trace("Setting up globally disabled telemetry")
        handle_globally_disabled_setup()
    else:
        # Configure log file if specified
        if current_config.logging.log_file is not None:
            from provide.foundation.streams.file import configure_file_logging

            configure_file_logging(log_file_path=str(current_config.logging.log_file))

        core_setup_logger.trace("Configuring structlog output processors")
        configure_structlog_output(current_config, get_log_stream())

    # Use __dict__ access to avoid triggering proxy initialization
    foundation_logger.__dict__["_is_configured_by_setup"] = is_explicit_call
    foundation_logger.__dict__["_active_config"] = current_config
    _LAZY_SETUP_STATE["done"] = True

    # Configure Python stdlib logging for module-level suppression
    if not current_config.globally_disabled and current_config.logging.module_levels:
        _configure_stdlib_module_logging(current_config.logging.module_levels)

    if not current_config.globally_disabled:
        core_setup_logger.debug(
            "✅ Logger setup complete",
            processors_configured=True,
            log_file_enabled=current_config.logging.log_file is not None,
        )