Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logfire-api #268

Merged
merged 27 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/
Comment on lines +150 to +160
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created the publisher... Not sure if this works.

Screenshot 2024-06-21 at 17 55 49

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:
Kludex marked this conversation as resolved.
Show resolved Hide resolved
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