Skip to content

feat(ai): add OpenTelemetry integration for AI span export#494

Open
richardsolomou wants to merge 3 commits intomasterfrom
feat/otel-integration
Open

feat(ai): add OpenTelemetry integration for AI span export#494
richardsolomou wants to merge 3 commits intomasterfrom
feat/otel-integration

Conversation

@richardsolomou
Copy link
Copy Markdown
Member

@richardsolomou richardsolomou commented Apr 9, 2026

Problem

Users instrumenting their AI/LLM applications with OpenTelemetry have no way to route AI spans to PostHog without writing custom glue code. This is the Python equivalent of PostHog/posthog-js#3358.

Changes

Adds a posthog.ai.otel module with two integration patterns:

  • PostHogSpanProcessor (recommended) — self-contained SpanProcessor that wraps BatchSpanProcessor + OTLPSpanExporter, filtering to only forward AI spans to PostHog's OTLP endpoint at /i/v0/ai/otel
  • PostHogTraceExporterSpanExporter that filters AI spans before forwarding, for setups that only accept an exporter (e.g. passed to SimpleSpanProcessor)
  • is_ai_span() — shared predicate matching gen_ai.*, llm.*, ai.*, traceloop.* prefixes on span names and attribute keys

Also adds posthog[otel] optional dependency group for opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http.

Usage

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from posthog.ai.otel import PostHogSpanProcessor

resource = Resource(
    attributes={
        SERVICE_NAME: "my-app",
        "posthog.distinct_id": "user-123",
    }
)
provider = TracerProvider(resource=resource)
provider.add_span_processor(
    PostHogSpanProcessor(api_key="phc_...")
)
trace.set_tracer_provider(provider)

Consistency with #482

Uses the same OTLP endpoint (/i/v0/ai/otel) and auth header format (Authorization: Bearer {api_key}) as the migrated examples in #482.

How did you test this code?

31 unit tests covering span filtering, processor forwarding/dropping, exporter filtering, endpoint configuration, and lifecycle delegation.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

posthog-python Compliance Report

Date: 2026-04-14 11:25:50 UTC
Duration: 159505ms

✅ All Tests Passed!

29/29 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 517ms
Format Validation.Event Has Uuid 1507ms
Format Validation.Event Has Lib Properties 1508ms
Format Validation.Distinct Id Is String 1508ms
Format Validation.Token Is Present 1507ms
Format Validation.Custom Properties Preserved 1508ms
Format Validation.Event Has Timestamp 1507ms
Retry Behavior.Retries On 503 9515ms
Retry Behavior.Does Not Retry On 400 3510ms
Retry Behavior.Does Not Retry On 401 3509ms
Retry Behavior.Respects Retry After Header 9514ms
Retry Behavior.Implements Backoff 23530ms
Retry Behavior.Retries On 500 7502ms
Retry Behavior.Retries On 502 7512ms
Retry Behavior.Retries On 504 7517ms
Retry Behavior.Max Retries Respected 23529ms
Deduplication.Generates Unique Uuids 1497ms
Deduplication.Preserves Uuid On Retry 7516ms
Deduplication.Preserves Uuid And Timestamp On Retry 14518ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 7511ms
Deduplication.No Duplicate Events In Batch 1504ms
Deduplication.Different Events Have Different Uuids 1508ms
Compression.Sends Gzip When Enabled 1508ms
Batch Format.Uses Proper Batch Structure 1507ms
Batch Format.Flush With No Events Sends Nothing 1005ms
Batch Format.Multiple Events Batched Together 1505ms
Error Handling.Does Not Retry On 403 3509ms
Error Handling.Does Not Retry On 413 3507ms
Error Handling.Retries On 408 7510ms

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 9, 2026

Vulnerabilities

No security concerns identified. The API key is passed as a Bearer token in HTTP headers to PostHog's own endpoint, which is the expected pattern. No user-controlled data reaches the exporter endpoint URL.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: posthog/ai/otel/processor.py
Line: 17

Comment:
**`DEFAULT_HOST` duplicated in both modules**

`DEFAULT_HOST = "https://us.i.posthog.com"` is defined identically in both `processor.py` and `exporter.py`, violating the OnceAndOnlyOnce rule. Move it to `spans.py` (or a small `_constants.py`) and import it in both places.

```suggestion
from .spans import is_ai_span, DEFAULT_HOST
```
(after moving the constant to `spans.py`)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: posthog/test/ai/otel/test_spans.py
Line: 15-58

Comment:
**Non-parameterised tests — repo convention violated**

The four name-prefix tests and four attribute-key tests are structurally identical; the only difference is the input string. The repo convention is to use parameterised tests for exactly this pattern. Using `@parameterized.expand` would halve the line count and make adding a new prefix a one-line change.

```python
from parameterized import parameterized

class TestIsAISpan(unittest.TestCase):
    @parameterized.expand([
        ("gen_ai", "gen_ai.chat"),
        ("llm",    "llm.call"),
        ("ai",     "ai.completion"),
        ("traceloop", "traceloop.workflow"),
    ])
    def test_matches_ai_name_prefix(self, _name, span_name):
        self.assertTrue(is_ai_span(_make_span(span_name)))

    @parameterized.expand([
        ("gen_ai",    {"gen_ai.system": "openai"}),
        ("llm",       {"llm.model": "gpt-4"}),
        ("ai",        {"ai.provider": "anthropic"}),
        ("traceloop", {"traceloop.entity.name": "chain"}),
    ])
    def test_matches_ai_attribute_key(self, _name, attrs):
        self.assertTrue(is_ai_span(_make_span("http.request", attrs)))
```
The same pattern applies to the negative cases in `test_rejects_non_ai_name`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: posthog/test/ai/otel/test_exporter.py
Line: 9-13

Comment:
**`_make_span` helper duplicated across all three test files**

The same `_make_span` helper (lines 9-13 here, lines 7-11 in `test_processor.py`, lines 7-11 in `test_spans.py`) is copy-pasted verbatim into every test module. Extract it to a shared `posthog/test/ai/otel/conftest.py` or a small `_helpers.py` so it is expressed once.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(ai): add OpenTelemetry integration ..." | Re-trigger Greptile

@richardsolomou richardsolomou requested a review from a team April 9, 2026 08:49
richardsolomou added a commit that referenced this pull request Apr 14, 2026
…rter

Adopt the new PostHogSpanProcessor API from #494 across all OTEL examples.
Drops the manual SimpleSpanProcessor + OTLPSpanExporter wiring in favor of
the higher-level processor that handles batching and AI span filtering.

Each example now depends on posthog[otel] via a uv path source so it
resolves against the workspace checkout while #494 is unreleased.
Copy link
Copy Markdown
Member Author

richardsolomou commented Apr 14, 2026

@richardsolomou richardsolomou force-pushed the feat/otel-integration branch from ee77800 to 8818397 Compare April 14, 2026 09:40
richardsolomou added a commit that referenced this pull request Apr 14, 2026
…rter

Adopt the new PostHogSpanProcessor API from #494 across all OTEL examples.
Drops the manual SimpleSpanProcessor + OTLPSpanExporter wiring in favor of
the higher-level processor that handles batching and AI span filtering.

Each example now depends on posthog[otel] via a uv path source so it
resolves against the workspace checkout while #494 is unreleased.
Add PostHogSpanProcessor and PostHogTraceExporter that filter
AI-related OTel spans and forward them to PostHog's OTLP endpoint.

Generated-By: PostHog Code
Task-Id: 1ba1f07a-1453-4162-90a8-665958c5fe46
Aligns with the endpoint used by the OTel examples in PR #482.

Generated-By: PostHog Code
Task-Id: 1ba1f07a-1453-4162-90a8-665958c5fe46
Move DEFAULT_HOST to spans.py, extract shared make_span helper to
conftest.py, and parameterize span filter tests.

Generated-By: PostHog Code
Task-Id: 1ba1f07a-1453-4162-90a8-665958c5fe46
@richardsolomou richardsolomou force-pushed the feat/otel-integration branch from 8818397 to 284aba6 Compare April 14, 2026 11:22
richardsolomou added a commit that referenced this pull request Apr 14, 2026
…rter

Adopt the new PostHogSpanProcessor API from #494 across all OTEL examples.
Drops the manual SimpleSpanProcessor + OTLPSpanExporter wiring in favor of
the higher-level processor that handles batching and AI span filtering.

Each example now depends on posthog[otel] via a uv path source so it
resolves against the workspace checkout while #494 is unreleased.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant