DEV Community

Cover image for OpenInference Dynatrace: send AI traces through the Collector
Span Observer
Span Observer

Posted on Originally published at span.observer

OpenInference Dynatrace: send AI traces through the Collector

Picture the morning after launch: your team’s new assistant calls an LLM, spans are flowing into Dynatrace, and the AI Observability screen is still blank. With OpenInference Dynatrace, the cause is usually mundane. Two vocabularies are talking past each other: the instrumentation writes llm.model_name, while the product reads gen_ai.request.model.

This post starts from that operational headache and works toward a decision: where should the translation happen, and how do you prove it works? My sources are the page Get started with OpenInference and AI Observability (read on 8 November 2026), Arize’s OpenInference specification and a community post. Scope: Dynatrace SaaS only, because nothing is said about Managed.

A confession up front: I have not run this chain on a live environment. Anything that is my own assembly is labelled as « our » or « not executed ».

Why well-formed spans can still say nothing

OpenInference models an AI application as a series of typed spans: openinference.span.kind is LLM, EMBEDDING, AGENT, TOOL, RETRIEVER or GUARDRAIL. They are genuine OpenTelemetry spans, so traces rather than metrics. Only the attribute names differ from the gen_ai.* convention.

For an SRE the cost is practical. With no model name or token counts in the expected place, you cannot track spend or latency per model, nor tie a drift to an SLO. The trace exists, but it tells you little.

OpenInference Dynatrace: three places to translate

Before editing any file, decide where the conversion lives. I see three candidates.

  • In the instrumentor. Some can emit both attribute families when you set OPENINFERENCE_ENABLE_GENAI_SEMCONV=true; the docs mention OpenAI and Cohere. It is the simplest route, but coverage differs from package to package.
  • In the Collector. The genainormalizer processor rewrites attributes before export. One configuration serves every application, and none of them needs a Dynatrace token.
  • In Dynatrace. Possible in principle, but you give up control over what leaves your network, prompts included.

My default is the Collector. It keeps the token in one place, lets you strip content before it leaves, and fills the gaps left by instrumentors. That is the option I detail below.

What to have ready

  • A SaaS environment on a Dynatrace Platform Subscription (DPS), with Traces powered by Grail and OTLP ingestion switched on.
  • A token: a platform token with the openpipeline:traces:ingest scope, or a Classic access token with « Ingest OpenTelemetry traces » (openTelemetryTrace.ingest).
  • Docker and a Collector build that ships genainormalizer: Dynatrace Collector 0.53.1 or later, or Dynatrace Bindplane OTel Collector 1.1.2 or later.
  • A Python application and your provider’s OpenInference package, for instance openinference-instrumentation-openai.

Two variables are enough: DT_ENDPOINT (the base URL, such as https://<environment-id>.live.dynatrace.com, without /api/v2/otlp) and DT_API_TOKEN. Keep the .env file that holds them out of Git.

The Collector file, line by line

The documentation describes the expected behaviour but does not print the full YAML. The file below is therefore our own assembly: receiver and exporter modelled on the official samples, the normaliser block taken from the community post (upstream OpenTelemetry Collector 0.153.0 or later). Check the exact component name in your distribution’s component list.

The 33-line otel-collector-config.yaml file: OTLP HTTP receiver on 0.0.0.0:4318, gen\_ai\_normalizer processor with the openinference source, transform/response\_model, otlp\_http/dynatrace exporter to DT\_ENDPOINT/api/v2/otlp and the traces pipeline

Three details deserve a word.

  1. remove_originals: false. Start by keeping both attribute sets on the same span, so a before-and-after comparison is trivial. The docs flip it to true to keep only gen_ai.*.
  2. transform/response_model. OpenInference has no notion of a « response model ». The processor copies the requested model into gen_ai.response.model; the OTTL statement shown is mine.
  3. Trace pipelines only. According to the community post, attaching this processor to a metrics or logs pipeline stops the Collector from starting.

First run: one container and one question to the model

Start the Collector, then launch a small application. The commands come from the docs, shortened; the image is ghcr.io/dynatrace/dynatrace-otel-collector/dynatrace-otel-collector:0.53.1.

Terminal: source .env, docker run for the otel-collector container with port 4318, the mounted configuration file and DT\_ENDPOINT and DT\_API\_TOKEN, then app.py launched with OTEL\_EXPORTER\_OTLP\_ENDPOINT=http://localhost:4318 and docker logs -f

The docs’ script builds a TracerProvider, an OTLP/HTTP exporter and the OpenAI instrumentor, then asks for a haiku about observability. My variant, not re-verified: leave the exporter without arguments so it reads OTEL_EXPORTER_OTLP_ENDPOINT, as the SDK intends. The application then only knows localhost:4318.

exporter = OTLPSpanExporter()  # reads OTEL_EXPORTER_OTLP_ENDPOINT
tracer_provider.add_span_processor(BatchSpanProcessor(exporter))
OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
Enter fullscreen mode Exit fullscreen mode

A privacy trap: Python instrumentors built on TraceConfig capture prompts and replies by default. To switch that off, set OPENINFERENCE_HIDE_INPUTS, OPENINFERENCE_HIDE_OUTPUTS, OPENINFERENCE_HIDE_INPUT_MESSAGES and OPENINFERENCE_HIDE_OUTPUT_MESSAGES to true.

What the processor can really convert

The docs’ mapping table runs to more than ten rows. The gist: span kind becomes gen_ai.operation.name (chat, embeddings, invoke_agent, execute_tool), the model name becomes gen_ai.request.model, and token counts land in gen_ai.usage.*.

OpenInference to gen\_ai mappings: openinference.span.kind to gen\_ai.operation.name (LLM chat, EMBEDDING embeddings, AGENT invoke\_agent, TOOL execute\_tool), llm.model\_name to gen\_ai.request.model, prompt and completion tokens to gen\_ai.usage, indexed messages to gen\_ai.input.messages and gen\_ai.output.messages

Numbered messages (llm.input_messages.N.*) are stitched into one JSON history in gen_ai.input.messages, provided the instrumentor emits enough of them.

Three checks before you trust the result

  1. At the Collector. Run docker logs -f otel-collector with the debug exporter at detailed verbosity, and look for gen_ai.request.model on each span.
  2. In the interface. Press Ctrl+K, open « AI Observability », then the Explorer tab: model, tokens and content should show up. The same request is listed in Distributed Traces.
  3. With DQL. The docs give no OpenInference-specific example. The first query comes from their RAG tutorial; the second is mine and has not been executed.
fetch spans
| summarize models = collectDistinct(gen_ai.request.model)
| expand models
| sort models

fetch spans
| filter isNotNull(gen_ai.request.model)
| summarize input = sum(gen_ai.usage.input_tokens),
            output = sum(gen_ai.usage.output_tokens),
            by: { gen_ai.request.model }
Enter fullscreen mode Exit fullscreen mode

Nothing on screen? Check the URL first. The Collector appends /api/v2/otlp to DT_ENDPOINT by itself.

Blind spots to know about

  • A transformation cannot rebuild what the instrumentor never emitted.
  • Temperature, top_p, max_tokens, finish reasons, prompt-cache flags and gen_ai.system need your own transform statements; the processor fills gen_ai.provider.name.
  • GUARDRAIL spans have no rich standard attribute set: the value is passed unchanged into gen_ai.operation.name.
  • For Managed, nothing has been verified.

Best practices

  • Run a central Collector: the token lives in one place and can be rotated without touching applications.
  • Scope the token to trace ingestion only.
  • Settle the content question before production: a prompt may carry personal data.
  • Once validated, set remove_originals to true to avoid duplicate attributes.
  • Then tie latency and tokens per model to your objectives, as in the piece on reliability of AI workloads.

To widen the picture: the background in Dynatrace and Arize, then MCP agents, monitoring a coding agent and end-to-end OpenTelemetry. For the attributes themselves: the OpenInference specification and the community’s GenAI Normalizer post.

Frequently asked questions

Is the Collector still worth it if my instrumentor already emits gen_ai.*?

Often, yes. OPENINFERENCE_ENABLE_GENAI_SEMCONV=true adds the gen_ai.* set, but coverage varies. The Collector then acts as a safety net for missing attributes and as a single point to mask content.

Which token, with which scopes?

A platform token with openpipeline:traces:ingest, or a Classic access token with openTelemetryTrace.ingest. In the docs’ scripts the header is Bearer for the former and Api-Token for the latter. Our YAML uses Api-Token: with a platform token, change the header.

What about Dynatrace Managed?

The page I read targets SaaS (DPS, Grail). I cannot claim anything for Managed.

Conclusion

For OpenInference Dynatrace, keep one decision and one habit. The decision: translate in a central Collector. The habit: validate first with remove_originals: false and content masked, then read the spans in the logs, and only afterwards in AI Observability.

span.observer is an independent community blog, not affiliated with Dynatrace LLC or Arize. Trademarks belong to their owners. Tools change quickly: check commands and options against the official documentation.


Originally published on span.observer. French version: https://span.observer/fr/openinference-dynatrace-traces-collector/.

Top comments (0)