# OpenTelemetry LLM analytics installation - Docs

1.  1

    ## Install dependencies

    Required

    **Full working examples**

    The [Node.js](https://github.com/PostHog/posthog-js/tree/main/examples/example-ai-openai) and [Python](https://github.com/PostHog/posthog-python/tree/master/examples/example-ai-openai) OpenAI examples show a complete end-to-end OpenTelemetry setup. Swap the instrumentation for any other `gen_ai.*`\-emitting library to trace a different provider or framework.

    Install the OpenTelemetry SDK, PostHog's OpenTelemetry helper, and an OpenTelemetry instrumentation for the provider you want to trace. The examples below use the OpenAI instrumentation, but any library that emits `gen_ai.*` spans will work.

    PostHog AI

    ### Python

    ```bash
    pip install openai opentelemetry-sdk posthog[otel] opentelemetry-instrumentation-openai-v2
    ```

    ### Node

    ```bash
    npm install openai @posthog/ai @opentelemetry/sdk-node @opentelemetry/resources @opentelemetry/instrumentation-openai
    ```

2.  2

    ## Set up OpenTelemetry tracing

    Required

    Configure OpenTelemetry to export spans to PostHog via the `PostHogSpanProcessor`. The processor only forwards AI-related spans — spans whose name or attribute keys start with `gen_ai.`, `llm.`, `ai.`, or `traceloop.` — and drops everything else. PostHog converts `gen_ai.*` spans into `$ai_generation` events automatically.

    PostHog AI

    ### Python

    ```python
    from opentelemetry import trace
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.resources import Resource, SERVICE_NAME
    from posthog.ai.otel import PostHogSpanProcessor
    from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
    resource = Resource(attributes={
        SERVICE_NAME: "my-app",
        "posthog.distinct_id": "user_123", # optional: identifies the user in PostHog
        "foo": "bar", # custom properties are passed through
    })
    provider = TracerProvider(resource=resource)
    provider.add_span_processor(
        PostHogSpanProcessor(
            api_key="<ph_project_token>",
            host="https://us.i.posthog.com",
        )
    )
    trace.set_tracer_provider(provider)
    OpenAIInstrumentor().instrument()
    ```

    ### Node

    ```typescript
    import { NodeSDK } from '@opentelemetry/sdk-node'
    import { resourceFromAttributes } from '@opentelemetry/resources'
    import { PostHogSpanProcessor } from '@posthog/ai/otel'
    import { OpenAIInstrumentation } from '@opentelemetry/instrumentation-openai'
    const sdk = new NodeSDK({
      resource: resourceFromAttributes({
        'service.name': 'my-app',
        'posthog.distinct_id': 'user_123', // optional: identifies the user in PostHog
        foo: 'bar', // custom properties are passed through
      }),
      spanProcessors: [
        new PostHogSpanProcessor({
          apiKey: '<ph_project_token>',
          host: 'https://us.i.posthog.com',
        }),
      ],
      instrumentations: [new OpenAIInstrumentation()],
    })
    sdk.start()
    ```

    PostHog identifies each event using the `posthog.distinct_id` attribute on the OpenTelemetry **Resource** (with `user.id` as a fallback, then a random UUID if neither is set). Because the Resource applies to every span in a batched export, you only need to set the distinct ID once — there's no need for a `BaggageSpanProcessor` or per-span propagation. Any other Resource or span attributes pass through as event properties.

3.  3

    ## Make an LLM call

    Required

    With the processor and instrumentation wired up, any LLM call made through the instrumented SDK is captured. PostHog receives the emitted `gen_ai.*` span and converts it into an `$ai_generation` event.

    PostHog AI

    ### Python

    ```python
    import openai
    client = openai.OpenAI(api_key="<openai_api_key>")
    response = client.chat.completions.create(
        model="gpt-5-mini",
        messages=[
            {"role": "user", "content": "Tell me a fun fact about hedgehogs"}
        ],
    )
    print(response.choices[0].message.content)
    ```

    ### Node

    ```typescript
    import OpenAI from 'openai'
    const client = new OpenAI({ apiKey: '<openai_api_key>' })
    const response = await client.chat.completions.create({
      model: 'gpt-5-mini',
      messages: [{ role: 'user', content: 'Tell me a fun fact about hedgehogs' }],
    })
    console.log(response.choices[0].message.content)
    ```

    > **Note:** If you want to capture LLM events anonymously, omit the `posthog.distinct_id` resource attribute. See our docs on [anonymous vs identified events](/docs/data/anonymous-vs-identified-events.md) to learn more.

    You can expect captured `$ai_generation` events to have the following properties:

    | Property | Description |
    | --- | --- |
    | $ai_model | The specific model, like gpt-5-mini or claude-4-sonnet |
    | $ai_latency | The latency of the LLM call in seconds |
    | $ai_time_to_first_token | Time to first token in seconds (streaming only) |
    | $ai_tools | Tools and functions available to the LLM |
    | $ai_input | List of messages sent to the LLM |
    | $ai_input_tokens | The number of tokens in the input (often found in response.usage) |
    | $ai_output_choices | List of response choices from the LLM |
    | $ai_output_tokens | The number of tokens in the output (often found in response.usage) |
    | $ai_total_cost_usd | The total cost in USD (input + output) |
    | [[...]](/docs/llm-analytics/generations.md#event-properties) | See [full list](/docs/llm-analytics/generations.md#event-properties) of properties |

4.  4

    ## How attributes map to event properties

    Recommended

    PostHog translates standard OpenTelemetry GenAI semantic convention attributes into the same `$ai_*` event properties our native SDK wrappers emit, so traces look the same in PostHog whether they arrive through OpenTelemetry or a native wrapper. The most common mappings:

    | OpenTelemetry attribute | PostHog event property |
    | --- | --- |
    | gen_ai.response.model (or gen_ai.request.model) | $ai_model |
    | gen_ai.provider.name (or gen_ai.system) | $ai_provider |
    | gen_ai.input.messages | $ai_input |
    | gen_ai.output.messages | $ai_output_choices |
    | gen_ai.usage.input_tokens (or gen_ai.usage.prompt_tokens) | $ai_input_tokens |
    | gen_ai.usage.output_tokens (or gen_ai.usage.completion_tokens) | $ai_output_tokens |
    | server.address | $ai_base_url |
    | telemetry.sdk.name / telemetry.sdk.version | $ai_lib / $ai_lib_version |
    | Span start/end timestamps | $ai_latency (computed in seconds) |
    | Span name | $ai_span_name |

    Additional behavior worth knowing:

    -   **Custom attributes pass through.** Any Resource or span attribute that isn't part of the known mapping is forwarded onto the event as-is, so you can add dimensions like `conversation_id` or `tenant_id` and filter on them in PostHog.
    -   **Trace and span IDs are preserved** as `$ai_trace_id`, `$ai_span_id`, and `$ai_parent_id`, so multi-step traces reconstruct correctly.
    -   **Events are classified by operation.** `gen_ai.operation.name=chat` becomes an `$ai_generation` event; `embeddings` becomes `$ai_embedding`. Spans without a recognized operation become `$ai_span` (or `$ai_trace` if they're the root of a trace).
    -   **Vercel AI SDK, Pydantic AI, and Traceloop/OpenLLMetry** emit their own namespaces (`ai.*`, `pydantic_ai.*`, `traceloop.*`) and PostHog normalizes those to the same `$ai_*` properties.
    -   **Noisy resource attributes are dropped.** OpenTelemetry auto-detected attributes under `host.*`, `process.*`, `os.*`, and `telemetry.*` (except `telemetry.sdk.name` / `telemetry.sdk.version`) don't pollute event properties.

5.  5

    ## Other instrumentations, direct OTLP, and troubleshooting

    Optional

    **Alternative instrumentation libraries.** Any library that emits standard `gen_ai.*` spans (or `ai.*` / `traceloop.*` / `pydantic_ai.*`) works with the setup above. Swap `@opentelemetry/instrumentation-openai` / `opentelemetry-instrumentation-openai-v2` for one of these to broaden provider coverage:

    -   [OpenLIT](https://github.com/openlit/openlit) — single instrumentation that covers many providers, vector DBs, and frameworks.
    -   [OpenLLMetry](https://github.com/traceloop/openllmetry) (Traceloop) — broad provider and framework support in Python and JavaScript.
    -   [OpenInference](https://github.com/Arize-ai/openinference) (Arize) — provider- and framework-specific instrumentations for Python and JavaScript.
    -   [MLflow tracing](https://mlflow.org/docs/latest/llms/tracing/index.html) — if you already run MLflow.

    **Direct OTLP export.** If you run an OpenTelemetry Collector, or want to export from a language that isn't Python or Node.js, point any OTLP/HTTP exporter directly at PostHog's AI ingestion endpoint. PostHog accepts OTLP over HTTP in both `application/x-protobuf` and `application/json`, authenticated with a `Bearer` token. The endpoint is signal-specific (traces only), so use the `OTEL_EXPORTER_OTLP_TRACES_*` variants rather than the general `OTEL_EXPORTER_OTLP_*` ones (the SDK appends `/v1/traces` to the latter and would 404).

    PostHog AI

    ### Environment

    ```bash
    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://us.i.posthog.com/i/v0/ai/otel"
    OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer <ph_project_token>"
    ```

    ### Collector

    ```yaml
    receivers:
      otlp:
        protocols:
          http:
            endpoint: 0.0.0.0:4318
    processors:
      batch:
      memory_limiter:
        check_interval: 5s
        limit_mib: 1500
        spike_limit_mib: 512
    exporters:
      otlphttp/posthog:
        traces_endpoint: "https://us.i.posthog.com/i/v0/ai/otel"
        headers:
          Authorization: "Bearer <ph_project_token>"
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [otlphttp/posthog]
    ```

    **Limits and troubleshooting.**

    -   **Only AI spans are ingested.** Spans whose name and attribute keys don't start with `gen_ai.`, `llm.`, `ai.`, or `traceloop.` are dropped server-side, so it's safe to send a mixed trace stream.
    -   **HTTP only, no gRPC.** The endpoint speaks OTLP over HTTP in either `application/x-protobuf` or `application/json`. If your collector or SDK is configured for gRPC, switch to HTTP.
    -   **Request body is capped at 4 MB.** Large or unbounded traces (for example, long chat histories with base64-encoded images) can exceed this. Use a collector with the `batch` processor to keep individual exports small.
    -   **Missing traces?** Make sure you're pointing at the traces-specific OTLP variable (`OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `traces_endpoint`) rather than the general one, and that your project token is set correctly in the `Authorization: Bearer` header.

6.  ## Verify traces and generations

    Recommended

    *Confirm LLM events are being sent to PostHog*

    Let's make sure LLM events are being captured and sent to PostHog. Under **LLM analytics**, you should see rows of data appear in the **Traces** and **Generations** tabs.

    ![LLM generations in PostHog](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250807_syne_ecd0801880.png)![LLM generations in PostHog](https://res.cloudinary.com/dmukukwp6/image/upload/SCR_20250807_syjm_5baab36590.png)

    [Check for LLM events in PostHog](https://app.posthog.com/llm-analytics/generations)

7.  6

    ## Next steps

    Recommended

    Now that you're capturing AI conversations, continue with the resources below to learn what else LLM Analytics enables within the PostHog platform.

    | Resource | Description |
    | --- | --- |
    | [Basics](/docs/llm-analytics/basics.md) | Learn the basics of how LLM calls become events in PostHog. |
    | [Generations](/docs/llm-analytics/generations.md) | Read about the $ai_generation event and its properties. |
    | [Traces](/docs/llm-analytics/traces.md) | Explore the trace hierarchy and how to use it to debug LLM calls. |
    | [Spans](/docs/llm-analytics/spans.md) | Review spans and their role in representing individual operations. |
    | [Anaylze LLM performance](/docs/llm-analytics/dashboard.md) | Learn how to create dashboards to analyze LLM performance. |

### Community questions

Ask a question

### Was this page useful?

HelpfulCould be better