Skip to content

Commit

Permalink
Merge branch 'main' into test-with-upgraded-deps
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmojaki authored Nov 13, 2024
2 parents 8e72641 + d6879d9 commit 4791ded
Show file tree
Hide file tree
Showing 43 changed files with 1,174 additions and 500 deletions.
14 changes: 3 additions & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,19 +178,11 @@ jobs:
with:
version_file_path: pyproject.toml

- run: uv build
- name: Build artifacts
run: uv build --all

- name: Publish logfire to PyPI
- name: Publish logfire and logfire-api to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true

- name: Build logfire-api
run: uv build
working-directory: logfire-api/

- name: Publish logfire-api to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: logfire-api/dist
verbose: true
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Release Notes

## [v2.2.1] (2024-11-13)

* Ignore trivial/empty functions in auto-tracing by @alexmojaki in [#596](https://github.com/pydantic/logfire/pull/596)
* Handle missing attributes in `_custom_object_schema` by @alexmojaki in [#597](https://github.com/pydantic/logfire/pull/597)
* Let user know what they should install for integrations by @Kludex in [#593](https://github.com/pydantic/logfire/pull/593)

## [v2.2.0] (2024-11-13)

* Allow instrumenting a single httpx client by @alexmojaki in [#575](https://github.com/pydantic/logfire/pull/575)
* Log LLM tool call for streamed response by @jackmpcollins in [#545](https://github.com/pydantic/logfire/pull/545)

## [v2.1.2] (2024-11-04)

* Check `.logfire` for creds to respect `'if-token-present'` setting by @sydney-runkle in [#561](https://github.com/pydantic/logfire/pull/561)
Expand Down Expand Up @@ -406,3 +417,5 @@ First release from new repo!
[v2.1.0]: https://github.com/pydantic/logfire/compare/v2.0.0...v2.1.0
[v2.1.1]: https://github.com/pydantic/logfire/compare/v2.1.0...v2.1.1
[v2.1.2]: https://github.com/pydantic/logfire/compare/v2.1.1...v2.1.2
[v2.2.0]: https://github.com/pydantic/logfire/compare/v2.1.2...v2.2.0
[v2.2.1]: https://github.com/pydantic/logfire/compare/v2.2.0...v2.2.1
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

.PHONY: install # Install the package, dependencies, and pre-commit for local development
install: .uv .pre-commit
uv sync --frozen
uv sync --frozen --group docs
uv pip install -e logfire-api
pre-commit install --install-hooks

Expand Down
53 changes: 40 additions & 13 deletions docs/integrations/httpx.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,56 @@ Install `logfire` with the `httpx` extra:

Let's see a minimal example below. You can run it with `python main.py`:

```py title="main.py"
import logfire
import httpx
=== "Instrument the package"

logfire.configure()
logfire.instrument_httpx()
```py title="main.py"
import asyncio

import httpx
import logfire

url = "https://httpbin.org/get"
logfire.configure()
logfire.instrument_httpx()

with httpx.Client() as client:
client.get(url)
url = "https://httpbin.org/get"

with httpx.Client() as client:
client.get(url)

async def main():
async with httpx.AsyncClient() as client:
await client.get(url)

async def main():
async with httpx.AsyncClient() as client:
await client.get(url)

if __name__ == "__main__":

asyncio.run(main())
```

=== "Instrument a single client"

```py title="main.py"
import asyncio

import httpx
import logfire

logfire.configure()

url = 'https://httpbin.org/get'

with httpx.Client() as client:
logfire.instrument_httpx(client)
client.get(url)


async def main():
async with httpx.AsyncClient() as client:
logfire.instrument_httpx(client)
await client.get(url)


asyncio.run(main())
```
```

[`logfire.instrument_httpx()`][logfire.Logfire.instrument_httpx] uses the
**OpenTelemetry HTTPX Instrumentation** package,
Expand Down
2 changes: 1 addition & 1 deletion docs/integrations/openai.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Logfire supports instrumenting calls to OpenAI with one extra line of code.

```python hl_lines="6"
```python hl_lines="7"
import openai
import logfire

Expand Down
4 changes: 2 additions & 2 deletions docs/integrations/use-cases/web-frameworks.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ if __name__ == '__main__':
uvicorn.run(app)
```

If you visit http://127.0.0.1:8000/, that matches the above regex, so no spans will be sent to Logfire.
If you visit http://127.0.0.1:8000/hello/ (or any other endpoint that's not `/`, for that matter), a trace will be started and sent to Logfire.
If you visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/), that matches the above regex, so no spans will be sent to Logfire.
If you visit [http://127.0.0.1:8000/hello/](http://127.0.0.1:8000/hello/) (or any other endpoint that's not `/`, for that matter), a trace will be started and sent to Logfire.

!!! note
Under the hood, the `opentelemetry` library is using `re.search` (not `re.match` or `re.fullmatch`) to check for a match between the route and the `excluded_urls` regex, which is why we need to include the `^` at the start and `$` at the end of the regex.
Expand Down
3 changes: 2 additions & 1 deletion logfire-api/logfire_api/_internal/integrations/httpx.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import httpx
from _typeshed import Incomplete
from logfire import Logfire as Logfire
from typing import TypedDict, Unpack
Expand All @@ -14,7 +15,7 @@ class HTTPXInstrumentKwargs(TypedDict, total=False):
async_response_hook: AsyncResponseHook
skip_dep_check: bool

def instrument_httpx(logfire_instance: Logfire, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None:
def instrument_httpx(logfire_instance: Logfire, client: httpx.Client | httpx.AsyncClient | None, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None:
"""Instrument the `httpx` module so that spans are automatically created for each request.
See the `Logfire.instrument_httpx` method for details.
Expand Down
5 changes: 4 additions & 1 deletion logfire-api/logfire_api/_internal/main.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import anthropic
import httpx
import openai
import opentelemetry.trace as trace_api
from . import async_ as async_
Expand Down Expand Up @@ -529,9 +530,11 @@ class Logfire:
"""
def instrument_asyncpg(self, **kwargs: Unpack[AsyncPGInstrumentKwargs]) -> None:
"""Instrument the `asyncpg` module so that spans are automatically created for each query."""
def instrument_httpx(self, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None:
def instrument_httpx(self, client: httpx.Client | httpx.AsyncClient | None = None, **kwargs: Unpack[HTTPXInstrumentKwargs]) -> None:
"""Instrument the `httpx` module so that spans are automatically created for each request.
Optionally, pass an `httpx.Client` instance to instrument only that client.
Uses the
[OpenTelemetry HTTPX Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/httpx/httpx.html)
library, specifically `HTTPXClientInstrumentor().instrument()`, to which it passes `**kwargs`.
Expand Down
9 changes: 5 additions & 4 deletions logfire-api/logfire_api/_internal/metrics.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dataclasses
from _typeshed import Incomplete
from abc import ABC
from opentelemetry.context import Context
from opentelemetry.metrics import CallbackT as CallbackT, Counter, Histogram, Instrument, Meter, MeterProvider, ObservableCounter, ObservableGauge, ObservableUpDownCounter, UpDownCounter, _Gauge
from opentelemetry.util.types import Attributes
from threading import Lock
Expand Down Expand Up @@ -45,17 +46,17 @@ class _ProxyAsynchronousInstrument(_ProxyInstrument[InstrumentT], ABC):
def __init__(self, instrument: InstrumentT, name: str, callbacks: Sequence[CallbackT] | None, unit: str, description: str) -> None: ...

class _ProxyCounter(_ProxyInstrument[Counter], Counter):
def add(self, amount: int | float, attributes: Attributes | None = None) -> None: ...
def add(self, amount: int | float, attributes: Attributes | None = None, context: Context | None = None) -> None: ...

class _ProxyHistogram(_ProxyInstrument[Histogram], Histogram):
def record(self, amount: int | float, attributes: Attributes | None = None) -> None: ...
def record(self, amount: int | float, attributes: Attributes | None = None, context: Context | None = None) -> None: ...

class _ProxyObservableCounter(_ProxyAsynchronousInstrument[ObservableCounter], ObservableCounter): ...
class _ProxyObservableGauge(_ProxyAsynchronousInstrument[ObservableGauge], ObservableGauge): ...
class _ProxyObservableUpDownCounter(_ProxyAsynchronousInstrument[ObservableUpDownCounter], ObservableUpDownCounter): ...

class _ProxyUpDownCounter(_ProxyInstrument[UpDownCounter], UpDownCounter):
def add(self, amount: int | float, attributes: Attributes | None = None) -> None: ...
def add(self, amount: int | float, attributes: Attributes | None = None, context: Context | None = None) -> None: ...

class _ProxyGauge(_ProxyInstrument[Gauge], Gauge):
def set(self, amount: int | float, attributes: Attributes | None = None) -> None: ...
def set(self, amount: int | float, attributes: Attributes | None = None, context: Context | None = None) -> None: ...
2 changes: 1 addition & 1 deletion logfire-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "logfire-api"
version = "2.1.2"
version = "2.2.1"
description = "Shim for the Logfire SDK which does nothing unless Logfire is installed"
authors = [
{ name = "Pydantic Team", email = "engineering@pydantic.dev" },
Expand Down
13 changes: 13 additions & 0 deletions logfire/_internal/ast_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ def rewrite_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualnam
# so it's still recognized as a docstring.
new_body.append(body.pop(0))

# Ignore functions with a trivial/empty body:
# - If `body` is empty, that means it originally was just a docstring that got popped above.
# - If `body` is just a single `pass` statement
# - If `body` is just a constant expression, particularly an ellipsis (`...`)
if not body or (
len(body) == 1
and (
isinstance(body[0], ast.Pass)
or (isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant))
)
):
return node

span = ast.With(
items=[
ast.withitem(
Expand Down
10 changes: 8 additions & 2 deletions logfire/_internal/integrations/aiohttp_client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from typing import Any

from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor

try:
from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor
except ModuleNotFoundError:
raise RuntimeError(
'`logfire.instrument_aiohttp_client()` requires the `opentelemetry-instrumentation-aiohttp-client` package.\n'
'You can install this with:\n'
" pip install 'logfire[aiohttp]'"
)
from logfire import Logfire


Expand Down
9 changes: 8 additions & 1 deletion logfire/_internal/integrations/asyncpg.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

from typing import TYPE_CHECKING

from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
try:
from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
except ModuleNotFoundError:
raise RuntimeError(
'`logfire.instrument_asyncpg()` requires the `opentelemetry-instrumentation-asyncpg` package.\n'
'You can install this with:\n'
" pip install 'logfire[asyncpg]'"
)

from logfire import Logfire

Expand Down
9 changes: 8 additions & 1 deletion logfire/_internal/integrations/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

from typing import TYPE_CHECKING

from opentelemetry.instrumentation.celery import CeleryInstrumentor
try:
from opentelemetry.instrumentation.celery import CeleryInstrumentor
except ModuleNotFoundError:
raise RuntimeError(
'`logfire.instrument_celery()` requires the `opentelemetry-instrumentation-celery` package.\n'
'You can install this with:\n'
" pip install 'logfire[celery]'"
)

from logfire import Logfire

Expand Down
10 changes: 9 additions & 1 deletion logfire/_internal/integrations/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
from typing import TYPE_CHECKING

from flask.app import Flask
from opentelemetry.instrumentation.flask import FlaskInstrumentor

try:
from opentelemetry.instrumentation.flask import FlaskInstrumentor
except ModuleNotFoundError:
raise RuntimeError(
'`logfire.instrument_flask()` requires the `opentelemetry-instrumentation-flask` package.\n'
'You can install this with:\n'
" pip install 'logfire[flask]'"
)

from logfire import Logfire
from logfire._internal.utils import maybe_capture_server_headers
Expand Down
9 changes: 8 additions & 1 deletion logfire/_internal/integrations/httpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

from typing import TYPE_CHECKING, Any

from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
try:
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
except ModuleNotFoundError:
raise RuntimeError(
'`logfire.instrument_httpx()` requires the `opentelemetry-instrumentation-httpx` package.\n'
'You can install this with:\n'
" pip install 'logfire[httpx]'"
)

from logfire import Logfire

Expand Down
18 changes: 15 additions & 3 deletions logfire/_internal/integrations/llm_providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import anthropic
from anthropic.types import Message, TextBlock, TextDelta

from .types import EndpointConfig
from .types import EndpointConfig, StreamState

if TYPE_CHECKING:
from anthropic._models import FinalRequestOptions
Expand All @@ -32,13 +32,12 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig:
return EndpointConfig(
message_template='Message with {request_data[model]!r}',
span_data={'request_data': json_data},
content_from_stream=content_from_messages,
stream_state_cls=AnthropicMessageStreamState,
)
else:
return EndpointConfig(
message_template='Anthropic API call to {url!r}',
span_data={'request_data': json_data, 'url': url},
content_from_stream=None,
)


Expand All @@ -50,6 +49,19 @@ def content_from_messages(chunk: anthropic.types.MessageStreamEvent) -> str | No
return None


class AnthropicMessageStreamState(StreamState):
def __init__(self):
self._content: list[str] = []

def record_chunk(self, chunk: anthropic.types.MessageStreamEvent) -> None:
content = content_from_messages(chunk)
if content:
self._content.append(content)

def get_response_data(self) -> Any:
return {'combined_chunk_content': ''.join(self._content), 'chunk_count': len(self._content)}


def on_response(response: ResponseT, span: LogfireSpan) -> ResponseT:
"""Updates the span based on the type of response."""
if isinstance(response, Message): # pragma: no branch
Expand Down
Loading

0 comments on commit 4791ded

Please sign in to comment.