Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
46 changes: 46 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 @@ -201,6 +202,15 @@ class AdvancedOptions:
serialized configuration sent to child processes. See the [distributed tracing guide](https://logfire.pydantic.dev/docs/how-to-guides/distributed-tracing/#thread-and-pool-executors) for more details.
"""

emit_configuration_span: bool = False
Comment thread
Kludex marked this conversation as resolved.
Outdated
"""Whether to emit a `Logfire configured` log span after `logfire.configure()`.

The span includes the SDK version, installed package versions, and a small set of
non-sensitive configuration flags (sampling rate, console enabled, etc.). Identity
and secrets (token, api_key, service name, environment, base URL) are never sent.
Useful for diagnosing setup issues from the Logfire UI. Off by default.
"""

def generate_base_url(self, token: str) -> str:
if self.base_url is not None:
return self.base_url
Expand Down Expand Up @@ -1352,6 +1362,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 `advanced.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.advanced.emit_configuration_span:
return

from logfire._internal.main import Logfire

with handle_internal_errors:
sampling = self.sampling
config_dict: dict[str, Any] = {
Comment thread
alexmojaki marked this conversation as resolved.
'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(
Comment thread
alexmojaki marked this conversation as resolved.
Outdated
'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
26 changes: 26 additions & 0 deletions tests/test_configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1597,6 +1597,32 @@ 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):
advanced = dataclasses.replace(config_kwargs['advanced'], emit_configuration_span=True)
configure(**{**config_kwargs, 'advanced': advanced})

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_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