Skip to content

Commit

Permalink
Add logfire-api (#268)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kludex committed Jul 5, 2024
1 parent 39fc014 commit 822d554
Show file tree
Hide file tree
Showing 73 changed files with 3,357 additions and 14 deletions.
16 changes: 14 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- run: rye config --set-bool behavior.use-uv=true
- run: rye sync --no-lock
- run: make lint
- run: rye run pyright
- run: rye run pyright logfire tests

docs:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -73,6 +73,9 @@ jobs:
- run: rye config --set-bool behavior.use-uv=true
# Update all dependencies to the latest version possible
- run: rye sync --update-all
- run: |
pip install uv
uv pip install "logfire-api @ file://logfire-api"
- run: rye show
- run: mkdir coverage
- run: make test
Expand Down Expand Up @@ -144,5 +147,14 @@ jobs:
- run: rye config --set-bool behavior.use-uv=true
- run: rye build

- name: Upload package to PyPI
- name: Publish logfire to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

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

- name: Publish logfire-api to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: logfire-api/
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ __pycache__
*.env
/scratch/
/.coverage

# stubgen
out
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ sources = pydantic tests docs/plugins
install: .rye .pre-commit
rye show
rye sync --no-lock
uv pip install -e logfire-api
pre-commit install --install-hooks

.PHONY: format # Format the code
Expand All @@ -29,6 +30,10 @@ lint:
test:
rye run coverage run -m pytest

.PHONY: generate-stubs # Generate stubs for logfire-api
generate-stubs:
rye run generate-stubs

.PHONY: testcov # Run tests and generate a coverage report
testcov: test
@echo "building coverage html"
Expand Down
2 changes: 1 addition & 1 deletion docs/integrations/third_party/litellm.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# LiteLLM
# LiteLLM

LiteLLM allows you to call over 100 Large Language Models (LLMs) using the same input/output format. It also supports Logfire for logging and monitoring.

Expand Down
10 changes: 10 additions & 0 deletions logfire-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# python generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# venv
.venv
7 changes: 7 additions & 0 deletions logfire-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# logfire-api

Shim for the logfire SDK Python API which does nothing unless logfire is installed.

This package is designed to be used by packages that want to provide opt-in integration with [Logfire](https://github.com/pydantic/logfire).

The package provides a clone of the Python API exposed by the `logfire` package which does nothing if the `logfire` package is not installed, but makes real calls when it is.
190 changes: 190 additions & 0 deletions logfire-api/logfire_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
from __future__ import annotations

from contextlib import contextmanager
import importlib
import sys
from typing import TYPE_CHECKING, ContextManager, Literal
from contextlib import nullcontext
from unittest.mock import MagicMock

try:
logfire_module = importlib.import_module('logfire')
sys.modules[__name__] = logfire_module

except ImportError:
if not TYPE_CHECKING: # pragma: no branch
LevelName = Literal['trace', 'debug', 'info', 'notice', 'warn', 'warning', 'error', 'fatal']
VERSION = '0.0.0'
METRICS_PREFERRED_TEMPORALITY = {}

def configure(*args, **kwargs): ...

class LogfireSpan:
def __getattr__(self, attr):
return MagicMock() # pragma: no cover

def __enter__(self):
return self

def __exit__(self, *args, **kwargs) -> None: ...

@property
def message_template(self) -> str: # pragma: no cover
return ''

@property
def tags(self) -> Sequence[str]: # pragma: no cover
return []

@property
def message(self) -> str: # pragma: no cover
return ''

@message.setter
def message(self, message: str): ... # pragma: no cover

def is_recording(self) -> bool: # pragma: no cover
return False

class Logfire:
def __getattr__(self, attr):
return MagicMock() # pragma: no cover

def __init__(self, *args, **kwargs) -> None: ...

def span(self, *args, **kwargs) -> LogfireSpan:
return LogfireSpan()

def log(self, *args, **kwargs) -> None: ...

def trace(self, *args, **kwargs) -> None: ...

def debug(self, *args, **kwargs) -> None: ...

def notice(self, *args, **kwargs) -> None: ...

def info(self, *args, **kwargs) -> None: ...

def warn(self, *args, **kwargs) -> None: ...

def error(self, *args, **kwargs) -> None: ...

def fatal(self, *args, **kwargs) -> None: ...

def with_tags(self, *args, **kwargs) -> Logfire:
return self

def with_settings(self, *args, **kwargs) -> Logfire:
return self

def force_flush(self, *args, **kwargs) -> None: ...

def log_slow_async_callbacks(self, *args, **kwargs) -> None: # pragma: no branch
return nullcontext()

def install_auto_tracing(self, *args, **kwargs) -> None: ...

def instrument(self, *args, **kwargs):
def decorator(func):
return func

return decorator

def instrument_fastapi(self, *args, **kwargs) -> ContextManager[None]:
return nullcontext()

def instrument_pymongo(self, *args, **kwargs) -> None: ...

def instrument_sqlalchemy(self, *args, **kwargs) -> None: ...

def instrument_redis(self, *args, **kwargs) -> None: ...

def instrument_flask(self, *args, **kwargs) -> None: ...

def instrument_starlette(self, *args, **kwargs) -> None: ...

def instrument_django(self, *args, **kwargs) -> None: ...

def instrument_psycopg(self, *args, **kwargs) -> None: ...

def instrument_requests(self, *args, **kwargs) -> None: ...

def instrument_httpx(self, *args, **kwargs) -> None: ...

def instrument_asyncpg(self, *args, **kwargs) -> None: ...

def instrument_anthropic(self, *args, **kwargs) -> ContextManager[None]:
return nullcontext()

def instrument_openai(self, *args, **kwargs) -> ContextManager[None]:
return nullcontext()

def instrument_aiohttp_client(self, *args, **kwargs) -> None: ...

def shutdown(self, *args, **kwargs) -> None: ...

DEFAULT_LOGFIRE_INSTANCE = Logfire()
span = DEFAULT_LOGFIRE_INSTANCE.span
log = DEFAULT_LOGFIRE_INSTANCE.log
trace = DEFAULT_LOGFIRE_INSTANCE.trace
debug = DEFAULT_LOGFIRE_INSTANCE.debug
notice = DEFAULT_LOGFIRE_INSTANCE.notice
info = DEFAULT_LOGFIRE_INSTANCE.info
warn = DEFAULT_LOGFIRE_INSTANCE.warn
error = DEFAULT_LOGFIRE_INSTANCE.error
fatal = DEFAULT_LOGFIRE_INSTANCE.fatal
with_tags = DEFAULT_LOGFIRE_INSTANCE.with_tags
with_settings = DEFAULT_LOGFIRE_INSTANCE.with_settings
force_flush = DEFAULT_LOGFIRE_INSTANCE.force_flush
log_slow_async_callbacks = DEFAULT_LOGFIRE_INSTANCE.log_slow_async_callbacks
install_auto_tracing = DEFAULT_LOGFIRE_INSTANCE.install_auto_tracing
instrument = DEFAULT_LOGFIRE_INSTANCE.instrument
instrument_fastapi = DEFAULT_LOGFIRE_INSTANCE.instrument_fastapi
instrument_openai = DEFAULT_LOGFIRE_INSTANCE.instrument_openai
instrument_anthropic = DEFAULT_LOGFIRE_INSTANCE.instrument_anthropic
instrument_asyncpg = DEFAULT_LOGFIRE_INSTANCE.instrument_asyncpg
instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx
instrument_requests = DEFAULT_LOGFIRE_INSTANCE.instrument_requests
instrument_psycopg = DEFAULT_LOGFIRE_INSTANCE.instrument_psycopg
instrument_django = DEFAULT_LOGFIRE_INSTANCE.instrument_django
instrument_flask = DEFAULT_LOGFIRE_INSTANCE.instrument_flask
instrument_starlette = DEFAULT_LOGFIRE_INSTANCE.instrument_starlette
instrument_aiohttp_client = DEFAULT_LOGFIRE_INSTANCE.instrument_aiohttp_client
instrument_sqlalchemy = DEFAULT_LOGFIRE_INSTANCE.instrument_sqlalchemy
instrument_redis = DEFAULT_LOGFIRE_INSTANCE.instrument_redis
instrument_pymongo = DEFAULT_LOGFIRE_INSTANCE.instrument_pymongo
shutdown = DEFAULT_LOGFIRE_INSTANCE.shutdown

def no_auto_trace(x):
return x

@contextmanager
def suppress_instrumentation():
yield

class ConsoleOptions:
def __init__(self, *args, **kwargs) -> None: ...

class TailSamplingOptions:
def __init__(self, *args, **kwargs) -> None: ...

class ScrubbingOptions:
def __init__(self, *args, **kwargs) -> None: ...

class PydanticPlugin:
def __init__(self, *args, **kwargs) -> None: ...

class ScrubMatch:
def __init__(self, *args, **kwargs) -> None: ...

class AutoTraceModule:
def __init__(self, *args, **kwargs) -> None: ...

class StructlogProcessor:
def __init__(self, *args, **kwargs) -> None: ...

class LogfireLoggingHandler:
def __init__(self, *args, **kwargs) -> None: ...

def load_spans_from_file(*args, **kwargs):
return []
47 changes: 47 additions & 0 deletions logfire-api/logfire_api/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from ._internal.auto_trace import AutoTraceModule as AutoTraceModule
from ._internal.auto_trace.rewrite_ast import no_auto_trace as no_auto_trace
from ._internal.config import ConsoleOptions as ConsoleOptions, METRICS_PREFERRED_TEMPORALITY as METRICS_PREFERRED_TEMPORALITY, PydanticPlugin as PydanticPlugin, configure as configure
from ._internal.constants import LevelName as LevelName
from ._internal.exporters.file import load_file as load_spans_from_file
from ._internal.exporters.tail_sampling import TailSamplingOptions as TailSamplingOptions
from ._internal.main import Logfire as Logfire, LogfireSpan as LogfireSpan
from ._internal.scrubbing import ScrubMatch as ScrubMatch, ScrubbingOptions as ScrubbingOptions
from ._internal.utils import suppress_instrumentation as suppress_instrumentation
from .integrations.logging import LogfireLoggingHandler as LogfireLoggingHandler
from .integrations.structlog import LogfireProcessor as StructlogProcessor
from .version import VERSION as VERSION

__all__ = ['Logfire', 'LogfireSpan', 'LevelName', 'ConsoleOptions', 'PydanticPlugin', 'configure', 'span', 'instrument', 'log', 'trace', 'debug', 'notice', 'info', 'warn', 'error', 'fatal', 'force_flush', 'log_slow_async_callbacks', 'install_auto_tracing', 'instrument_fastapi', 'instrument_openai', 'instrument_anthropic', 'instrument_asyncpg', 'instrument_httpx', 'instrument_requests', 'instrument_psycopg', 'instrument_django', 'instrument_flask', 'instrument_starlette', 'instrument_aiohttp_client', 'instrument_sqlalchemy', 'instrument_redis', 'instrument_pymongo', 'AutoTraceModule', 'with_tags', 'with_settings', 'shutdown', 'load_spans_from_file', 'no_auto_trace', 'METRICS_PREFERRED_TEMPORALITY', 'ScrubMatch', 'ScrubbingOptions', 'VERSION', 'suppress_instrumentation', 'StructlogProcessor', 'LogfireLoggingHandler', 'TailSamplingOptions']

DEFAULT_LOGFIRE_INSTANCE = Logfire()
span = DEFAULT_LOGFIRE_INSTANCE.span
instrument = DEFAULT_LOGFIRE_INSTANCE.instrument
force_flush = DEFAULT_LOGFIRE_INSTANCE.force_flush
log_slow_async_callbacks = DEFAULT_LOGFIRE_INSTANCE.log_slow_async_callbacks
install_auto_tracing = DEFAULT_LOGFIRE_INSTANCE.install_auto_tracing
instrument_fastapi = DEFAULT_LOGFIRE_INSTANCE.instrument_fastapi
instrument_openai = DEFAULT_LOGFIRE_INSTANCE.instrument_openai
instrument_anthropic = DEFAULT_LOGFIRE_INSTANCE.instrument_anthropic
instrument_asyncpg = DEFAULT_LOGFIRE_INSTANCE.instrument_asyncpg
instrument_httpx = DEFAULT_LOGFIRE_INSTANCE.instrument_httpx
instrument_requests = DEFAULT_LOGFIRE_INSTANCE.instrument_requests
instrument_psycopg = DEFAULT_LOGFIRE_INSTANCE.instrument_psycopg
instrument_django = DEFAULT_LOGFIRE_INSTANCE.instrument_django
instrument_flask = DEFAULT_LOGFIRE_INSTANCE.instrument_flask
instrument_starlette = DEFAULT_LOGFIRE_INSTANCE.instrument_starlette
instrument_aiohttp_client = DEFAULT_LOGFIRE_INSTANCE.instrument_aiohttp_client
instrument_sqlalchemy = DEFAULT_LOGFIRE_INSTANCE.instrument_sqlalchemy
instrument_redis = DEFAULT_LOGFIRE_INSTANCE.instrument_redis
instrument_pymongo = DEFAULT_LOGFIRE_INSTANCE.instrument_pymongo
shutdown = DEFAULT_LOGFIRE_INSTANCE.shutdown
with_tags = DEFAULT_LOGFIRE_INSTANCE.with_tags
with_settings = DEFAULT_LOGFIRE_INSTANCE.with_settings
log = DEFAULT_LOGFIRE_INSTANCE.log
trace = DEFAULT_LOGFIRE_INSTANCE.trace
debug = DEFAULT_LOGFIRE_INSTANCE.debug
info = DEFAULT_LOGFIRE_INSTANCE.info
notice = DEFAULT_LOGFIRE_INSTANCE.notice
warn = DEFAULT_LOGFIRE_INSTANCE.warn
error = DEFAULT_LOGFIRE_INSTANCE.error
fatal = DEFAULT_LOGFIRE_INSTANCE.fatal
__version__ = VERSION
Empty file.
34 changes: 34 additions & 0 deletions logfire-api/logfire_api/_internal/ast_utils.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import ast
from .constants import ATTRIBUTES_MESSAGE_TEMPLATE_KEY as ATTRIBUTES_MESSAGE_TEMPLATE_KEY, ATTRIBUTES_SAMPLE_RATE_KEY as ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_TAGS_KEY as ATTRIBUTES_TAGS_KEY
from .stack_info import StackInfo as StackInfo, get_filepath_attribute as get_filepath_attribute
from .utils import uniquify_sequence as uniquify_sequence
from dataclasses import dataclass
from opentelemetry.util import types as otel_types

@dataclass(frozen=True)
class LogfireArgs:
"""Values passed to `logfire.instrument` and/or values stored in a logfire instance as basic configuration.
These determine the arguments passed to the method calls added by the AST transformer.
"""
tags: tuple[str, ...]
sample_rate: float | None
msg_template: str | None = ...
span_name: str | None = ...
extract_args: bool = ...

@dataclass
class BaseTransformer(ast.NodeTransformer):
"""Helper for rewriting ASTs to wrap function bodies in `with {logfire_method_name}(...):`."""
logfire_args: LogfireArgs
logfire_method_name: str
filename: str
module_name: str
qualname_stack = ...
def __post_init__(self) -> None: ...
def visit_ClassDef(self, node: ast.ClassDef): ...
def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef): ...
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): ...
def rewrite_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualname: str) -> ast.AST: ...
def logfire_method_call_node(self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualname: str) -> ast.Call: ...
def logfire_method_arg_values(self, qualname: str, lineno: int) -> tuple[str, dict[str, otel_types.AttributeValue]]: ...
22 changes: 22 additions & 0 deletions logfire-api/logfire_api/_internal/async_.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from .constants import ONE_SECOND_IN_NANOSECONDS as ONE_SECOND_IN_NANOSECONDS
from .main import Logfire as Logfire
from .stack_info import StackInfo as StackInfo, get_code_object_info as get_code_object_info, get_stack_info_from_frame as get_stack_info_from_frame
from .utils import safe_repr as safe_repr
from _typeshed import Incomplete
from types import CoroutineType
from typing import Any, ContextManager

ASYNCIO_PATH: Incomplete

def log_slow_callbacks(logfire: Logfire, slow_duration: float) -> ContextManager[None]:
"""Log a warning whenever a function running in the asyncio event loop blocks for too long.
See Logfire.log_slow_async_callbacks.
Inspired by https://gitlab.com/quantlane/libs/aiodebug.
"""

class _CallbackAttributes(StackInfo, total=False):
name: str
stack: list[StackInfo]

def stack_info_from_coroutine(coro: CoroutineType[Any, Any, Any]) -> StackInfo: ...
Loading

0 comments on commit 822d554

Please sign in to comment.