Skip to content

Commit 97077f1

Browse files
alexmojakidmontagu
andauthored
Add min_level argument to logfire.configure. (#1265)
Co-authored-by: David Montague <[email protected]>
1 parent 3395136 commit 97077f1

File tree

10 files changed

+152
-24
lines changed

10 files changed

+152
-24
lines changed

docs/guides/onboarding-checklist/add-manual-tracing.md

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,14 +250,6 @@ The following methods exist for creating logs with different levels:
250250
- `logfire.error`
251251
- `logfire.fatal`
252252

253-
You can set the minimum level used for console logging (`info` by default) with [`logfire.configure`][logfire.configure], e.g:
254-
255-
```python
256-
import logfire
257-
258-
logfire.configure(console=logfire.ConsoleOptions(min_log_level='debug'))
259-
```
260-
261253
To log a message with a variable level you can use `logfire.log`, e.g. `logfire.log('info', 'This is an info log')` is equivalent to `logfire.info('This is an info log')`.
262254

263255
Spans are level `info` by default. You can change this with the `_level` argument, e.g. `with logfire.span('This is a debug span', _level='debug'):`. You can also change the level after the span has started but before it's finished with [`span.set_level`][logfire.LogfireSpan.set_level], e.g:
@@ -270,3 +262,44 @@ with logfire.span('Doing a thing') as span:
270262
```
271263

272264
If a span finishes with an unhandled exception, then in addition to recording a traceback as described above, the span's log level will be set to `error`. This will not happen when using the [`span.record_exception`][logfire.LogfireSpan.record_exception] method.
265+
266+
To skip creating logs/spans below a certain level, use the [`min_level`][logfire.configure(min_level)] argument to
267+
`logfire.configure`, e.g:
268+
269+
```python
270+
import logfire
271+
272+
logfire.configure(min_level='info')
273+
```
274+
275+
For spans, this only applies when `_level` is explicitly specified in `logfire.span`.
276+
Setting the level after will be ignored by this.
277+
If a span is not created because of the minimum level, this has no effect on parents or children.
278+
For example, this code:
279+
280+
```python
281+
import logfire
282+
283+
logfire.configure(min_level='info')
284+
285+
with logfire.span('root') as root:
286+
root.set_level('debug') # (1)!
287+
with logfire.span('debug span excluded', _level='debug'): # (2)!
288+
logfire.info('info message')
289+
```
290+
291+
1. This span has already been created with the default level of `info`, so `min_level` won't affect it.
292+
It's also logged to the console because that happens at creation time.
293+
But it will show in the Live view as having level `debug`.
294+
2. This span is not created because its level is below the minimum.
295+
That makes this line a complete no-op.
296+
297+
creates, logs, and sends the `root` span and the `info message` log, with the log being a direct child of the span.
298+
299+
To set the minimum level for console logging only (`info` by default):
300+
301+
```python
302+
import logfire
303+
304+
logfire.configure(console=logfire.ConsoleOptions(min_log_level='debug'))
305+
```

logfire/_internal/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from .client import InvalidProjectName, LogfireClient, ProjectAlreadyExists
7070
from .config_params import ParamManager, PydanticPluginRecordValues
7171
from .constants import (
72+
LEVEL_NUMBERS,
7273
RESOURCE_ATTRIBUTES_CODE_ROOT_PATH,
7374
RESOURCE_ATTRIBUTES_CODE_WORK_DIR,
7475
RESOURCE_ATTRIBUTES_DEPLOYMENT_ENVIRONMENT_NAME,
@@ -268,6 +269,7 @@ def configure( # noqa: D417
268269
scrubbing: ScrubbingOptions | Literal[False] | None = None,
269270
inspect_arguments: bool | None = None,
270271
sampling: SamplingOptions | None = None,
272+
min_level: int | LevelName | None = None,
271273
add_baggage_to_attributes: bool = True,
272274
code_source: CodeSource | None = None,
273275
distributed_tracing: bool | None = None,
@@ -320,6 +322,14 @@ def configure( # noqa: D417
320322
321323
Defaults to `True` if and only if the Python version is at least 3.11.
322324
325+
min_level:
326+
Minimum log level for logs and spans to be created. By default, all logs and spans are created.
327+
For example, set to 'info' to only create logs with level 'info' or higher, thus filtering out debug logs.
328+
For spans, this only applies when `_level` is explicitly specified in `logfire.span`.
329+
Changing the level of a span _after_ it is created will be ignored by this.
330+
If a span is not created, this has no effect on the current active span, or on logs/spans created inside the
331+
filtered `logfire.span` context manager.
332+
If set to `None`, uses the `LOGFIRE_MIN_LEVEL` environment variable; if that is not set, there is no minimum level.
323333
sampling: Sampling options. See the [sampling guide](https://logfire.pydantic.dev/docs/guides/advanced/sampling/).
324334
add_baggage_to_attributes: Set to `False` to prevent OpenTelemetry Baggage from being added to spans as attributes.
325335
See the [Baggage documentation](https://logfire.pydantic.dev/docs/reference/advanced/baggage/) for more details.
@@ -456,6 +466,7 @@ def configure( # noqa: D417
456466
additional_span_processors=additional_span_processors,
457467
scrubbing=scrubbing,
458468
inspect_arguments=inspect_arguments,
469+
min_level=min_level,
459470
sampling=sampling,
460471
add_baggage_to_attributes=add_baggage_to_attributes,
461472
code_source=code_source,
@@ -514,6 +525,9 @@ class _LogfireConfigData:
514525
sampling: SamplingOptions
515526
"""Sampling options."""
516527

528+
min_level: int
529+
"""Minimum log level for logs and spans to be created."""
530+
517531
add_baggage_to_attributes: bool
518532
"""Whether to add OpenTelemetry Baggage to span attributes."""
519533

@@ -544,6 +558,7 @@ def _load_configuration(
544558
scrubbing: ScrubbingOptions | Literal[False] | None,
545559
inspect_arguments: bool | None,
546560
sampling: SamplingOptions | None,
561+
min_level: int | LevelName | None,
547562
add_baggage_to_attributes: bool,
548563
code_source: CodeSource | None,
549564
distributed_tracing: bool | None,
@@ -561,6 +576,12 @@ def _load_configuration(
561576
self.inspect_arguments = param_manager.load_param('inspect_arguments', inspect_arguments)
562577
self.distributed_tracing = param_manager.load_param('distributed_tracing', distributed_tracing)
563578
self.ignore_no_config = param_manager.load_param('ignore_no_config')
579+
min_level = param_manager.load_param('min_level', min_level)
580+
if min_level is None:
581+
min_level = 0
582+
elif isinstance(min_level, str):
583+
min_level = LEVEL_NUMBERS[min_level]
584+
self.min_level = min_level
564585
self.add_baggage_to_attributes = add_baggage_to_attributes
565586

566587
# We save `scrubbing` just so that it can be serialized and deserialized.
@@ -647,6 +668,7 @@ def __init__(
647668
scrubbing: ScrubbingOptions | Literal[False] | None = None,
648669
inspect_arguments: bool | None = None,
649670
sampling: SamplingOptions | None = None,
671+
min_level: int | LevelName | None = None,
650672
add_baggage_to_attributes: bool = True,
651673
code_source: CodeSource | None = None,
652674
distributed_tracing: bool | None = None,
@@ -674,6 +696,7 @@ def __init__(
674696
scrubbing=scrubbing,
675697
inspect_arguments=inspect_arguments,
676698
sampling=sampling,
699+
min_level=min_level,
677700
add_baggage_to_attributes=add_baggage_to_attributes,
678701
code_source=code_source,
679702
distributed_tracing=distributed_tracing,
@@ -712,6 +735,7 @@ def configure(
712735
scrubbing: ScrubbingOptions | Literal[False] | None,
713736
inspect_arguments: bool | None,
714737
sampling: SamplingOptions | None,
738+
min_level: int | LevelName | None,
715739
add_baggage_to_attributes: bool,
716740
code_source: CodeSource | None,
717741
distributed_tracing: bool | None,
@@ -733,6 +757,7 @@ def configure(
733757
scrubbing,
734758
inspect_arguments,
735759
sampling,
760+
min_level,
736761
add_baggage_to_attributes,
737762
code_source,
738763
distributed_tracing,
@@ -1053,6 +1078,7 @@ def fix_pid(): # pragma: no cover
10531078
self._logger_provider.shutdown()
10541079

10551080
self._logger_provider.set_provider(logger_provider)
1081+
self._logger_provider.set_min_level(self.min_level)
10561082

10571083
if self is GLOBAL_CONFIG and not self._has_set_providers:
10581084
self._has_set_providers = True

logfire/_internal/config_params.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ class _DefaultCallback:
5555
# fmt: off
5656
SEND_TO_LOGFIRE = ConfigParam(env_vars=['LOGFIRE_SEND_TO_LOGFIRE'], allow_file_config=True, default=_send_to_logfire_default, tp=Union[bool, Literal['if-token-present']])
5757
"""Whether to send spans to Logfire."""
58+
MIN_LEVEL = ConfigParam(env_vars=['LOGFIRE_MIN_LEVEL'], allow_file_config=True, default=None, tp=LevelName)
59+
"""Minimum log level for logs and spans to be created. By default, all logs and spans are created."""
5860
TOKEN = ConfigParam(env_vars=['LOGFIRE_TOKEN'])
5961
"""Token for the Logfire API."""
6062
SERVICE_NAME = ConfigParam(env_vars=['LOGFIRE_SERVICE_NAME', OTEL_SERVICE_NAME], allow_file_config=True, default='')
@@ -107,6 +109,7 @@ class _DefaultCallback:
107109
CONFIG_PARAMS = {
108110
'base_url': BASE_URL,
109111
'send_to_logfire': SEND_TO_LOGFIRE,
112+
'min_level': MIN_LEVEL,
110113
'token': TOKEN,
111114
'service_name': SERVICE_NAME,
112115
'service_version': SERVICE_VERSION,

logfire/_internal/constants.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from typing import Literal
55

66
from opentelemetry.context import create_key
7-
from opentelemetry.util import types as otel_types
87

98
LOGFIRE_ATTRIBUTES_NAMESPACE = 'logfire'
109
"""Namespace within OTEL attributes used by logfire."""
@@ -90,7 +89,7 @@
9089

9190

9291
# This is in this file to encourage using it instead of setting these attributes manually.
93-
def log_level_attributes(level: LevelName | int) -> dict[str, otel_types.AttributeValue]:
92+
def log_level_attributes(level: LevelName | int) -> dict[str, int]:
9493
if isinstance(level, str):
9594
if level not in LEVEL_NUMBERS:
9695
warnings.warn(f'Invalid log level name: {level!r}')

logfire/_internal/integrations/fastapi.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,6 @@ async def run_endpoint_function(
294294
# both `http.method` and `http.route`.
295295
**{'http.route': request.scope['route'].path},
296296
**stack_info,
297-
_level='debug',
298297
)
299298
else:
300299
extra_span = NoopSpan()

logfire/_internal/logs.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from weakref import WeakSet
99

1010
from opentelemetry import trace
11-
from opentelemetry._logs import Logger, LoggerProvider, LogRecord, NoOpLoggerProvider
11+
from opentelemetry._logs import Logger, LoggerProvider, LogRecord, NoOpLoggerProvider, SeverityNumber
12+
13+
from logfire._internal.constants import LEVEL_NUMBERS
1214

1315
if TYPE_CHECKING:
1416
from opentelemetry.util.types import _ExtendedAttributes # type: ignore
@@ -23,6 +25,7 @@ class ProxyLoggerProvider(LoggerProvider):
2325
loggers: WeakSet[ProxyLogger] = dataclasses.field(default_factory=WeakSet) # type: ignore[reportUnknownVariableType]
2426
lock: Lock = dataclasses.field(default_factory=Lock)
2527
suppressed_scopes: set[str] = dataclasses.field(default_factory=set) # type: ignore[reportUnknownVariableType]
28+
min_level: int = 0
2629

2730
def get_logger(
2831
self,
@@ -37,10 +40,16 @@ def get_logger(
3740
else:
3841
provider = self.provider
3942
inner_logger = provider.get_logger(name, version, schema_url, attributes)
40-
logger = ProxyLogger(inner_logger, name, version, schema_url, attributes)
43+
logger = ProxyLogger(inner_logger, self.min_level, name, version, schema_url, attributes)
4144
self.loggers.add(logger)
4245
return logger
4346

47+
def set_min_level(self, min_level: int) -> None:
48+
with self.lock:
49+
self.min_level = min_level
50+
for logger in self.loggers:
51+
logger.min_level = min_level
52+
4453
def suppress_scopes(self, *scopes: str) -> None:
4554
with self.lock:
4655
self.suppressed_scopes.update(scopes)
@@ -78,12 +87,23 @@ def wrapper(*args: Any, **kwargs: Any):
7887
@dataclass(eq=False)
7988
class ProxyLogger(Logger):
8089
logger: Logger
90+
min_level: int
8191
name: str
8292
version: str | None = None
8393
schema_url: str | None = None
8494
attributes: _ExtendedAttributes | None = None
8595

8696
def emit(self, record: LogRecord) -> None:
97+
if record.severity_number is not None:
98+
if record.severity_number.value < self.min_level:
99+
return
100+
elif record.severity_text and (level_name := record.severity_text.lower()) in LEVEL_NUMBERS:
101+
level_number = LEVEL_NUMBERS[level_name]
102+
if level_number < self.min_level:
103+
return
104+
# record.severity_number is known to be None here, so we can safely set it
105+
record.severity_number = SeverityNumber(level_number)
106+
87107
if not record.trace_id:
88108
span_context = trace.get_current_span().get_span_context()
89109
record.trace_id = span_context.trace_id

logfire/_internal/main.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ def _span(
179179
_links: Sequence[tuple[SpanContext, otel_types.Attributes]] = (),
180180
) -> LogfireSpan:
181181
try:
182+
if _level is not None:
183+
level_attributes = log_level_attributes(_level)
184+
level_num = level_attributes[ATTRIBUTES_LOG_LEVEL_NUM_KEY]
185+
if level_num < self.config.min_level:
186+
return NoopSpan() # type: ignore
187+
else:
188+
level_attributes = None
189+
182190
stack_info = get_user_stack_info()
183191
merged_attributes = {**stack_info, **attributes}
184192

@@ -215,8 +223,8 @@ def _span(
215223
if sample_rate is not None and sample_rate != 1: # pragma: no cover
216224
otlp_attributes[ATTRIBUTES_SAMPLE_RATE_KEY] = sample_rate
217225

218-
if _level is not None:
219-
otlp_attributes.update(log_level_attributes(_level))
226+
if level_attributes is not None:
227+
otlp_attributes.update(level_attributes)
220228

221229
return LogfireSpan(
222230
_span_name or msg_template,
@@ -673,6 +681,11 @@ def log(
673681
console_log: Whether to log to the console, defaults to `True`.
674682
"""
675683
with handle_internal_errors:
684+
level_attributes = log_level_attributes(level)
685+
level_num = level_attributes[ATTRIBUTES_LOG_LEVEL_NUM_KEY]
686+
if level_num < self.config.min_level:
687+
return
688+
676689
stack_info = get_user_stack_info()
677690

678691
attributes = attributes or {}
@@ -706,7 +719,7 @@ def log(
706719
otlp_attributes = prepare_otlp_attributes(merged_attributes)
707720
otlp_attributes = {
708721
ATTRIBUTES_SPAN_TYPE_KEY: 'log',
709-
**log_level_attributes(level),
722+
**level_attributes,
710723
ATTRIBUTES_MESSAGE_TEMPLATE_KEY: msg_template,
711724
ATTRIBUTES_MESSAGE_KEY: msg,
712725
**otlp_attributes,

tests/otel_integrations/test_fastapi.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,6 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None:
450450
'logfire.span_type': 'pending_span',
451451
'logfire.json_schema': '{"type":"object","properties":{"method":{},"http.route":{}}}',
452452
'logfire.msg': 'GET /with_path_param/{param} (with_path_param)',
453-
'logfire.level_num': 5,
454453
'logfire.pending_parent_id': '0000000000000001',
455454
},
456455
},
@@ -470,7 +469,6 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None:
470469
'logfire.span_type': 'span',
471470
'logfire.json_schema': '{"type":"object","properties":{"method":{},"http.route":{}}}',
472471
'logfire.msg': 'GET /with_path_param/{param} (with_path_param)',
473-
'logfire.level_num': 5,
474472
},
475473
},
476474
{
@@ -687,7 +685,6 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) ->
687685
'logfire.json_schema': {'type': 'object', 'properties': {'method': {}, 'http.route': {}}},
688686
'logfire.span_type': 'pending_span',
689687
'logfire.msg': 'GET / (homepage)',
690-
'logfire.level_num': 5,
691688
'logfire.pending_parent_id': '0000000000000003',
692689
},
693690
},
@@ -723,7 +720,6 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) ->
723720
'logfire.span_type': 'span',
724721
'logfire.json_schema': {'type': 'object', 'properties': {'method': {}, 'http.route': {}}},
725722
'logfire.msg': 'GET / (homepage)',
726-
'logfire.level_num': 5,
727723
},
728724
},
729725
{
@@ -1086,7 +1082,6 @@ def test_get_fastapi_arguments(client: TestClient, exporter: TestExporter) -> No
10861082
'logfire.msg_template': '{method} {http.route} ({code.function})',
10871083
'logfire.msg': 'GET /other (other_route)',
10881084
'logfire.json_schema': '{"type":"object","properties":{"method":{},"http.route":{}}}',
1089-
'logfire.level_num': 5,
10901085
'logfire.span_type': 'span',
10911086
},
10921087
},
@@ -1222,7 +1217,6 @@ def test_first_lvl_subapp_fastapi_arguments(client: TestClient, exporter: TestEx
12221217
'logfire.msg_template': '{method} {http.route} ({code.function})',
12231218
'logfire.msg': 'GET /other (other_route)',
12241219
'logfire.json_schema': '{"type":"object","properties":{"method":{},"http.route":{}}}',
1225-
'logfire.level_num': 5,
12261220
'logfire.span_type': 'span',
12271221
},
12281222
},
@@ -1358,7 +1352,6 @@ def test_second_lvl_subapp_fastapi_arguments(client: TestClient, exporter: TestE
13581352
'logfire.msg_template': '{method} {http.route} ({code.function})',
13591353
'logfire.msg': 'GET /other (other_route)',
13601354
'logfire.json_schema': '{"type":"object","properties":{"method":{},"http.route":{}}}',
1361-
'logfire.level_num': 5,
13621355
'logfire.span_type': 'span',
13631356
},
13641357
},
@@ -1785,7 +1778,6 @@ def test_scrubbing(client: TestClient, exporter: TestExporter) -> None:
17851778
'logfire.msg_template': '{method} {http.route} ({code.function})',
17861779
'logfire.msg': 'GET /secret/{path_param} (get_secret)',
17871780
'logfire.json_schema': '{"type":"object","properties":{"method":{},"http.route":{}}}',
1788-
'logfire.level_num': 5,
17891781
'logfire.span_type': 'span',
17901782
},
17911783
},

0 commit comments

Comments
 (0)