Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions logfire/_internal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@

from logfire._internal.auth import PYDANTIC_LOGFIRE_TOKEN_PATTERN, REGIONS
from logfire._internal.baggage import DirectBaggageAttributesSpanProcessor
from logfire._internal.collect_system_info import collect_package_info
from logfire.exceptions import LogfireConfigError
from logfire.sampling import SamplingOptions
from logfire.sampling._tail_sampling import TailSamplingProcessor
Expand Down Expand Up @@ -395,6 +396,7 @@ def configure(
code_source: CodeSource | None = None,
variables: VariablesOptions | LocalVariablesOptions | None = None,
distributed_tracing: bool | None = None,
emit_configuration_span: bool | None = None,
advanced: AdvancedOptions | None = None,
**deprecated_kwargs: Unpack[DeprecatedKwargs],
) -> Logfire:
Expand Down Expand Up @@ -470,6 +472,10 @@ def configure(
See [Unintentional Distributed Tracing](https://logfire.pydantic.dev/docs/how-to-guides/distributed-tracing/#unintentional-distributed-tracing)
for more information.
This setting always applies globally, and the last value set is used, including the default value.
emit_configuration_span: If `True`, emit a `Logfire configured` log span containing the SDK version,
installed package versions, and a curated set of non-sensitive configuration flags. Useful for
diagnosing setup issues from the Logfire UI. Defaults to the `LOGFIRE_EMIT_CONFIGURATION_SPAN`
environment variable, or `False`.
advanced: Advanced options primarily used for testing by Logfire developers.
"""
from .. import DEFAULT_LOGFIRE_INSTANCE, Logfire
Expand Down Expand Up @@ -603,6 +609,7 @@ def configure(
code_source=code_source,
variables=variables,
distributed_tracing=distributed_tracing,
emit_configuration_span=emit_configuration_span,
advanced=advanced,
)

Expand Down Expand Up @@ -684,6 +691,9 @@ class _LogfireConfigData:
distributed_tracing: bool | None
"""Whether to extract incoming trace context."""

emit_configuration_span: bool
"""Whether to emit a `Logfire configured` log span after `logfire.configure()`."""

advanced: AdvancedOptions
"""Advanced options primarily used for testing by Logfire developers."""

Expand Down Expand Up @@ -711,6 +721,7 @@ def _load_configuration(
code_source: CodeSource | None,
variables: VariablesOptions | LocalVariablesOptions | None,
distributed_tracing: bool | None,
emit_configuration_span: bool | None,
advanced: AdvancedOptions | None,
) -> None:
"""Merge the given parameters with the environment variables file configurations."""
Expand All @@ -726,6 +737,7 @@ def _load_configuration(
self.data_dir = param_manager.load_param('data_dir', data_dir)
self.inspect_arguments = param_manager.load_param('inspect_arguments', inspect_arguments)
self.distributed_tracing = param_manager.load_param('distributed_tracing', distributed_tracing)
self.emit_configuration_span = param_manager.load_param('emit_configuration_span', emit_configuration_span)
self.ignore_no_config = param_manager.load_param('ignore_no_config')
min_level = param_manager.load_param('min_level', min_level)
if min_level is None:
Expand Down Expand Up @@ -844,6 +856,7 @@ def __init__(
variables: VariablesOptions | None = None,
code_source: CodeSource | None = None,
distributed_tracing: bool | None = None,
emit_configuration_span: bool | None = None,
advanced: AdvancedOptions | None = None,
) -> None:
"""Create a new LogfireConfig.
Expand Down Expand Up @@ -874,6 +887,7 @@ def __init__(
code_source=code_source,
variables=variables,
distributed_tracing=distributed_tracing,
emit_configuration_span=emit_configuration_span,
advanced=advanced,
)
# initialize with no-ops so that we don't impact OTEL's global config just because logfire is installed
Expand Down Expand Up @@ -910,6 +924,7 @@ def configure(
code_source: CodeSource | None,
variables: VariablesOptions | LocalVariablesOptions | None,
distributed_tracing: bool | None,
emit_configuration_span: bool | None,
advanced: AdvancedOptions | None,
) -> None:
with self._lock:
Expand All @@ -934,6 +949,7 @@ def configure(
code_source,
variables,
distributed_tracing,
emit_configuration_span,
advanced,
)
self.initialize()
Expand Down Expand Up @@ -1352,6 +1368,42 @@ def fix_pid(): # pragma: no cover

self._ensure_flush_after_aws_lambda()

self._emit_configuration_span()

def _emit_configuration_span(self) -> None:
"""Emit a span describing the active Logfire configuration and installed packages.

Only runs when `emit_configuration_span` is `True`. Sends a curated set of
non-sensitive configuration fields - never the token, api_key, or opaque
objects like span/log processors.
"""
if not self.emit_configuration_span:
return

from logfire._internal.main import Logfire

with handle_internal_errors:
sampling = self.sampling
config_dict: dict[str, Any] = {
'send_to_logfire': self.send_to_logfire,
'console_enabled': bool(self.console),
'scrubbing_enabled': bool(self.scrubbing),
'inspect_arguments': self.inspect_arguments,
'min_level': self.min_level,
'add_baggage_to_attributes': self.add_baggage_to_attributes,
'distributed_tracing': self.distributed_tracing,
'head_sample_rate': sampling.head if isinstance(sampling.head, (int, float)) else None,
'tail_sampling_enabled': sampling.tail is not None,
'code_source_set': self.code_source is not None,
'variables_set': self.variables is not None,
}
Logfire(config=self).info(
'Logfire configured',
logfire_version=VERSION,
logfire_config=config_dict,
package_versions=collect_package_info(),
)

def force_flush(self, timeout_millis: int = 30_000) -> bool:
"""Force flush all spans and metrics.

Expand Down
3 changes: 3 additions & 0 deletions logfire/_internal/config_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ class _DefaultCallback:
"""The base URL of the Logfire backend. Primarily for testing purposes."""
DISTRIBUTED_TRACING = ConfigParam(env_vars=['LOGFIRE_DISTRIBUTED_TRACING'], allow_file_config=True, default=None, tp=bool)
"""Whether to extract incoming trace context. By default, will extract but warn about it."""
EMIT_CONFIGURATION_SPAN = ConfigParam(env_vars=['LOGFIRE_EMIT_CONFIGURATION_SPAN'], allow_file_config=True, default=False, tp=bool)
"""Whether to emit a `Logfire configured` log span after `logfire.configure()`."""

# Instrumentation packages parameters
HTTPX_CAPTURE_ALL = ConfigParam(env_vars=['LOGFIRE_HTTPX_CAPTURE_ALL'], allow_file_config=True, default=False, tp=bool)
Expand Down Expand Up @@ -140,6 +142,7 @@ class _DefaultCallback:
'inspect_arguments': INSPECT_ARGUMENTS,
'ignore_no_config': IGNORE_NO_CONFIG,
'distributed_tracing': DISTRIBUTED_TRACING,
'emit_configuration_span': EMIT_CONFIGURATION_SPAN,
# Instrumentation packages parameters
'httpx_capture_all': HTTPX_CAPTURE_ALL,
'aiohttp_client_capture_all': AIOHTTP_CLIENT_CAPTURE_ALL,
Expand Down
33 changes: 33 additions & 0 deletions tests/test_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,39 @@ def test_configure_twice_no_warning(caplog: LogCaptureFixture):
assert not caplog.messages


def test_configuration_span_not_emitted_by_default(config_kwargs: dict[str, Any], exporter: TestExporter):
configure(**config_kwargs)
assert not [s for s in exporter.exported_spans_as_dict() if s['name'] == 'Logfire configured']


def test_configuration_span_emitted_when_opted_in(config_kwargs: dict[str, Any], exporter: TestExporter):
configure(**config_kwargs, emit_configuration_span=True)

spans = [
s for s in exporter.exported_spans_as_dict(parse_json_attributes=True) if s['name'] == 'Logfire configured'
]
assert len(spans) == 1
attrs = spans[0]['attributes']
assert attrs['logfire_version']
assert isinstance(attrs['package_versions'], dict)
assert 'logfire' in attrs['package_versions']
cfg = attrs['logfire_config']
assert isinstance(cfg, dict)
assert cfg['send_to_logfire'] is False
assert cfg['inspect_arguments'] is True
# No identity/secrets in the payload.
for forbidden in ('token', 'api_key', 'service_name', 'service_version', 'environment', 'base_url'):
assert forbidden not in cfg


def test_configuration_span_enabled_via_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
from logfire._internal.config import GLOBAL_CONFIG

monkeypatch.setenv('LOGFIRE_EMIT_CONFIGURATION_SPAN', '1')
configure(send_to_logfire=False, console=False, inspect_arguments=False)
assert GLOBAL_CONFIG.emit_configuration_span is True


def test_exit_open_spans_exports_suspended_generator_span_before_shutdown() -> None:
script_path = Path(__file__).parent / 'import_used_for_tests' / 'open_span_at_shutdown.py'

Expand Down
Loading