Skip to content

Add _level param to @logfire.instrument()#1871

Open
imp-joshi wants to merge 6 commits intopydantic:mainfrom
imp-joshi:feat/1452-instrument-decorator-log-level
Open

Add _level param to @logfire.instrument()#1871
imp-joshi wants to merge 6 commits intopydantic:mainfrom
imp-joshi:feat/1452-instrument-decorator-log-level

Conversation

@imp-joshi
Copy link
Copy Markdown

@imp-joshi imp-joshi commented Apr 18, 2026

Closes #1452.

Every span API in logfire (logfire.span(), logfire.debug(), logfire.warn(), etc.) accepts a _level parameter that tags the span with a log level and respects min_level filtering. @logfire.instrument() was the only one that didn't. This PR closes that gap.

What changed

You can now do:

@logfire.instrument("my span", _level="warn")
def my_func(): ...

and it behaves exactly like a manual with logfire.span("my span", _level="warn"): block. The span gets the warn-level attributes, and if your min_level is above warn, the span is suppressed entirely.

The _level=None default is completely untouched, no overhead on existing code.

Implementation note

log_level_attributes(_level) is called once at decoration time and merged into the attribute dict. At call time the only extra work is level_num < config.min_level before deciding whether to proceed or return a NoopSpan.

NoopSpan._span (pre-existing bug, fixed here)

When open_span returns a NoopSpan (now an intentional code path, not just an error case), record_return=True would crash trying to call .is_recording() on a lambda returned by __getattr__. Added _span as an explicit property returning trace_api.INVALID_SPAN so set_user_attributes_on_raw_span exits cleanly.

Tests

  • test_instrument_with_level - warn level, verifies logfire.level_num: 13 on the span
  • test_instrument_level_filtered - debug with min_level="info", nothing exported
  • test_instrument_level_filtered_record_return - same but with record_return=True (exercises the NoopSpan fix)
  • test_instrument_with_level_async - async path
  • test_instrument_with_level_and_extract_args - level attrs and function args coexist
  • test_instrument_level_filtered_extract_args - filtering with extract_args=True
  • test_instrument_level_filtered_extract_args_iterable - filtering with extract_args=('x',)

- adds _level: LevelName | int | None = None to Logfire.instrument()
- level attrs merged into span attributes at decoration time (static)
- _level=None leaves the existing fast path untouched
- test: warn-level span carries logfire.level_num=13
- extracts level_num at decoration time for O(1) call-time check
- guard (level_num < config.min_level) added to all three open_span variants
- NoopSpan._span property returns trace_api.INVALID_SPAN
- set_user_attributes_on_raw_span exits cleanly via is_recording(); fixes latent crash on record_return=True
- async path works via same open_span closure, verified with snapshot (logfire.level_num: 13)
- extract_args + _level test verifies both attrs appear together
- shim gets explicit _level=None kwarg
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

if _level is not None:
_level_attrs = log_level_attributes(_level)
level_num = int(_level_attrs[ATTRIBUTES_LOG_LEVEL_NUM_KEY])
attributes = {**attributes, **_level_attrs}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Shared mutable attributes dict in _instrument_span_with_args is a pre-existing concern

The attributes dict created in get_open_span is captured by the open_span closures and passed to _instrument_span_with_args (logfire/_internal/main.py:282-299), which mutates it in-place (adding logfire.msg, logfire.json_schema, and function arg keys). This means the dict accumulates state across calls. It currently works because each call overwrites the same keys, but it's fragile — e.g., if a function had **kwargs and different keyword arguments were passed on different calls, stale keys from previous calls would persist. This is pre-existing and not introduced by this PR; the PR's {**attributes, **_level_attrs} copy at logfire/_internal/instrument.py:140 doesn't change this dynamic since the new dict is still the one that gets mutated on subsequent calls.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 18, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +134 to 137
def instrument(self, *args, _level=None, **kwargs):
def decorator(func):
return func

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 *logfire-api shim uses args to accept positional func arg but silently replaces it with decorator

In the logfire-api no-op shim, instrument(self, *args, _level=None, **kwargs) always returns decorator (which takes a func and returns it). When used as @logfire.instrument (without parens), instrument(func) returns decorator rather than decorator(func), so my_func becomes decorator — a function that takes one arg and returns it, rather than the original function. This is a pre-existing issue in the logfire-api shim (not introduced by this PR), and is unlikely to matter in practice since the shim is only used when logfire is not installed.

(Refers to lines 134-138)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for log level specification in the instrument decorator

1 participant