Repository navigation
feat: add OpenTelemetry bridge for AI observability #306
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
Merged
marandaneto
merged 16 commits into
main
from
posthog-self-driving/featposthog-go-add-otel-bridge-for-ai-c11a1b
Sep 2, 2026
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
5fb0c02
feat: add OpenTelemetry bridge for AI observability
posthog[bot] 0f8865b
fix(otel): cap SpanProcessor batches at the endpoint span limit
posthog[bot] 34d4a60
fix(otel): confirm export before the example reports success
posthog[bot] 2975a81
docs(otel): note ADK Go message content is not forwarded
posthog[bot] 3487e55
feat(otel): warn when spans route through the PostHog AI Gateway
posthog[bot] 21733ba
fix(otel): reject invalid hosts instead of silently misdirecting spans
posthog[bot] 1b4cd7c
fix(otel): chunk exports so the Exporter path also honors the span limit
posthog[bot] 2109ad3
docs(otel): use a fresh context for shutdown in the usage example
posthog[bot] 7248d3b
docs(otel): register on the existing provider instead of replacing it
posthog[bot] ec8c65b
fix: address OTel bridge review findings
marandaneto 3ea9373
docs: remove OTel README code snippets
marandaneto c85e334
docs: expand OTel validation example
marandaneto 572412f
fix(otel): address exporter review feedback
marandaneto e9dc7f9
ci: release OTel module with root SDK
marandaneto c0d55ee
ci: pin release tags to version commit
marandaneto ea75c83
fix(otel): resolve the ingest path for hosts carrying a query or fragβ¦
carlos-marchal-ph File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "posthog-go": minor | ||
| --- | ||
|
|
||
| Add the OpenTelemetry bridge for AI observability as an independently installable nested Go module. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| The MIT License (MIT) | ||
|
|
||
| Copyright (c) 2020 PostHog (part of Hiberly Inc) | ||
|
|
||
| Copyright (c) 2016 Segment, Inc. | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # PostHog OpenTelemetry bridge for AI observability | ||
|
|
||
| `posthogotel` forwards OpenTelemetry AI spans to [PostHog AI observability](https://posthog.com/docs/ai-engineering/observability). | ||
|
|
||
| It keeps only spans that follow a known AI semantic convention β a span whose | ||
| name or any attribute key starts with `gen_ai.`, `llm.`, `ai.`, or | ||
| `traceloop.` β and drops every other span. Kept spans go over OTLP/HTTP to the | ||
| PostHog `/i/v0/ai/otel` endpoint with the project API key as a bearer token. | ||
|
|
||
| This is a separate Go module, so the core `posthog-go` SDK does not depend on | ||
| OpenTelemetry. | ||
|
|
||
| ## Usage | ||
|
|
||
| `SpanProcessor` is the recommended integration. Register it on the | ||
| `TracerProvider` your application already owns β rather than replacing the | ||
| global provider with a new one β so your resource, sampler, and existing | ||
| exporters are kept and tracers already handed out (such as ADK Go's) route | ||
| through it. Shut down the processor with a fresh context to flush its buffered | ||
| spans without shutting down the application-owned provider. | ||
|
|
||
| If you don't already have a `TracerProvider`, create one and register the | ||
| processor on it, as [`example/`](example) does. Use `WithHost` for a host other | ||
| than PostHog US cloud. For a framework that accepts only a span exporter, use | ||
| `NewExporter` instead and pair it with your own batch span processor. | ||
|
|
||
| ## Failed generations | ||
|
|
||
| PostHog decides that a generation failed from the OpenTelemetry span status, so set the | ||
| status to the error code when a model call fails. Recording the error on the span is not | ||
| enough on its own: in OpenTelemetry for Go that only adds an exception event and leaves | ||
| the span status unset, so the failed generation reaches PostHog looking successful with an | ||
| empty response. The Python and JavaScript instrumentation sets the status for you, which | ||
| is why this step is specific to Go. Once the status is set, PostHog fills in the error | ||
| message and HTTP status from the recorded exception event. | ||
|
|
||
| ## Google Agent Development Kit (ADK) for Go | ||
|
|
||
| [ADK Go](https://google.golang.org/adk) instruments its agents with | ||
| OpenTelemetry and emits `gen_ai.*` spans on the global tracer provider. Register | ||
| the PostHog span processor on that provider before you run the agent, and the | ||
| agent's `gen_ai.*` spans reach PostHog with no further code. | ||
|
|
||
| Those spans carry the generation's model, token counts, latency, and finish | ||
| reason. They do **not** carry prompt and response message content: ADK Go emits | ||
| message bodies as OpenTelemetry **log records** (event names | ||
| `gen_ai.system.message`, `gen_ai.user.message`, and `gen_ai.choice`), gated | ||
| behind the `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment | ||
| variable, not as span attributes. This bridge forwards spans only, so prompt and | ||
| response fields stay empty for ADK Go generations. | ||
|
|
||
| See [`example/`](example) for a runnable program. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| package posthogotel | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "net/url" | ||
| "strings" | ||
|
|
||
| "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" | ||
| sdktrace "go.opentelemetry.io/otel/sdk/trace" | ||
| ) | ||
|
|
||
| const ( | ||
| // DefaultHost is the PostHog US cloud host used when WithHost is not set. | ||
| DefaultHost = "https://us.i.posthog.com" | ||
|
|
||
| // ingestPath is the PostHog AI observability OTLP endpoint path. | ||
| ingestPath = "/i/v0/ai/otel" | ||
|
|
||
| // maxSpansPerRequest is the maximum number of AI spans the PostHog AI | ||
| // observability endpoint accepts in a single OTLP request. Larger requests | ||
| // are rejected with a non-retryable HTTP 400 and the whole batch is lost, so | ||
| // batches must be capped at this limit. | ||
| maxSpansPerRequest = 100 | ||
| ) | ||
|
|
||
| // errEmptyAPIKey is returned when the project API key is missing. | ||
| var errEmptyAPIKey = errors.New("posthogotel: apiKey must not be empty") | ||
|
|
||
| // errInvalidHost is returned when the configured host is not an absolute http | ||
| // or https URL with a hostname. | ||
| var errInvalidHost = errors.New("posthogotel: host must be an absolute http or https URL, for example https://us.i.posthog.com") | ||
|
|
||
| // config holds the resolved settings for the exporter and the span processor. | ||
| type config struct { | ||
| host string | ||
| // endpoint is the resolved OTLP URL, host joined with ingestPath. | ||
| endpoint string | ||
| } | ||
|
|
||
| // Option configures the exporter or the span processor. | ||
| type Option func(*config) | ||
|
|
||
| // WithHost sets the PostHog host, for example "https://eu.i.posthog.com". | ||
| // An empty or blank value keeps DefaultHost. | ||
| func WithHost(host string) Option { | ||
| return func(c *config) { | ||
| if h := strings.TrimSpace(host); h != "" { | ||
| c.host = h | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func newConfig(opts ...Option) (config, error) { | ||
| c := config{host: DefaultHost} | ||
| for _, opt := range opts { | ||
| opt(&c) | ||
| } | ||
| c.host = strings.TrimRight(c.host, "/") | ||
| endpoint, err := resolveEndpoint(c.host) | ||
| if err != nil { | ||
| return config{}, err | ||
| } | ||
| c.endpoint = endpoint | ||
| return c, nil | ||
| } | ||
|
|
||
| // resolveEndpoint builds the OTLP URL for host and rejects a host that | ||
| // otlptracehttp.WithEndpointURL would silently discard. On a parse failure it | ||
| // keeps its localhost defaults, so spans go nowhere while the request still | ||
| // carries the API key; a scheme-less host such as "us.i.posthog.com" parses but | ||
| // yields an empty endpoint. Requiring an absolute http or https URL with a | ||
| // hostname turns both into an upfront error. | ||
| // | ||
| // ingestPath is joined rather than concatenated. A host that carries a query or | ||
| // fragment, such as "https://us.i.posthog.com?region=eu", would concatenate into | ||
| // a URL whose path is empty, and the exporter would then fall back to the OTLP | ||
| // default "/v1/traces" and send every AI span, with the API key attached, to a | ||
| // path PostHog does not serve. | ||
| func resolveEndpoint(host string) (string, error) { | ||
| u, err := url.Parse(host) | ||
| if err != nil { | ||
| return "", fmt.Errorf("%w: %v", errInvalidHost, err) | ||
| } | ||
| if (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" { | ||
| return "", errInvalidHost | ||
| } | ||
| return u.JoinPath(ingestPath).String(), nil | ||
| } | ||
|
|
||
| // newOTLPExporter builds an OTLP/HTTP exporter that targets the PostHog AI | ||
| // observability endpoint with the project API key as a bearer token. The | ||
| // exporter is wrapped so that no single request exceeds maxSpansPerRequest, | ||
| // which protects both public entry points regardless of the batch size of the | ||
| // span processor that feeds them. | ||
| func newOTLPExporter(ctx context.Context, apiKey string, cfg config) (sdktrace.SpanExporter, error) { | ||
| apiKey = strings.TrimSpace(apiKey) | ||
| exporter, err := otlptracehttp.New(ctx, | ||
| otlptracehttp.WithEndpointURL(cfg.endpoint), | ||
| otlptracehttp.WithHeaders(map[string]string{ | ||
| "Authorization": "Bearer " + apiKey, | ||
| }), | ||
|
marandaneto marked this conversation as resolved.
|
||
| ) | ||
|
posthog[bot] marked this conversation as resolved.
|
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return &chunkingExporter{inner: exporter, limit: maxSpansPerRequest}, nil | ||
| } | ||
|
|
||
| // chunkingExporter splits each ExportSpans batch into requests of at most limit | ||
| // spans. The PostHog AI observability endpoint rejects larger requests with a | ||
| // non-retryable HTTP 400 that discards the whole batch, and nothing below this | ||
| // module splits a batch: the OTLP exporter turns whatever slice it receives | ||
| // into exactly one request. Chunking here caps every request for both the | ||
| // SpanProcessor and the caller-supplied Exporter path. | ||
| type chunkingExporter struct { | ||
| inner sdktrace.SpanExporter | ||
| limit int | ||
| } | ||
|
|
||
| var _ sdktrace.SpanExporter = (*chunkingExporter)(nil) | ||
|
|
||
| // ExportSpans forwards spans to the inner exporter in slices of at most limit. | ||
| func (e *chunkingExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error { | ||
| for start := 0; start < len(spans); start += e.limit { | ||
| end := start + e.limit | ||
| if end > len(spans) { | ||
| end = len(spans) | ||
| } | ||
| if err := e.inner.ExportSpans(ctx, spans[start:end]); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // Shutdown shuts down the inner exporter. | ||
| func (e *chunkingExporter) Shutdown(ctx context.Context) error { | ||
| return e.inner.Shutdown(ctx) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| // Package posthogotel forwards OpenTelemetry AI spans to PostHog AI observability. | ||
| // | ||
| // It keeps only spans that follow a known AI semantic convention. A span | ||
| // qualifies when its name or any of its attribute keys starts with one of | ||
| // "gen_ai.", "llm.", "ai.", or "traceloop.". Every other span is dropped. | ||
| // Kept spans go over OTLP/HTTP to the PostHog "/i/v0/ai/otel" endpoint with the | ||
| // project API key in an Authorization: Bearer header. | ||
| // | ||
| // The package offers two integrations: | ||
| // | ||
| // - SpanProcessor is the recommended integration. It filters spans, batches | ||
| // the AI spans, and exports them. Register it with | ||
| // TracerProvider.RegisterSpanProcessor (or the WithSpanProcessor option). | ||
| // | ||
| // - Exporter is for setups that supply their own span processor, or | ||
| // frameworks that accept only a span exporter. It filters spans and | ||
| // delegates the AI spans to an OTLP/HTTP exporter. | ||
| // | ||
| // This is a separate Go module so that the core posthog-go SDK does not depend | ||
| // on OpenTelemetry. | ||
| package posthogotel |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.