Skip to content

Commit 6524d2f

Browse files
adriangbclaude
andcommitted
split out response-header handling into its own PR
Per review (#1905#discussion_r3189043209), the X-Logfire-Warning / X-Logfire-Error response-hook handling is logically independent from the X-Logfire-Telemetry request header — they just happened to be introduced together. Moved that code (plus its exceptions, install calls, and tests) to a separate PR (#1906) so each can be reviewed and landed on its own. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9f07209 commit 6524d2f

7 files changed

Lines changed: 14 additions & 157 deletions

File tree

logfire/_internal/cli/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ..client import LogfireClient
2626
from ..config import REGIONS, LogfireCredentials, get_base_url_from_token
2727
from ..config_params import ParamManager
28-
from ..telemetry_header import TELEMETRY_HEADER_NAME, build_telemetry_header, install_logfire_response_hook
28+
from ..telemetry_header import TELEMETRY_HEADER_NAME, build_telemetry_header
2929
from ..tracer import SDKTracerProvider
3030
from .auth import parse_auth, parse_logout
3131
from .prompt import parse_prompt
@@ -435,10 +435,9 @@ def log_trace_id(response: requests.Response, context: ContextCarrier, *args: An
435435
else:
436436
with tracer.start_as_current_span('logfire._internal.cli'), requests.Session() as session:
437437
context = get_context()
438-
session.hooks = {'response': [functools.partial(log_trace_id, context=context)]}
438+
session.hooks = {'response': functools.partial(log_trace_id, context=context)}
439439
session.headers.update(context)
440440
session.headers[TELEMETRY_HEADER_NAME] = build_telemetry_header()
441-
install_logfire_response_hook(session)
442441
namespace._session = session
443442
namespace.func(namespace)
444443

logfire/_internal/client.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@
1010
from logfire.version import VERSION
1111

1212
from .auth import UserToken, UserTokenCollection
13-
from .telemetry_header import (
14-
TELEMETRY_HEADER_NAME,
15-
build_telemetry_header,
16-
install_logfire_response_hook,
17-
)
13+
from .telemetry_header import TELEMETRY_HEADER_NAME, build_telemetry_header
1814
from .utils import UnexpectedResponse
1915

2016
UA_HEADER = f'logfire/{VERSION}'
@@ -49,7 +45,6 @@ def __init__(self, user_token: UserToken) -> None:
4945
TELEMETRY_HEADER_NAME: build_telemetry_header(),
5046
}
5147
)
52-
install_logfire_response_hook(self._session)
5348

5449
@classmethod
5550
def from_url(cls, base_url: str | None) -> Self:

logfire/_internal/config.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,7 @@
111111
from .metrics import ProxyMeterProvider
112112
from .scrubbing import NOOP_SCRUBBER, BaseScrubber, Scrubber, ScrubbingOptions
113113
from .stack_info import warn_at_user_stacklevel
114-
from .telemetry_header import (
115-
TELEMETRY_HEADER_NAME,
116-
build_telemetry_header,
117-
install_logfire_response_hook,
118-
)
114+
from .telemetry_header import TELEMETRY_HEADER_NAME, build_telemetry_header
119115
from .tracer import OPEN_SPANS, PendingSpanProcessor, ProxyTracerProvider
120116
from .utils import (
121117
SeededRandomIdGenerator,
@@ -1164,7 +1160,6 @@ def check_tokens():
11641160
TELEMETRY_HEADER_NAME: telemetry_header_value,
11651161
}
11661162
session = OTLPExporterHttpSession()
1167-
install_logfire_response_hook(session)
11681163
span_exporter = BodySizeCheckingOTLPSpanExporter(
11691164
endpoint=urljoin(base_url, '/v1/traces'),
11701165
session=session,
@@ -1491,11 +1486,9 @@ def warn_if_not_initialized(self, message: str):
14911486
)
14921487

14931488
def _initialize_credentials_from_token(self, token: str) -> LogfireCredentials | None:
1494-
session = requests.Session()
1495-
install_logfire_response_hook(session)
14961489
return LogfireCredentials.from_token(
14971490
token,
1498-
session,
1491+
requests.Session(),
14991492
self.advanced.generate_base_url(token),
15001493
telemetry_header=build_telemetry_header(self),
15011494
)

logfire/_internal/telemetry_header.py

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,26 @@
1-
"""SDK <-> server out-of-band metadata exchanged via custom HTTP headers.
2-
3-
* `X-Logfire-Telemetry` (request): non-sensitive information about the SDK and how
4-
it is configured, encoded as a compact JSON object. Used by the backend to
5-
answer questions like which SDK versions are still in active use, which Python
6-
versions we can drop, and which configuration options users actually enable.
7-
Secrets (`token`, `api_key`, `service_name`, etc.) are never included.
8-
* `X-Logfire-Warning` (response): an out-of-band warning the server wants the
9-
user to see. Surfaced via `warnings.warn(...)`; the standard "default" filter
10-
deduplicates identical messages so a chatty server only warns once.
11-
* `X-Logfire-Error` (response): an out-of-band error the server wants the SDK
12-
to raise. Always raised — callers that want to keep working past it (the OTLP
13-
pipeline, the variables provider) already swallow exceptions from their HTTP
14-
calls.
1+
"""Build the `X-Logfire-Telemetry` request header.
2+
3+
The header carries non-sensitive information about the SDK and how it is
4+
configured, encoded as a compact JSON object. The backend uses it to answer
5+
questions like which SDK versions are still in active use, which Python
6+
versions we can drop, and which configuration options users actually enable.
7+
Secrets (`token`, `api_key`, `service_name`, etc.) are never included.
158
"""
169

1710
from __future__ import annotations
1811

1912
import functools
2013
import json
2114
import sys
22-
import warnings
2315
from typing import TYPE_CHECKING, Any
2416

25-
import requests
26-
27-
from logfire.exceptions import LogfireServerError, LogfireServerWarning
2817
from logfire.version import VERSION
2918

3019
if TYPE_CHECKING:
3120
from .config import LogfireConfig
3221

3322

3423
TELEMETRY_HEADER_NAME = 'X-Logfire-Telemetry'
35-
WARNING_HEADER_NAME = 'X-Logfire-Warning'
36-
ERROR_HEADER_NAME = 'X-Logfire-Error'
3724

3825

3926
@functools.cache
@@ -102,28 +89,3 @@ def build_telemetry_header(config: LogfireConfig | None = None) -> str:
10289
if config is not None:
10390
pairs.update(_config_telemetry_pairs(config))
10491
return json.dumps(pairs, separators=(',', ':'))
105-
106-
107-
def process_logfire_response_headers(response: requests.Response, *_args: Any, **_kwargs: Any) -> requests.Response:
108-
"""Handle `X-Logfire-Warning` / `X-Logfire-Error` headers on a Logfire API response.
109-
110-
Designed to be installed as a `requests` response hook
111-
(`session.hooks['response'].append(...)`).
112-
"""
113-
warning_message = response.headers.get(WARNING_HEADER_NAME)
114-
if warning_message:
115-
warnings.warn(warning_message, LogfireServerWarning, stacklevel=2)
116-
error_message = response.headers.get(ERROR_HEADER_NAME)
117-
if error_message:
118-
raise LogfireServerError(error_message)
119-
return response
120-
121-
122-
def install_logfire_response_hook(session: requests.Session) -> None:
123-
"""Install `process_logfire_response_headers` as a response hook on `session`.
124-
125-
`requests.Session()` always initialises `hooks['response']` to a list, and every
126-
call site here passes a freshly-built session, so we just append.
127-
"""
128-
response_hooks: list[Any] = session.hooks.setdefault('response', [])
129-
response_hooks.append(process_logfire_response_headers)

logfire/exceptions.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,3 @@
33

44
class LogfireConfigError(ValueError):
55
"""Error raised when there is a problem with the Logfire configuration."""
6-
7-
8-
class LogfireServerError(Exception):
9-
"""Error raised when the Logfire server returns an `X-Logfire-Error` header on a response."""
10-
11-
12-
class LogfireServerWarning(UserWarning):
13-
"""Warning emitted when the Logfire server returns an `X-Logfire-Warning` header on a response."""

logfire/variables/remote.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@
1717

1818
from logfire._internal.client import UA_HEADER
1919
from logfire._internal.config import VariablesOptions
20-
from logfire._internal.telemetry_header import (
21-
TELEMETRY_HEADER_NAME,
22-
build_telemetry_header,
23-
install_logfire_response_hook,
24-
)
20+
from logfire._internal.telemetry_header import TELEMETRY_HEADER_NAME, build_telemetry_header
2521
from logfire._internal.utils import UnexpectedResponse
2622
from logfire.variables.abstract import (
2723
ResolvedVariable,
@@ -85,7 +81,6 @@ def __init__(self, base_url: str, token: str, options: VariablesOptions, telemet
8581
TELEMETRY_HEADER_NAME: self._telemetry_header,
8682
}
8783
)
88-
install_logfire_response_hook(self._session)
8984
self._timeout = options.timeout
9085
self._block_before_first_fetch = block_before_first_resolve
9186
self._polling_interval: timedelta = (
@@ -215,7 +210,6 @@ def _sse_listener(self): # pragma: no cover
215210
'Cache-Control': 'no-cache',
216211
}
217212
)
218-
install_logfire_response_hook(sse_session)
219213

220214
# Open streaming connection
221215
response = sse_session.get(sse_url, stream=True, timeout=(10, None))

tests/test_telemetry_header.py

Lines changed: 1 addition & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
11
from __future__ import annotations
22

33
import json
4-
import warnings
54
from typing import Any
65
from unittest.mock import patch
76

8-
import pytest
97
import requests
108
import requests_mock
11-
from inline_snapshot import snapshot
129

1310
import logfire
1411
from logfire._internal.config import GLOBAL_CONFIG, LogfireCredentials
15-
from logfire._internal.telemetry_header import (
16-
ERROR_HEADER_NAME,
17-
TELEMETRY_HEADER_NAME,
18-
WARNING_HEADER_NAME,
19-
build_telemetry_header,
20-
process_logfire_response_headers,
21-
)
22-
from logfire.exceptions import LogfireServerError, LogfireServerWarning
12+
from logfire._internal.telemetry_header import TELEMETRY_HEADER_NAME, build_telemetry_header
2313
from logfire.version import VERSION
2414

2515

@@ -112,71 +102,3 @@ def test_from_token_sends_telemetry_header():
112102
)
113103
[history] = m.request_history
114104
assert history.headers[TELEMETRY_HEADER_NAME] == '{"sdk_version":"1.2.3"}'
115-
116-
117-
def test_process_response_warning_header_emits_warning():
118-
response = requests.Response()
119-
response.headers[WARNING_HEADER_NAME] = 'The /foo/bar endpoint is deprecated, please use /bar/baz'
120-
with warnings.catch_warnings(record=True) as caught:
121-
warnings.simplefilter('always')
122-
process_logfire_response_headers(response)
123-
assert [(w.category, str(w.message)) for w in caught] == snapshot(
124-
[(LogfireServerWarning, 'The /foo/bar endpoint is deprecated, please use /bar/baz')]
125-
)
126-
127-
128-
def test_process_response_warning_header_dedupes():
129-
"""Python's default `warnings` filter should fold repeats of the same message into one entry."""
130-
response = requests.Response()
131-
response.headers[WARNING_HEADER_NAME] = 'a duplicated warning'
132-
with warnings.catch_warnings(record=True) as caught:
133-
warnings.simplefilter('default')
134-
for _ in range(5):
135-
process_logfire_response_headers(response)
136-
messages = [str(w.message) for w in caught]
137-
assert messages == ['a duplicated warning']
138-
139-
140-
def test_process_response_error_header_raises():
141-
response = requests.Response()
142-
response.headers[ERROR_HEADER_NAME] = 'something is wrong'
143-
with pytest.raises(LogfireServerError, match='something is wrong'):
144-
process_logfire_response_headers(response)
145-
146-
147-
def test_response_hook_installed_on_logfire_client():
148-
from logfire._internal.auth import UserToken
149-
from logfire._internal.client import LogfireClient
150-
151-
token = UserToken(
152-
token='pylf_v1_us_xxx',
153-
base_url='https://logfire-us.pydantic.dev',
154-
expiration='2099-12-31T23:59:59',
155-
)
156-
client = LogfireClient(user_token=token)
157-
158-
with requests_mock.Mocker() as m:
159-
m.get(
160-
'https://logfire-us.pydantic.dev/v1/account/me',
161-
json={'name': 'me'},
162-
headers={WARNING_HEADER_NAME: 'deprecated endpoint'},
163-
)
164-
with warnings.catch_warnings(record=True) as caught:
165-
warnings.simplefilter('always')
166-
client.get_user_information()
167-
168-
assert any(isinstance(w.message, LogfireServerWarning) for w in caught)
169-
170-
with requests_mock.Mocker() as m:
171-
m.get(
172-
'https://logfire-us.pydantic.dev/v1/account/me',
173-
json={'name': 'me'},
174-
headers={ERROR_HEADER_NAME: 'no longer supported'},
175-
)
176-
with pytest.raises(LogfireServerError, match='no longer supported'):
177-
client.get_user_information()
178-
179-
[history, *_] = m.request_history
180-
assert TELEMETRY_HEADER_NAME in history.headers
181-
pairs = _parse_header(history.headers[TELEMETRY_HEADER_NAME])
182-
assert pairs['sdk_version'] == VERSION

0 commit comments

Comments
 (0)