Skip to content

Send spec-compliant User-Agent: logfire-python/<version>, retaining the OTLP exporter identifier on exports#2106

Open
dmontagu wants to merge 5 commits into
mainfrom
dm/otel-spec-user-agent
Open

Send spec-compliant User-Agent: logfire-python/<version>, retaining the OTLP exporter identifier on exports#2106
dmontagu wants to merge 5 commits into
mainfrom
dm/otel-spec-user-agent

Conversation

@dmontagu

@dmontagu dmontagu commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Aligns the SDK's User-Agent handling with the OpenTelemetry exporter specification and with the other Logfire SDKs, and unifies it across every HTTP surface:

Surface Before After
OTLP exports (traces/metrics/logs) logfire/<v> (upstream exporter identifier dropped) logfire-python/<v> OTel-OTLP-Exporter-Python/<exporter-v>
API client, token validation, variables logfire/<v> logfire-python/<v>
Experimental query client logfire-sdk-python/<v> (Python ..., os ..., arch ...) logfire-python/<v>
CLI session requests default (python-requests/<v>) logfire-python/<v>

Why

The OTel exporter spec's User Agent section recommends that when a distribution adds its identifier to the User-Agent, the exporter's default identifier is retained, typically by prepending:

"The resulting User-Agent SHOULD include the exporter's default User-Agent string. The intention is to support an identifier for an OpenTelemetry SDK/Agent distribution. Typically an exporter would prepend the given identifier to its own. For example: MyDistribution/x.y.z OTel-OTLP-Exporter-Python/1.2.3"

That guidance was added in open-telemetry/opentelemetry-specification#4560 (Sept 2025, motivated by #4479). Our current behavior predates it: the logfire/<version> header dates to #250 (Sept 2023) and, once reused for the OTLP exporters, silently replaced the upstream OTel-OTLP-Exporter-Python/<v> identifier because the Python exporter applies custom headers over its defaults. Since the Python exporter does not (yet) implement the spec's user-agent enrichment option, we rebuild its default identifier from the installed package version and prepend ours; a test compares against a freshly constructed exporter's actual default so we notice if the upstream format ever changes.

Retaining the exporter identifier also has direct debugging value: it tells the backend which upstream exporter version produced a request without having to ask the user.

Choices and their grounding

  • Product token logfire-python/<v>: matches the sibling SDKs (logfire-js/<v>, logfire-rust/<v>) and the spec's MyDistribution/x.y.z product-token form (RFC 9110).
  • No parenthetical platform details: the Python version, OS, and architecture added to the query client's UA in Add user agent to query client #1875 are removed, as proposed by their author in review of feat: add X-Logfire-Telemetry header #1905. On OTLP exports this information is already carried properly by resource attributes (process.runtime.*, os.*, host.*, see Pre-populate host.* and os.* resource attributes #2013), and a single low-cardinality product token keeps the UA usable for filtering and aggregating by SDK version.
  • One constant for every surface: previously three different formats existed (API/OTLP, query client, CLI). All requests now share UA_HEADER, with OTLP exports additionally retaining the upstream identifier via OTLP_EXPORTER_UA_HEADER.

This complements (and does not replace) the request-telemetry-header discussion in #1905; richer runtime/config signals belong there, not in the UA.

Compatibility notes

  • The UA is informational; no behavior depends on it. Anything matching the exact string logfire/<version> (e.g. proxy rules) should match the logfire-python/ prefix instead. Older SDK versions keep sending the old formats, so consumers should expect both during the transition.
  • User-supplied user-agent headers on the query client are still respected (setdefault).

Test plan

  • New tests/test_user_agent.py: product-token format; OTLP UA exactly equals <our token> <upstream exporter default> (drift guard against the installed exporter's real default); end-to-end logfire.configure() asserting the UA per endpoint (/v1/info vs /v1/traces) via requests_mock; API client, query client, and CLI request headers.
  • make format lint typecheck clean; tests/test_configure.py, tests/test_cli.py, tests/test_query_client.py, tests/exporters/test_otlp_session.py, tests/test_variables.py all pass (625 tests).

https://claude.ai/code/session_0159PYpdaBhM35pcPFiVcotC

Review in cubic

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Standardizes the Logfire HTTP User-Agent as logfire-python/<version>, defines an OTLP-specific value that preserves the upstream exporter identifier, and applies these constants across API, OTLP, query-client, LogfireClient, and CLI requests. New tests validate formatting, composition, request headers, client initialization, and CLI behavior.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: standardizing the User-Agent and preserving the OTLP exporter identifier.
Description check ✅ Passed The description is clearly related to the changeset and matches the SDK-wide User-Agent updates described in the diffs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dm/otel-spec-user-agent

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

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

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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

Comment thread logfire/_internal/config.py


_ACCEPT = Literal['application/json', 'application/vnd.apache.arrow.stream', 'text/csv']
_USER_AGENT = f'logfire-sdk-python/{VERSION} (Python {platform.python_version()}, os {platform.platform()}, arch {platform.machine()})'

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.

It's not super clear what the harm of keeping the other info in the UA is. I guess it makes parsing and querying harder? But if we're going to remove info then it seems important to merge some version of #1905 first so that we don't lose it entirely.

@Viicos Viicos Jul 23, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd say it's fine to leave the telemetry in this header, and we rename logfire-sdk-python/... to logfire-python/.... I initially had concerns with parsing/querying because I have a dashboard where I keep track of SDK queries by UA, but if we properly follow the RFC 9110 grammar and have the correct UDF to do to the parsing then it should be fine.

cc @jirikuncar

@dmontagu dmontagu Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I just want to follow the otel spec here and not rely on this too much for extra info. I feel like we don't depend on the python/os/arch stuff so much that we can't afford a gap in awareness of this.

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.

Are we currently using the info in this UA? Is there pressure to change this UA soon?

headers = Headers(client_kwargs.pop('headers', None))
headers['authorization'] = read_token
headers.setdefault('user-agent', _USER_AGENT)
headers.setdefault('user-agent', UA_HEADER)

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.

why do we even support custom headers? and should our UA be appended to a custom UA?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Unfortunately removing the option to set it would be breaking in some way, but I agree it's questionable. I'd also add the httpx one as a component after ours

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed with Victorien that removing custom-header support would be breaking, so I've kept it, including the existing semantics where a caller-supplied UA fully replaces ours (it doubles as an opt-out). On appending python-httpx/<v> after ours: I'd rather not. The spec guidance we're following only asks that the exporter's identifier be retained; httpx is just the transport, and consistency would then also suggest python-requests/<v> on the API client and CLI, re-growing the UA right after this PR trimmed it to one low-cardinality token per surface. If we want richer client-runtime detail later, I think #1905's telemetry header is the right home for it.

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.

The spec is irrelevant here, this isn't the exporter.

httpx/requests does naturally seem like it fits in the UA. I think it fits there better than in the telemetry header. And it's not currently there anyway.

Comment thread logfire/_internal/exporters/otlp.py Outdated
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.

3 participants