Skip to content

Commit 94e0a60

Browse files
authored
Release v3.22.0 (#1220)
1 parent e6549cf commit 94e0a60

File tree

12 files changed

+150
-50
lines changed

12 files changed

+150
-50
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Release Notes
22

3+
## [v3.22.0] (2025-07-02)
4+
5+
* Add `instrument_google_genai` by @alexmojaki in [#1217](https://github.com/pydantic/logfire/pull/1217)
6+
* Refactor user tokens, introduce Logfire client by @Viicos in [#981](https://github.com/pydantic/logfire/pull/981)
7+
* Use new endpoint for project creation by @hramezani in [#1202](https://github.com/pydantic/logfire/pull/1202)
8+
39
## [v3.21.2] (2025-06-30)
410

511
* Fix importlib resources with auto tracing by @alexmojaki in [#1212](https://github.com/pydantic/logfire/pull/1212)
@@ -758,3 +764,4 @@ First release from new repo!
758764
[v3.21.0]: https://github.com/pydantic/logfire/compare/v3.20.0...v3.21.0
759765
[v3.21.1]: https://github.com/pydantic/logfire/compare/v3.21.0...v3.21.1
760766
[v3.21.2]: https://github.com/pydantic/logfire/compare/v3.21.1...v3.21.2
767+
[v3.22.0]: https://github.com/pydantic/logfire/compare/v3.21.2...v3.22.0

logfire-api/logfire_api/__init__.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ from .version import VERSION as VERSION
1414
from logfire.sampling import SamplingOptions as SamplingOptions
1515
from typing import Any
1616

17-
__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_system_metrics', 'instrument_mcp', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'logfire_info', 'get_baggage', 'set_baggage']
17+
__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'AdvancedOptions', 'ConsoleOptions', 'CodeSource', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'warning', 'error', 'exception', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_asgi', 'instrument_wsgi', 'instrument_pydantic', 'instrument_pydantic_ai', 'instrument_fastapi', 'instrument_openai', 'instrument_openai_agents', 'instrument_anthropic', 'instrument_google_genai', 'instrument_asyncpg', 'instrument_httpx', 'instrument_celery', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_aiohttp_server', 'instrument_sqlalchemy', 'instrument_sqlite3', 'instrument_aws_lambda', 'instrument_redis', 'instrument_pymongo', 'instrument_mysql', 'instrument_system_metrics', 'instrument_mcp', 'AutoTraceModule', 'with_tags', 'with_settings', 'suppress_scopes', 'shutdown', 'no_auto_trace', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'add_non_user_code_prefix', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'loguru_handler', 'SamplingOptions', 'MetricsOptions', 'logfire_info', 'get_baggage', 'set_baggage']
1818

1919
DEFAULT_LOGFIRE_INSTANCE = Logfire()
2020
span = DEFAULT_LOGFIRE_INSTANCE.span
@@ -30,6 +30,7 @@ instrument_fastapi = DEFAULT_LOGFIRE_INSTANCE.instrument_fastapi
3030
instrument_openai = DEFAULT_LOGFIRE_INSTANCE.instrument_openai
3131
instrument_openai_agents = DEFAULT_LOGFIRE_INSTANCE.instrument_openai_agents
3232
instrument_anthropic = DEFAULT_LOGFIRE_INSTANCE.instrument_anthropic
33+
instrument_google_genai = DEFAULT_LOGFIRE_INSTANCE.instrument_google_genai
3334
instrument_asyncpg = DEFAULT_LOGFIRE_INSTANCE.instrument_asyncpg
3435
instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx
3536
instrument_celery = DEFAULT_LOGFIRE_INSTANCE.instrument_celery

logfire-api/logfire_api/_internal/auth.pyi

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,77 @@
11
import requests
2-
from .utils import UnexpectedResponse as UnexpectedResponse
2+
from .utils import UnexpectedResponse as UnexpectedResponse, read_toml_file as read_toml_file
33
from _typeshed import Incomplete
4+
from dataclasses import dataclass
45
from logfire.exceptions import LogfireConfigError as LogfireConfigError
6+
from pathlib import Path
57
from typing import TypedDict
8+
from typing_extensions import Self
69

710
HOME_LOGFIRE: Incomplete
811
DEFAULT_FILE: Incomplete
12+
PYDANTIC_LOGFIRE_TOKEN_PATTERN: Incomplete
13+
14+
class _RegionData(TypedDict):
15+
base_url: str
16+
gcp_region: str
17+
18+
REGIONS: dict[str, _RegionData]
919

1020
class UserTokenData(TypedDict):
1121
"""User token data."""
1222
token: str
1323
expiration: str
1424

15-
class DefaultFile(TypedDict):
16-
"""Content of the default.toml file."""
25+
class UserTokensFileData(TypedDict, total=False):
26+
"""Content of the file containing the user tokens."""
1727
tokens: dict[str, UserTokenData]
1828

29+
@dataclass
30+
class UserToken:
31+
"""A user token."""
32+
token: str
33+
base_url: str
34+
expiration: str
35+
@classmethod
36+
def from_user_token_data(cls, base_url: str, token: UserTokenData) -> Self: ...
37+
@property
38+
def is_expired(self) -> bool:
39+
"""Whether the token is expired."""
40+
41+
@dataclass
42+
class UserTokenCollection:
43+
"""A collection of user tokens, read from a user tokens file.
44+
45+
Args:
46+
path: The path where the user tokens will be stored. If the path doesn't exist,
47+
an empty collection is created. Defaults to `~/.logfire/default.toml`.
48+
"""
49+
user_tokens: dict[str, UserToken]
50+
path: Path
51+
def __init__(self, path: Path | None = None) -> None: ...
52+
def get_token(self, base_url: str | None = None) -> UserToken:
53+
"""Get a user token from the collection.
54+
55+
Args:
56+
base_url: Only look for user tokens valid for this base URL. If not provided,
57+
all the tokens of the collection will be considered: if only one token is
58+
available, it will be used, otherwise the user will be prompted to choose
59+
a token.
60+
61+
Raises:
62+
LogfireConfigError: If no user token is found (no token matched the base URL,
63+
the collection is empty, or the selected token is expired).
64+
"""
65+
def is_logged_in(self, base_url: str | None = None) -> bool:
66+
"""Check whether the user token collection contains at least one valid user token.
67+
68+
Args:
69+
base_url: Only check for user tokens valid for this base URL. If not provided,
70+
all the tokens of the collection will be considered.
71+
"""
72+
def add_token(self, base_url: str, token: UserTokenData) -> UserToken:
73+
"""Add a user token to the collection."""
74+
1975
class NewDeviceFlow(TypedDict):
2076
"""Matches model of the same name in the backend."""
2177
device_code: str
@@ -47,9 +103,3 @@ def poll_for_token(session: requests.Session, device_code: str, base_api_url: st
47103
Returns:
48104
The user token.
49105
"""
50-
def is_logged_in(data: DefaultFile, logfire_url: str) -> bool:
51-
"""Check if the user is logged in.
52-
53-
Returns:
54-
True if the user is logged in, False otherwise.
55-
"""
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from .auth import UserToken as UserToken, UserTokenCollection as UserTokenCollection
2+
from .utils import UnexpectedResponse as UnexpectedResponse
3+
from _typeshed import Incomplete
4+
from logfire.exceptions import LogfireConfigError as LogfireConfigError
5+
from logfire.version import VERSION as VERSION
6+
from typing import Any
7+
from typing_extensions import Self
8+
9+
UA_HEADER: Incomplete
10+
11+
class ProjectAlreadyExists(Exception): ...
12+
13+
class InvalidProjectName(Exception):
14+
reason: Incomplete
15+
def __init__(self, reason: str, /) -> None: ...
16+
17+
class LogfireClient:
18+
"""A Logfire HTTP client to interact with the API.
19+
20+
Args:
21+
user_token: The user token to use when authenticating against the API.
22+
"""
23+
base_url: Incomplete
24+
def __init__(self, user_token: UserToken) -> None: ...
25+
@classmethod
26+
def from_url(cls, base_url: str | None) -> Self:
27+
"""Create a client from the provided base URL.
28+
29+
Args:
30+
base_url: The base URL to use when looking for a user token. If `None`, will prompt
31+
the user into selecting a token from the token collection (or, if only one available,
32+
use it directly). The token collection will be created from the `~/.logfire/default.toml`
33+
file (or an empty one if no such file exists).
34+
"""
35+
def get_user_organizations(self) -> list[dict[str, Any]]:
36+
"""Get the organizations of the logged-in user."""
37+
def get_user_information(self) -> dict[str, Any]:
38+
"""Get information about the logged-in user."""
39+
def get_user_projects(self) -> list[dict[str, Any]]:
40+
"""Get the projects of the logged-in user."""
41+
def create_new_project(self, organization: str, project_name: str):
42+
"""Create a new project.
43+
44+
Args:
45+
organization: The organization that should hold the new project.
46+
project_name: The name of the project to be created.
47+
48+
Returns:
49+
The newly created project.
50+
"""
51+
def create_write_token(self, organization: str, project_name: str) -> dict[str, Any]:
52+
"""Create a write token for the given project in the given organization."""

logfire-api/logfire_api/_internal/config.pyi

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dataclasses
22
import requests
33
from ..propagate import NoExtractTraceContextPropagator as NoExtractTraceContextPropagator, WarnOnExtractTraceContextPropagator as WarnOnExtractTraceContextPropagator
4-
from .auth import DEFAULT_FILE as DEFAULT_FILE, DefaultFile as DefaultFile, is_logged_in as is_logged_in
4+
from .client import InvalidProjectName as InvalidProjectName, LogfireClient as LogfireClient, ProjectAlreadyExists as ProjectAlreadyExists
55
from .config_params import ParamManager as ParamManager, PydanticPluginRecordValues as PydanticPluginRecordValues
66
from .constants import LevelName as LevelName, RESOURCE_ATTRIBUTES_CODE_ROOT_PATH as RESOURCE_ATTRIBUTES_CODE_ROOT_PATH, RESOURCE_ATTRIBUTES_CODE_WORK_DIR as RESOURCE_ATTRIBUTES_CODE_WORK_DIR, RESOURCE_ATTRIBUTES_DEPLOYMENT_ENVIRONMENT_NAME as RESOURCE_ATTRIBUTES_DEPLOYMENT_ENVIRONMENT_NAME, RESOURCE_ATTRIBUTES_VCS_REPOSITORY_REF_REVISION as RESOURCE_ATTRIBUTES_VCS_REPOSITORY_REF_REVISION, RESOURCE_ATTRIBUTES_VCS_REPOSITORY_URL as RESOURCE_ATTRIBUTES_VCS_REPOSITORY_URL
77
from .exporters.console import ConsoleColorsValues as ConsoleColorsValues, ConsoleLogExporter as ConsoleLogExporter, IndentedConsoleSpanExporter as IndentedConsoleSpanExporter, ShowParentsConsoleSpanExporter as ShowParentsConsoleSpanExporter, SimpleConsoleSpanExporter as SimpleConsoleSpanExporter
@@ -19,10 +19,11 @@ from .metrics import ProxyMeterProvider as ProxyMeterProvider
1919
from .scrubbing import BaseScrubber as BaseScrubber, NOOP_SCRUBBER as NOOP_SCRUBBER, Scrubber as Scrubber, ScrubbingOptions as ScrubbingOptions
2020
from .stack_info import warn_at_user_stacklevel as warn_at_user_stacklevel
2121
from .tracer import OPEN_SPANS as OPEN_SPANS, PendingSpanProcessor as PendingSpanProcessor, ProxyTracerProvider as ProxyTracerProvider
22-
from .utils import SeededRandomIdGenerator as SeededRandomIdGenerator, UnexpectedResponse as UnexpectedResponse, ensure_data_dir_exists as ensure_data_dir_exists, handle_internal_errors as handle_internal_errors, platform_is_emscripten as platform_is_emscripten, read_toml_file as read_toml_file, suppress_instrumentation as suppress_instrumentation
22+
from .utils import SeededRandomIdGenerator as SeededRandomIdGenerator, ensure_data_dir_exists as ensure_data_dir_exists, handle_internal_errors as handle_internal_errors, platform_is_emscripten as platform_is_emscripten, suppress_instrumentation as suppress_instrumentation
2323
from _typeshed import Incomplete
2424
from collections.abc import Sequence
2525
from dataclasses import dataclass, field
26+
from logfire._internal.auth import PYDANTIC_LOGFIRE_TOKEN_PATTERN as PYDANTIC_LOGFIRE_TOKEN_PATTERN, REGIONS as REGIONS
2627
from logfire._internal.baggage import DirectBaggageAttributesSpanProcessor as DirectBaggageAttributesSpanProcessor
2728
from logfire.exceptions import LogfireConfigError as LogfireConfigError
2829
from logfire.sampling import SamplingOptions as SamplingOptions
@@ -40,15 +41,8 @@ from typing_extensions import Self, Unpack
4041
CREDENTIALS_FILENAME: str
4142
COMMON_REQUEST_HEADERS: Incomplete
4243
PROJECT_NAME_PATTERN: str
43-
PYDANTIC_LOGFIRE_TOKEN_PATTERN: Incomplete
4444
METRICS_PREFERRED_TEMPORALITY: Incomplete
4545

46-
class _RegionData(TypedDict):
47-
base_url: str
48-
gcp_region: str
49-
50-
REGIONS: dict[str, _RegionData]
51-
5246
@dataclass
5347
class ConsoleOptions:
5448
"""Options for controlling console output."""
@@ -270,31 +264,14 @@ class LogfireCredentials:
270264
LogfireConfigError: If the token is invalid.
271265
"""
272266
@classmethod
273-
def get_current_user(cls, session: requests.Session, logfire_api_url: str | None = None) -> dict[str, Any] | None: ...
274-
@classmethod
275-
def get_user_projects(cls, session: requests.Session, logfire_api_url: str | None = None) -> list[dict[str, Any]]:
276-
"""Get list of projects that user has access to them.
277-
278-
Args:
279-
session: HTTP client session used to communicate with the Logfire API.
280-
logfire_api_url: The Logfire API base URL.
281-
282-
Returns:
283-
List of user projects.
284-
285-
Raises:
286-
LogfireConfigError: If there was an error retrieving user projects.
287-
"""
288-
@classmethod
289-
def use_existing_project(cls, *, session: requests.Session, projects: list[dict[str, Any]], logfire_api_url: str | None = None, organization: str | None = None, project_name: str | None = None) -> dict[str, Any] | None:
267+
def use_existing_project(cls, *, client: LogfireClient, projects: list[dict[str, Any]], organization: str | None = None, project_name: str | None = None) -> dict[str, Any] | None:
290268
"""Configure one of the user projects to be used by Logfire.
291269
292270
It configures the project if organization/project_name is a valid project that
293271
the user has access to it. Otherwise, it asks the user to select a project interactively.
294272
295273
Args:
296-
session: HTTP client session used to communicate with the Logfire API.
297-
logfire_api_url: The Logfire API base URL.
274+
client: The Logfire client to use when making requests.
298275
projects: List of user projects.
299276
organization: Project organization.
300277
project_name: Name of project that has to be used.
@@ -306,15 +283,14 @@ class LogfireCredentials:
306283
LogfireConfigError: If there was an error configuring the project.
307284
"""
308285
@classmethod
309-
def create_new_project(cls, *, session: requests.Session, logfire_api_url: str | None = None, organization: str | None = None, default_organization: bool = False, project_name: str | None = None) -> dict[str, Any]:
286+
def create_new_project(cls, *, client: LogfireClient, organization: str | None = None, default_organization: bool = False, project_name: str | None = None) -> dict[str, Any]:
310287
"""Create a new project and configure it to be used by Logfire.
311288
312289
It creates the project under the organization if both project and organization are valid.
313290
Otherwise, it asks the user to select organization and enter a valid project name interactively.
314291
315292
Args:
316-
session: HTTP client session used to communicate with the Logfire API.
317-
logfire_api_url: The Logfire API base URL.
293+
client: The Logfire client to use when making requests.
318294
organization: The organization name of the new project.
319295
default_organization: Whether to create the project under the user default organization.
320296
project_name: The default name of the project.
@@ -326,12 +302,11 @@ class LogfireCredentials:
326302
LogfireConfigError: If there was an error creating projects.
327303
"""
328304
@classmethod
329-
def initialize_project(cls, *, session: requests.Session, logfire_api_url: str | None = None) -> Self:
305+
def initialize_project(cls, *, client: LogfireClient) -> Self:
330306
"""Create a new project or use an existing project on logfire.dev requesting the given project name.
331307
332308
Args:
333-
session: HTTP client session used to communicate with the Logfire API.
334-
logfire_api_url: The Logfire API base URL.
309+
client: The Logfire client to use when making requests.
335310
336311
Returns:
337312
The new credentials.

logfire-api/logfire_api/_internal/exporters/processor_wrapper.pyi

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ from ..constants import ATTRIBUTES_JSON_SCHEMA_KEY as ATTRIBUTES_JSON_SCHEMA_KEY
22
from ..db_statement_summary import message_from_db_statement as message_from_db_statement
33
from ..json_schema import JsonSchemaProperties as JsonSchemaProperties, attributes_json_schema as attributes_json_schema
44
from ..scrubbing import BaseScrubber as BaseScrubber
5-
from ..utils import ReadableSpanDict as ReadableSpanDict, is_asgi_send_receive_span_name as is_asgi_send_receive_span_name, is_instrumentation_suppressed as is_instrumentation_suppressed, span_to_dict as span_to_dict, truncate_string as truncate_string
5+
from ..utils import ReadableSpanDict as ReadableSpanDict, handle_internal_errors as handle_internal_errors, is_asgi_send_receive_span_name as is_asgi_send_receive_span_name, is_instrumentation_suppressed as is_instrumentation_suppressed, span_to_dict as span_to_dict, truncate_string as truncate_string
66
from .wrapper import WrapperSpanProcessor as WrapperSpanProcessor
77
from dataclasses import dataclass
88
from opentelemetry import context
9-
from opentelemetry.sdk.trace import ReadableSpan, Span
9+
from opentelemetry.sdk.trace import Event as Event, ReadableSpan, Span
1010

1111
class CheckSuppressInstrumentationProcessorWrapper(WrapperSpanProcessor):
1212
"""Checks if instrumentation is suppressed, then suppresses instrumentation itself.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import logfire
2+
from logfire._internal.utils import handle_internal_errors as handle_internal_errors
3+
from opentelemetry._events import Event, EventLogger, EventLoggerProvider
4+
from typing import Any
5+
6+
class SpanEventLogger(EventLogger):
7+
@handle_internal_errors
8+
def emit(self, event: Event) -> None: ...
9+
10+
class SpanEventLoggerProvider(EventLoggerProvider):
11+
def get_event_logger(self, *args: Any, **kwargs: Any) -> SpanEventLogger: ...
12+
13+
def instrument_google_genai(logfire_instance: logfire.Logfire): ...

logfire-api/logfire_api/_internal/main.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,7 @@ class Logfire:
579579
A context manager that will revert the instrumentation when exited.
580580
Use of this context manager is optional.
581581
"""
582+
def instrument_google_genai(self) -> None: ...
582583
def instrument_asyncpg(self, **kwargs: Any) -> None:
583584
"""Instrument the `asyncpg` module so that spans are automatically created for each query."""
584585
@overload

logfire-api/logfire_api/_internal/utils.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def span_to_dict(span: ReadableSpan) -> ReadableSpanDict:
6868

6969
class UnexpectedResponse(RequestException):
7070
"""An unexpected response was received from the server."""
71+
response: Response
7172
def __init__(self, response: Response) -> None: ...
7273
@classmethod
7374
def raise_for_status(cls, response: Response) -> None:

logfire-api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "logfire-api"
7-
version = "3.21.2"
7+
version = "3.22.0"
88
description = "Shim for the Logfire SDK which does nothing unless Logfire is installed"
99
authors = [
1010
{ name = "Pydantic Team", email = "[email protected]" },

0 commit comments

Comments
 (0)