Metadata-Version: 2.4
Name: latitude-telemetry
Version: 3.0.0a8
Summary: Latitude Telemetry for Python
Project-URL: repository, https://github.com/latitude-dev/latitude-llm/tree/main/packages/telemetry/python
Project-URL: homepage, https://github.com/latitude-dev/latitude-llm/tree/main/packages/telemetry/python#readme
Project-URL: documentation, https://github.com/latitude-dev/latitude-llm/tree/main/packages/telemetry/python#readme
Author-email: Latitude Data SL <hello@latitude.so>
Maintainer-email: Latitude Data SL <hello@latitude.so>
License-Expression: MIT
License-File: LICENSE.md
Requires-Python: <3.15,>=3.11
Requires-Dist: openai-agents==0.15.1
Requires-Dist: openinference-instrumentation-dspy==0.1.33
Requires-Dist: openinference-instrumentation-litellm==0.1.6
Requires-Dist: openinference-instrumentation-openai-agents==1.4.2
Requires-Dist: openinference-semantic-conventions==0.1.25
Requires-Dist: opentelemetry-api==1.38.0
Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.38.0
Requires-Dist: opentelemetry-instrumentation-alephalpha==0.50.1
Requires-Dist: opentelemetry-instrumentation-anthropic==0.50.1
Requires-Dist: opentelemetry-instrumentation-bedrock==0.50.1
Requires-Dist: opentelemetry-instrumentation-cohere==0.50.1
Requires-Dist: opentelemetry-instrumentation-crewai==0.50.1
Requires-Dist: opentelemetry-instrumentation-google-generativeai==0.50.1
Requires-Dist: opentelemetry-instrumentation-groq==0.50.1
Requires-Dist: opentelemetry-instrumentation-haystack==0.50.1
Requires-Dist: opentelemetry-instrumentation-langchain==0.50.1
Requires-Dist: opentelemetry-instrumentation-llamaindex==0.50.1
Requires-Dist: opentelemetry-instrumentation-mistralai==0.50.1
Requires-Dist: opentelemetry-instrumentation-ollama==0.50.1
Requires-Dist: opentelemetry-instrumentation-openai==0.50.1
Requires-Dist: opentelemetry-instrumentation-replicate==0.50.1
Requires-Dist: opentelemetry-instrumentation-sagemaker==0.50.1
Requires-Dist: opentelemetry-instrumentation-threading==0.59b0
Requires-Dist: opentelemetry-instrumentation-together==0.50.1
Requires-Dist: opentelemetry-instrumentation-transformers==0.50.1
Requires-Dist: opentelemetry-instrumentation-vertexai==0.50.1
Requires-Dist: opentelemetry-instrumentation-watsonx==0.50.1
Requires-Dist: opentelemetry-sdk==1.38.0
Requires-Dist: opentelemetry-semantic-conventions-ai==0.4.13
Requires-Dist: pydantic==2.12.5
Requires-Dist: typing-extensions==4.15.0
Description-Content-Type: text/markdown

# Latitude Telemetry for Python

Instrument your AI application and send traces to [Latitude](https://latitude.so). Built on [OpenTelemetry](https://opentelemetry.io/).

## Installation

```sh
pip install latitude-telemetry
```

Requires Python 3.11+.

## Quick Start

### Bootstrap (Recommended)

The fastest way to start tracing your LLM calls. One class sets up everything:

```python
import openai
from openai import OpenAI

from latitude_telemetry import Latitude

client = OpenAI()

latitude = Latitude(
    api_key="your-api-key",
    project="your-project-slug",
    instrumentations={"openai": openai},  # Pass the LLM SDK module you use in app code.
)

# Your LLM calls will now be traced and sent to Latitude
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Hello"}],
)

latitude.shutdown()
```

`instrumentations` takes a dict mapping integration name (`openai`, `anthropic`, …) to the LLM SDK module the consumer imports. Passing the module reference the consumer's own code uses sidesteps a class of import-cache bugs where the SDK could patch a different module instance than the app loads.

**What this does:**

- Creates a complete OpenTelemetry setup
- Registers LLM auto-instrumentation (OpenAI, Anthropic, etc.)
- Configures the Latitude span processor and exporter
- Sets up async context propagation (for passing context through async operations)

**When to use this:** Most applications should start here. It's the simplest path to get LLM observability into Latitude.

**When you might need the advanced setup:**

- You already have OpenTelemetry configured for other backends (Datadog, Sentry, Jaeger)
- You need custom span processing, sampling, or filtering
- You want multiple observability backends receiving the same spans

### Existing OpenTelemetry Setup (Advanced)

If your app already uses OpenTelemetry, add Latitude alongside your existing setup:

```python
import openai
from openai import OpenAI

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from latitude_telemetry import LatitudeSpanProcessor, register_latitude_instrumentations

provider = TracerProvider()
# Add Latitude as an additional processor
provider.add_span_processor(LatitudeSpanProcessor("api-key", "project-slug"))
# Add your other processors (Datadog, console exporter, etc.)

trace.set_tracer_provider(provider)

# Enable LLM auto-instrumentation
register_latitude_instrumentations(
    instrumentations={"openai": openai},
    tracer_provider=provider,
)

# Your LLM calls will now be traced and sent to Latitude
client = OpenAI()
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Hello"}],
)
```

**Important:** `LatitudeSpanProcessor` only exports spans to Latitude. You still need LLM instrumentations to create those spans—use `register_latitude_instrumentations()` or bring your own OTel-compatible LLM instrumentation.

## Using `capture()` for Context and Boundaries

The SDK automatically traces LLM calls when you use auto-instrumentation. However, you may want to add additional context (user ID, session ID, tags, or metadata) to group related spans together.

### What `capture()` Does

`capture()` wraps your code to attach Latitude context to all LLM spans created inside the callback:

- Adds attributes like `user.id`, `session.id`, `latitude.tags`, and `latitude.metadata` to every span
- Creates a named boundary for grouping related traces
- Uses OpenTelemetry's native context API for reliable async propagation

### When to Use It

You don't need `capture()` to get started—auto-instrumentation handles LLM calls automatically. Use `capture()` when you want to:

- **Group traces by user or session** — Track all LLM calls from a specific user or session
- **Add business context** — Tag traces with deployment environment, feature flags, or request IDs
- **Mark agent boundaries** — Wrap an entire agent run or conversation turn with a name and metadata
- **Filter and analyze** — Use tags and metadata to filter traces in the Latitude UI

### Example

```python
import openai

from latitude_telemetry import Latitude, capture

latitude = Latitude(
    api_key="your-api-key",
    project="your-project-slug",
    instrumentations={"openai": openai},
)

# Wrap a request or agent run to add context
capture(
    "handle-user-request",
    lambda: agent.process(user_message),
    {
        "user_id": "user_123",
        "session_id": "session_abc",
        "tags": ["production", "v2-agent"],
        "metadata": {"request_id": "req-xyz", "feature_flag": "new-prompt"},
    },
)

latitude.shutdown()
```

**Important:** `capture()` does not create spans—it only attaches context. The LLM spans are created by the auto-instrumentation. You only need one `capture()` call at the request or agent boundary, not for every internal step.

## Key Concepts

- **`Latitude`** — The primary way to use Latitude. Bootstraps a complete OpenTelemetry setup with LLM auto-instrumentation and the Latitude exporter, attaching to an existing provider when one is already registered.
- **`LatitudeSpanProcessor`** — For advanced use cases where you already have an OpenTelemetry setup. Exports spans to Latitude alongside your existing observability stack.
- **`register_latitude_instrumentations()`** — Registers LLM auto-instrumentations (OpenAI, Anthropic, etc.) when using the advanced setup with your own provider.
- **`capture()`** — Optional. Wraps your code to attach Latitude context (tags, user_id, session_id, metadata) to all spans created inside the callback. Use this when you want to group traces by user, session, or add business context.

**Important:** Auto-instrumentation traces LLM calls without `capture()`. Use `capture()` only when you need to add context or mark boundaries. Wrap the request, job, or agent entrypoint once—you don't need to wrap every internal step.

### Why OpenTelemetry?

Latitude Telemetry is built entirely on OpenTelemetry standards. When you're ready to add other observability tools (Datadog, Sentry, Jaeger, etc.), you can use them alongside Latitude without conflicts:

- **Standard span processors** — `LatitudeSpanProcessor` works with any `TracerProvider`
- **Smart filtering** — Only LLM-relevant spans are exported to Latitude (spans with `gen_ai.*`, `llm.*`, `openinference.*`, or `ai.*` attributes, plus known LLM instrumentation scopes)
- **Compatible with existing instrumentations** — Works alongside HTTP, DB, and other OTel instrumentations
- **No vendor lock-in** — Standard OTLP export, no proprietary wire format

## Public API

```python
from latitude_telemetry import (
    Latitude,
    LatitudeOptions,
    LatitudeSpanProcessor,
    capture,
    register_latitude_instrumentations,
)
```

### `Latitude(**options)`

The primary entry point. Bootstraps a complete OpenTelemetry setup with LLM instrumentations and Latitude export. If an OpenTelemetry provider is already registered, Latitude attaches its span processor to that provider instead of replacing it.

```python
class Latitude:
    def __init__(
        self,
        *,
        api_key: str,
        # Default project for spans. Optional — every `capture()` can override.
        # Sent as the `X-Latitude-Project` header on every export.
        project: str | None = None,
        # DEPRECATED alias for `project`. Still accepted; logs a one-time warning and will be
        # removed in a future release. When both are passed, `project` wins.
        project_slug: str | None = None,
        # Dict mapping integration name → the LLM SDK module the consumer imports.
        # Example: {"openai": openai, "anthropic": anthropic}.
        # Anything else (list, primitive, unknown key, non-dict) raises TypeError at register time.
        instrumentations: InstrumentationsInput | None = None,
        service_name: str | None = None,
        disable_batch: bool = False,
        disable_smart_filter: bool = False,
        should_export_span: Callable[[ReadableSpan], bool] | None = None,
        blocked_instrumentation_scopes: list[str] | None = None,
        disable_redact: bool = False,
        redact: RedactSpanProcessorOptions | None = None,
        exporter: SpanExporter | None = None,
        tracer_provider: TracerProvider | None = None,
    ):
        ...

    provider: TracerProvider
    def flush(self) -> None: ...
    def shutdown(self) -> None: ...
```

`init_latitude()` remains available as a backwards-compatible wrapper that returns `{"provider", "flush", "shutdown"}`.

### `LatitudeSpanProcessor`

Span processor for shared-provider setups. Reads Latitude context from OTel context and stamps attributes onto spans.

```python
class LatitudeSpanProcessor:
    def __init__(
        self,
        api_key: str,
        project: str | None,
        options: LatitudeSpanProcessorOptions | None = None,
    ):
        ...

@dataclass
class LatitudeSpanProcessorOptions:
    disable_redact: bool = False
    disable_batch: bool = False
    disable_smart_filter: bool = False
    should_export_span: Callable[[ReadableSpan], bool] | None = None
    blocked_instrumentation_scopes: tuple[str, ...] = ()
    exporter: SpanExporter | None = None
    service_name: str | None = None
```

### `capture(name, fn, options=None)`

Wraps a function to attach Latitude context to all spans created inside. Uses OpenTelemetry's native context API for scoping.

```python
def capture(
    name: str,
    fn: Callable[[], T],
    options: ContextOptions | None = None,
) -> T:
    ...

# ContextOptions:
# {
#     "name": str | None,        # Override the capture name
#     "user_id": str | None,     # User identifier (session.id attribute)
#     "session_id": str | None,  # Session identifier (user.id attribute)
#     "tags": list[str] | None,  # Tags for filtering traces
#     "metadata": dict | None,   # Arbitrary key-value metadata
#     "project": str | None,     # Route this capture (and child spans) to a specific
#                                # Latitude project, overriding the constructor default.
#     "project_slug": str | None, # DEPRECATED alias for `project`. Still accepted.
# }
```

**Nested `capture()` behavior:**

- `user_id`: last-write-wins
- `session_id`: last-write-wins
- `metadata`: shallow merge
- `tags`: append and dedupe while preserving order

### `register_latitude_instrumentations(instrumentations, tracer_provider)`

Registers LLM auto-instrumentations against a specific tracer provider.

```python
# InstrumentationName = Literal[
#   "openai", "openai-agents", "anthropic", "bedrock", "cohere",
#   "langchain", "llamaindex", "togetherai", "vertexai", "aiplatform",
#   "aleph_alpha", "crewai", "dspy", "google_generativeai", "groq",
#   "haystack", "litellm", "mistralai", "ollama", "replicate",
#   "sagemaker", "transformers", "watsonx",
# ]
# InstrumentationsInput = dict[InstrumentationName, object]

def register_latitude_instrumentations(
    # Dict mapping integration name → the LLM SDK module the consumer imports.
    # Anything else throws at register time.
    instrumentations: InstrumentationsInput,
    tracer_provider: TracerProvider,
) -> None:
    ...
```

## Migrating from `instrumentations=["openai"]` (3.0.0a6 and earlier)

The list-of-strings form is removed with no fallback in `3.0.0a7`. Anything other than a plain dict — including the old string list — **raises `TypeError`** at register time. Migration:

```diff
- from latitude_telemetry import Latitude
+ import openai
+ import anthropic
+ from latitude_telemetry import Latitude

  latitude = Latitude(
      api_key="your-api-key",
      project="your-project-slug",
-     instrumentations=["openai", "anthropic"],
+     instrumentations={"openai": openai, "anthropic": anthropic},
  )
```

## Supported AI Providers

Set the integration's key on the `instrumentations` dict to the LLM SDK module the consumer imports.

| Key                   | PyPI package                | What to pass                                |
| --------------------- | --------------------------- | ------------------------------------------- |
| `openai`              | `openai`                    | `import openai` → `openai`                  |
| `openai-agents`       | `openai-agents`             | `import agents` → `agents`                  |
| `anthropic`           | `anthropic`                 | `import anthropic` → `anthropic`            |
| `bedrock`             | `boto3`                     | `import boto3` → `boto3`                    |
| `cohere`              | `cohere`                    | `import cohere` → `cohere`                  |
| `langchain`           | `langchain-core`            | `import langchain_core` → `langchain_core`  |
| `llamaindex`          | `llama-index`               | `import llama_index` → `llama_index`        |
| `togetherai`          | `together`                  | `import together` → `together`              |
| `vertexai`            | `google-cloud-aiplatform`   | `import vertexai` → `vertexai`              |
| `aiplatform`          | `google-cloud-aiplatform`   | `import google.cloud.aiplatform` → that module |
| `aleph_alpha`         | `aleph-alpha-client`        | `import aleph_alpha_client`                 |
| `crewai`              | `crewai`                    | `import crewai`                             |
| `dspy`                | `dspy-ai`                   | `import dspy`                               |
| `google_generativeai` | `google-generativeai`       | `from google import genai` → `genai`        |
| `groq`                | `groq`                      | `import groq`                               |
| `haystack`            | `haystack-ai`               | `import haystack`                           |
| `litellm`             | `litellm`                   | `import litellm`                            |
| `mistralai`           | `mistralai`                 | `import mistralai`                          |
| `ollama`              | `ollama`                    | `import ollama`                             |
| `replicate`           | `replicate`                 | `import replicate`                          |
| `sagemaker`           | `boto3`                     | `import boto3` → `boto3`                    |
| `transformers`        | `transformers`              | `import transformers`                       |
| `watsonx`             | `ibm-watson-machine-learning` | `import ibm_watsonx_ai`                   |

## Context Options

`capture()` accepts these context options:

| Option       | Type                 | OTel Attribute          | Description                  |
| ------------ | -------------------- | ----------------------- | ---------------------------- |
| `name`       | `str`                | `latitude.capture.name` | Name for the capture context |
| `tags`       | `list[str]`          | `latitude.tags`         | Tags for filtering traces    |
| `metadata`   | `dict[str, Any]`     | `latitude.metadata`     | Arbitrary key-value metadata |
| `session_id` | `str`                | `session.id`            | Group traces by session      |
| `user_id`    | `str`                | `user.id`               | Associate traces with a user |

## Configuration Options

### Smart Filtering

By default, only LLM-relevant spans are exported:

```python
from latitude_telemetry import LatitudeSpanProcessor

processor = LatitudeSpanProcessor(
    "api-key",
    "project-slug",
    LatitudeSpanProcessorOptions(
        disable_smart_filter=True,  # Export all spans
    ),
)
```

### Redaction

PII redaction is enabled by default for security-sensitive attributes only:

**Redacted by default:**

- HTTP authorization headers
- HTTP cookies
- HTTP API key headers (`x-api-key`)
- Database statements

```python
from latitude_telemetry import LatitudeSpanProcessor, RedactSpanProcessorOptions

processor = LatitudeSpanProcessor(
    "api-key",
    "project-slug",
    LatitudeSpanProcessorOptions(
        disable_redact=True,  # Disable all redaction
        redact=RedactSpanProcessorOptions(
            attributes=[r"^password$", r"secret"],  # Add custom patterns
            mask=lambda attr, value: "[REDACTED]",
        ),
    ),
)
```

### Custom Filtering

```python
from latitude_telemetry import LatitudeSpanProcessor, LatitudeSpanProcessorOptions

processor = LatitudeSpanProcessor(
    "api-key",
    "project-slug",
    LatitudeSpanProcessorOptions(
        should_export_span=lambda span: span.attributes.get("custom") is True,
        blocked_instrumentation_scopes=["opentelemetry.instrumentation.fs"],
    ),
)
```

## Environment Variables

| Variable                 | Default                   | Description            |
| ------------------------ | ------------------------- | ---------------------- |
| `LATITUDE_TELEMETRY_URL` | `http://localhost:3002`   | OTLP exporter endpoint |

## Troubleshooting

### Spans not appearing in Latitude

1. **Check API key and project slug** — Must be non-empty strings
2. **Verify instrumentations are registered** — Create `Latitude(...)` before importing or constructing clients when possible, or use `register_latitude_instrumentations()` for manual setups
3. **Flush before exit** — Call `latitude.flush()` or `provider.force_flush()`
4. **Check smart filter** — Only LLM spans are exported by default. Use `disable_smart_filter=True` to export all spans
5. **Ensure `capture()` wraps the code that creates spans** — `capture()` itself doesn't create spans; it only attaches context to spans created by instrumentations

### No spans created inside `capture()`

`capture()` only attaches context. You need:

1. An active instrumentation (e.g., `opentelemetry-instrumentation-openai`)
2. That instrumentation to create spans for the operations inside your callback

### Context not propagating

Ensure you have a functioning OpenTelemetry context manager registered:

```python
from opentelemetry.context import set_global_textmap
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
from opentelemetry.baggage.propagation import W3CBaggagePropagator

set_global_textmap(
    CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()])
)
```

`Latitude(...)` does this automatically when it owns the provider. For shared-provider setups, your app's existing OTel setup should already have this.

## License

MIT
