Skip to content

Commit bca78cf

Browse files
Kludexclaude
andcommitted
Simplify url_from_eval to return None when not sending data
If project_url is not available (i.e. not sending data to Logfire), url_from_eval returns None since the URL wouldn't work anyway. This removes the early credential loading, background thread waiting, and generation tracking that were added to support url_from_eval when not sending data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 065ea81 commit bca78cf

3 files changed

Lines changed: 18 additions & 143 deletions

File tree

logfire/_internal/config.py

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -802,10 +802,6 @@ def _load_configuration(
802802

803803
self.additional_span_processors = additional_span_processors
804804
self.project_url: str | None = None
805-
if hasattr(self, '_check_tokens_thread') and self._check_tokens_thread is not None:
806-
self._check_tokens_thread.join(timeout=0)
807-
self._check_tokens_thread: Thread | None = None
808-
self._config_generation: int = getattr(self, '_config_generation', 0) + 1
809805

810806
if metrics is None:
811807
metrics = MetricsOptions()
@@ -1062,22 +1058,19 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
10621058
if isinstance(self.metrics, MetricsOptions):
10631059
metric_readers = list(self.metrics.additional_readers)
10641060

1065-
# Try loading credentials from a file.
1066-
# We do this before checking send_to_logfire so that project_url
1067-
# is available for url_from_eval even when not sending data.
1068-
try:
1069-
credentials = LogfireCredentials.load_creds_file(self.data_dir)
1070-
except Exception:
1071-
# If we have tokens configured by other means, e.g. the env, no need to worry about the creds file.
1072-
if self.send_to_logfire and not self.token:
1073-
raise
1074-
credentials = None
1075-
if credentials is not None:
1076-
self.project_url = self.project_url or credentials.project_url
1077-
10781061
if self.send_to_logfire:
10791062
show_project_link: bool = self.console and self.console.show_project_link or False
10801063

1064+
# Try loading credentials from a file.
1065+
# If that works, we can use it to immediately print the project link.
1066+
try:
1067+
credentials = LogfireCredentials.load_creds_file(self.data_dir)
1068+
except Exception:
1069+
# If we have tokens configured by other means, e.g. the env, no need to worry about the creds file.
1070+
if not self.token:
1071+
raise
1072+
credentials = None
1073+
10811074
if not self.token and self.send_to_logfire is True and credentials is None:
10821075
# If we don't have tokens or credentials from a file,
10831076
# try initializing a new project and writing a new creds file.
@@ -1113,25 +1106,22 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
11131106
# This may happen some time later in a background thread which can be annoying,
11141107
# hence we try to print it eagerly above.
11151108
# But we only have the link if we have a creds file, otherwise we only know the token at this point.
1116-
generation = self._config_generation
1117-
11181109
def check_tokens():
11191110
with suppress_instrumentation():
11201111
for token in token_list:
1121-
if self._config_generation != generation:
1122-
return
11231112
validated_credentials = self._initialize_credentials_from_token(token)
1124-
if validated_credentials is not None:
1125-
if self._config_generation == generation:
1126-
self.project_url = self.project_url or validated_credentials.project_url
1127-
if show_project_link and token not in printed_tokens:
1128-
validated_credentials.print_token_summary()
1113+
if (
1114+
validated_credentials is not None
1115+
and show_project_link
1116+
and token not in printed_tokens
1117+
):
1118+
validated_credentials.print_token_summary()
11291119

11301120
if emscripten: # pragma: no cover
11311121
check_tokens()
11321122
else:
1133-
self._check_tokens_thread = Thread(target=check_tokens, name='check_logfire_token')
1134-
self._check_tokens_thread.start()
1123+
thread = Thread(target=check_tokens, name='check_logfire_token')
1124+
thread.start()
11351125

11361126
# Create exporters for each token
11371127
for token in token_list:
@@ -1426,16 +1416,6 @@ def warn_if_not_initialized(self, message: str):
14261416
category=LogfireNotConfiguredWarning,
14271417
)
14281418

1429-
def wait_for_token_validation(self) -> None:
1430-
"""Wait for the background token validation thread to complete.
1431-
1432-
This ensures that `project_url` is populated when the token is provided
1433-
via environment variable rather than a credentials file.
1434-
"""
1435-
thread = self._check_tokens_thread
1436-
if thread is not None:
1437-
thread.join()
1438-
14391419
def _initialize_credentials_from_token(self, token: str) -> LogfireCredentials | None:
14401420
return LogfireCredentials.from_token(token, requests.Session(), self.advanced.generate_base_url(token))
14411421

logfire/_internal/main.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -902,9 +902,6 @@ def url_from_eval(self, report: EvaluationReport[Any, Any, Any]) -> str | None:
902902
Returns:
903903
The URL string, or `None` if the project URL or trace/span IDs are not available.
904904
"""
905-
# Wait for the background token validation thread to finish,
906-
# since it may populate project_url when no credentials file exists.
907-
self._config.wait_for_token_validation()
908905
project_url = self._config.project_url
909906
trace_id = report.trace_id
910907
span_id = report.span_id

tests/test_url_from_eval.py

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
from __future__ import annotations
22

3-
from pathlib import Path
4-
from threading import Event
5-
from unittest.mock import patch
6-
73
import pytest
8-
import requests_mock
94

105
try:
116
from pydantic_evals.reporting import EvaluationReport
@@ -77,100 +72,3 @@ def test_url_from_eval_no_ids() -> None:
7772
report = _make_report()
7873
result = instance.url_from_eval(report)
7974
assert result is None
80-
81-
82-
def test_reconfigure_discards_stale_project_url(tmp_path: Path) -> None:
83-
"""Test that a background thread from a previous configure() call
84-
does not write a stale project_url after reconfigure() bumps the generation."""
85-
block = Event()
86-
first_thread_done = Event()
87-
original_from_token = LogfireConfig._initialize_credentials_from_token # pyright: ignore[reportPrivateUsage]
88-
89-
call_count = 0
90-
91-
def slow_from_token(self: LogfireConfig, token: str):
92-
nonlocal call_count
93-
call_count += 1
94-
if call_count == 1:
95-
# First configure's thread blocks here until we release it
96-
block.wait()
97-
result = original_from_token(self, token)
98-
first_thread_done.set()
99-
return result
100-
return original_from_token(self, token)
101-
102-
with (
103-
requests_mock.Mocker() as mocker,
104-
patch.object(LogfireConfig, '_initialize_credentials_from_token', slow_from_token),
105-
):
106-
mocker.get(
107-
'https://logfire-us.pydantic.dev/v1/info',
108-
json={
109-
'project_name': 'stale',
110-
'project_url': 'https://logfire-us.pydantic.dev/stale-org/stale-project',
111-
},
112-
)
113-
config = LogfireConfig(
114-
send_to_logfire=True,
115-
token=['token-a', 'token-a2'],
116-
console=False,
117-
data_dir=tmp_path,
118-
)
119-
config.initialize()
120-
first_thread = config._check_tokens_thread # pyright: ignore[reportPrivateUsage]
121-
assert first_thread is not None
122-
# First thread is now blocked in slow_from_token.
123-
# Reconfigure the same config object - this bumps the generation.
124-
config.configure(
125-
send_to_logfire=False,
126-
token=None,
127-
api_key=None,
128-
service_name=None,
129-
service_version=None,
130-
environment=None,
131-
console=False,
132-
config_dir=None,
133-
data_dir=tmp_path,
134-
additional_span_processors=None,
135-
metrics=None,
136-
scrubbing=None,
137-
inspect_arguments=None,
138-
sampling=None,
139-
min_level=None,
140-
add_baggage_to_attributes=True,
141-
code_source=None,
142-
variables=None,
143-
distributed_tracing=None,
144-
advanced=None,
145-
)
146-
# Release the first thread
147-
block.set()
148-
assert first_thread_done.wait(timeout=5)
149-
first_thread.join(timeout=5)
150-
assert not first_thread.is_alive()
151-
# The stale thread should NOT have written its project_url
152-
assert config.project_url is None
153-
154-
155-
def test_url_from_eval_waits_for_token_validation(tmp_path: Path) -> None:
156-
"""Test that url_from_eval waits for the background token validation thread
157-
to populate project_url when the token is provided directly (no creds file)."""
158-
with requests_mock.Mocker() as mocker:
159-
mocker.get(
160-
'https://logfire-us.pydantic.dev/v1/info',
161-
json={
162-
'project_name': 'myproject',
163-
'project_url': 'https://logfire-us.pydantic.dev/my-org/my-project',
164-
},
165-
)
166-
logfire.configure(
167-
send_to_logfire=True,
168-
token='fake-token',
169-
console=False,
170-
data_dir=tmp_path,
171-
)
172-
173-
report = _make_report(trace_id='abc123', span_id='def456')
174-
# url_from_eval should wait for the background thread and return the URL
175-
result = logfire.url_from_eval(report)
176-
assert result == 'https://logfire-us.pydantic.dev/my-org/my-project/evals/compare?experiment=abc123-def456'

0 commit comments

Comments
 (0)