Skip to content

Index

๐Ÿค– AI-Generated Content

This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.

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,
        )