Metadata-Version: 2.4
Name: infinity-observability-sdk
Version: 0.6.0
Summary: Unified observability SDK for Infinity Constellation services — tracing, structured logging, error tracking.
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: opentelemetry-api>=1.39.1
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.39.1
Requires-Dist: opentelemetry-sdk>=1.39.1
Requires-Dist: structlog>=25.1.0
Provides-Extra: django
Requires-Dist: opentelemetry-instrumentation-django>=0.48b0; extra == 'django'
Provides-Extra: logfire
Requires-Dist: logfire>=4.18.0; extra == 'logfire'
Provides-Extra: sentry
Requires-Dist: sentry-sdk>=2.0.0; extra == 'sentry'
Description-Content-Type: text/markdown

# Infinity Observability SDK

[![CI](https://github.com/infinity-constellation/infinity-observability-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/infinity-constellation/infinity-observability-sdk/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/infinity-observability-sdk)](https://pypi.org/project/infinity-observability-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/infinity-observability-sdk)](https://pypi.org/project/infinity-observability-sdk/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![basedpyright](https://img.shields.io/badge/type_checker-basedpyright-blue)](https://github.com/DetachHead/basedpyright)

Unified observability for Infinity Constellation services — OTel tracing, structured logging, and optional error tracking in a single `configure_observability()` call.

## Installation

```bash
pip install infinity-observability-sdk
```

Optional extras:

```bash
pip install infinity-observability-sdk[sentry]    # Sentry error tracking
pip install infinity-observability-sdk[logfire]   # pydantic-ai / httpx auto-instrumentation
pip install infinity-observability-sdk[django]    # Django OTel auto-instrumentation
```

## Quick start

```python
from infinity_observability_sdk import (
    ObservabilityConfig,
    configure_observability,
    get_logger,
)

configure_observability(ObservabilityConfig(
    business_unit_name="my_bu",
    service_name="my_service",
))

logger = get_logger(__name__)
logger.info("service.started", port=8000)
```

Set the required environment variables before calling `configure_observability`:

| Variable | Description |
|---|---|
| `INFINITY_OBSERVABILITY_ENDPOINT` | OTLP collector endpoint (e.g. `https://collector.example.com`) |
| `INFINITY_OBSERVABILITY_API_KEY` | API key for collector authentication |
| `ENVIRONMENT` | Deployment environment — controls log format. `development` → human-readable console output; anything else → JSON. Defaults to `production`. |

## Configuration

```python
ObservabilityConfig(
    business_unit_name="my_bu",       # required
    service_name="my_service",        # required

    # Integrations
    instrument_pydantic_ai=True,      # default: True
    instrument_httpx=True,            # default: True
    sentry_dsn=None,                  # optional — enables Sentry if set
    sentry_environment=None,          # falls back to ENVIRONMENT env var
    sentry_traces_sample_rate=0.0,    # 0.0–1.0, default: 0.0

    # Logging
    log_level="INFO",                 # DEBUG | INFO | WARNING | ERROR | CRITICAL

    # Advanced
    additional_resource_attributes={},  # extra OTel resource attributes
)
```

`configure_observability()` is idempotent — safe to call multiple times; subsequent calls are no-ops.

## Logging contract

`get_logger()` is the **only** application-facing log API. The stdlib `logging`
bridge exists so third-party libraries (Django, uvicorn, httpx) land in the same
pipeline — not so application code can bypass `get_logger()`. Do not log via
`logfire.*`: that emits span events and never reaches stdout.

```python
from infinity_observability_sdk import get_logger

logger = get_logger(__name__)
logger.info("run.started", run_id=run.id, backend="devin")
```

Records go to **stdout, one JSON object per line** — the SDK ships no log
transport of its own; Vector tails stdout and forwards to VictoriaLogs. The
shape below is a versioned contract (`LOG_SCHEMA_VERSION`); the tests in
`tests/test_log_contract.py` fail if a processor change alters it.

`LOG_SCHEMA_VERSION` is deliberately **not** emitted on every record — it keeps
records lean, and consumers pin the SDK version instead. Bump it when the key
set or a value format changes incompatibly.

### Record schema

| Key | Always present | Format | Notes |
|---|---|---|---|
| `timestamp` | yes | ISO-8601 UTC, e.g. `2026-08-01T05:00:48.496049Z` | always UTC |
| `level` | yes | lowercase (`info`, `warning`, `error`, …) | |
| `event` | yes | short dotted string, e.g. `run.started` | first positional arg |
| `service` | yes | `<business_unit>.<service>` | set by the SDK |
| `logger` | yes | logger name, e.g. `gravity_api.modules.reports` | |
| `trace_id` | in-span only | 32 lowercase hex chars (`032x`) | Grafana derived field |
| `span_id` | in-span only | 16 lowercase hex chars (`016x`) | Grafana derived field |
| `exception` | on `.exception()` / `exc_info` | one string field holding the whole traceback | |
| `run_id`, `task_id`, `actor_id` | inside `agent_run_context()` | string | EvidenceBundle join keys |
| *your keys* | — | JSON scalars | snake_case |

Rules:

- Keys are `snake_case`; event names are dotted and lowercase.
- The keys above are **reserved** (`RESERVED_LOG_KEYS`) — the processor chain
  owns them and overwrites any application-supplied value of the same name.
- Tracebacks are collapsed into the single `exception` string, so a record never
  spans more than one line and Vector needs no multiline stitching.
- The trace/span ID hex widths are fixed; Grafana derived fields and LogsQL
  queries match on them.
- `ConsoleRenderer` is selected **only** when `ENVIRONMENT` is exactly
  `development` (case-insensitive). Every other value — unset, `staging`,
  `production`, or a typo — renders JSON.

## Agent runs

Wrap an agent run to emit the standard `agent.run` span and bind run-scoped log
context:

```python
from infinity_observability_sdk import agent_run_context, get_logger

logger = get_logger(__name__)

with agent_run_context(
    run.id,
    task_id=task.id,
    actor_id=actor.id,
    backend=run.backend_type,
    systems_context_hash=snapshot.systems_context_hash,
    context_hash=pack.content_hash,
):
    logger.info("run.launched")   # carries run_id / task_id / actor_id
```

Span attributes: `agent.run_id`, `agent.task_id`, `agent.actor_id`,
`agent.backend`, `agent.systems_context_hash`, `agent.context_hash`,
`agent.prompt_template_version`, `env`. Optional identifiers are omitted rather
than emitted as `None`; extra keyword arguments become span attributes verbatim.
Pass `span_name=` for a more specific name from the span vocabulary (e.g.
`agent.run.launch`).

`run_id` / `task_id` / `actor_id` are also bound into the structlog contextvars
for the duration of the block, so every log line written inside the run — via
`get_logger()` or the stdlib bridge — carries them as join keys independent of
`trace_id`. Previous values are restored on exit.

## Agent context (legacy)

The AI-wattage span API, frozen for back-compat. New code should use
`agent_run_context()`.

Wrap AI agent runs to emit a structured span with cost and identity metadata:

```python
from infinity_observability_sdk import agent_context

with agent_context(
    agent_employee_equivalent="data_engineer",
    hourly_rate=75.0,
    agent_name="my_agent",
    task_description="summarise quarterly report",
    task_instance_identifier="run-abc-123",
    approximate_person_hours=2.0,
    business_unit_name="my_bu",
    service_name="my_service",
) as span:
    # your agent logic here
    ...
```

Extra keyword arguments are forwarded as span attributes.

## Django integration

Call `instrument_django()` **before** the ASGI/WSGI app is loaded — typically at the top of `core/asgi.py`:

```python
from infinity_observability_sdk import configure_observability, ObservabilityConfig, instrument_django

configure_observability(ObservabilityConfig(
    business_unit_name="my_bu",
    service_name="my_api",
))
instrument_django()
```

Requires `infinity-observability-sdk[django]`.

## Standalone logging

For services that only need structured logging without full OTel tracing:

```python
from infinity_observability_sdk import configure_logging, get_logger

configure_logging(service_name="my_bu.my_service", log_level="INFO")
logger = get_logger(__name__)
```

The emitted records follow the same [logging contract](#logging-contract).

## Development

```bash
uv sync --dev   # install all dependencies
make test       # run tests
make lint       # ruff lint
make ci         # full CI suite (lint, format check, tests, build, package check)
```

## Release cycle

Releases are **fully automated** via [python-semantic-release](https://python-semantic-release.readthedocs.io/) on every merge to `main`. No manual version bumps or GitHub Releases are needed.

### How it works

1. **Every merge to `main`** triggers the release workflow, which first runs the full CI suite (lint, format, tests, build).
2. `python-semantic-release` analyses commit messages since the last release using [Conventional Commits](https://www.conventionalcommits.org/) to determine whether a release is warranted and what the version bump should be.
3. If a release is warranted, it creates and pushes a `vX.Y.Z` tag. **Nothing is committed back to `main`** — the org ruleset requires every commit on `main` to arrive through a pull request, so the tag is the only thing the release job writes.
4. The same job builds the package (the version comes from the tag via `hatch-vcs`), publishes to PyPI via OIDC (no stored secrets), and creates the GitHub Release with semantic-release-generated notes.

Because the version lives in the git tag, `pyproject.toml` has no `version` field and `CHANGELOG.md` is frozen at v0.5.2 — release notes live on the [Releases](https://github.com/infinity-constellation/infinity-observability-sdk/releases) page.

### Version bump rules

| Commit type | Example | Bump |
|---|---|---|
| `fix:`, `perf:` | `fix(sdk): handle missing env var` | patch |
| `feat:` | `feat(sdk): add configure_metrics()` | minor |
| `feat!:` or `BREAKING CHANGE:` footer | `feat(sdk)!: remove logfire hard dep` | major |
| `docs:`, `chore:`, `refactor:`, etc. | `docs: update README` | none |

> While the version is `0.x`, breaking changes produce a **minor** bump rather than jumping to `1.0.0`.

### Commit message format

```
<type>(<scope>): <short summary>

[optional body]

[optional footer]
```

All commit messages merged to `main` should follow this format. PR titles are used as the squash-merge commit message and must follow the same convention.
