DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

Translating LLM Telemetry Between OpenInference and OTel GenAI with Rust

The spans were there. The queries returned nothing.

I was tracing an LLM pipeline with Phoenix. Phoenix uses OpenInference semconv, so every span came out with attributes like llm.model_name, llm.token_count.prompt, and llm.provider. The traces looked clean in the Phoenix UI.

Then I tried to feed those same spans into Honeycomb. Honeycomb follows OTel GenAI convention. So it expected gen_ai.request.model, gen_ai.usage.input_tokens, gen_ai.system. The raw spans landed fine. The queries returned nothing because none of the attribute names matched.

I was not doing anything wrong. Two observability ecosystems grew up around LLM tracing at the same time. OpenInference came from the ML observability crowd: Arize, Phoenix, LlamaIndex. OTel GenAI came from the OpenTelemetry project. Both are real, both are used in production, and the attribute names overlap on concept but diverge completely on spelling.

The fix should be mechanical. Here is the model name. Rename it. That turned into otel-genai-bridge-rs.


The shape of the fix

You give the bridge a HashMap<String, serde_json::Value> representing the attributes on a span. You ask it to translate in one direction. It gives back a new map with renamed keys.

use otel_genai_bridge_rs::{translate, Direction};
use std::collections::HashMap;
use serde_json::json;

let mut attrs: HashMap<String, serde_json::Value> = HashMap::new();
attrs.insert("llm.model_name".into(), json!("claude-sonnet-4-6"));
attrs.insert("llm.token_count.prompt".into(), json!(512));
attrs.insert("llm.token_count.completion".into(), json!(128));

let translated = translate(&attrs, Direction::OpenInferenceToOtel);

assert_eq!(translated["gen_ai.request.model"], json!("claude-sonnet-4-6"));
assert_eq!(translated["gen_ai.usage.input_tokens"], json!(512));
assert_eq!(translated["gen_ai.usage.output_tokens"], json!(128));
Enter fullscreen mode Exit fullscreen mode

Going the other way is the same call with a different direction:

let translated = translate(&attrs, Direction::OtelToOpenInference);
Enter fullscreen mode Exit fullscreen mode

Keys the bridge does not know how to translate are passed through unchanged. You do not lose data.


What it does NOT do

  • It does not wrap the OTel SDK. There is no TracerProvider here, no Span object, no exporter.
  • It does not mutate spans in flight. You call it, you get a new map, you decide what to do with it.
  • It does not validate that the values are the right type for the target convention.
  • It does not handle nested resource attributes or scope metadata. It operates on the flat span attribute map only.

Inside the lib: raw attribute map design

The no-SDK decision was deliberate. Wrapping the OTel SDK would mean you take a dependency on a specific SDK version, a specific runtime (sync vs async), and a specific span type. If your pipeline already has an exporter, a processor, or a custom span builder, a wrapped SDK adds friction.

Raw attribute maps have no opinion. You can call the bridge:

  • Before writing to a custom exporter
  • In a span post-processor
  • In a batch transform step when migrating historical data
  • In a test, to assert that your instrumentation produces the expected attributes in both conventions

The core of the library is a static translation table. Each entry is a pair: the OpenInference key and the OTel GenAI key. The translate function iterates the input map, looks each key up in the table for the requested direction, and writes either the translated key or the original key into the output map.

// Simplified to show the shape
static MAPPINGS: &[(&str, &str)] = &[
    ("llm.model_name",            "gen_ai.request.model"),
    ("llm.provider",              "gen_ai.system"),
    ("llm.token_count.prompt",    "gen_ai.usage.input_tokens"),
    ("llm.token_count.completion","gen_ai.usage.output_tokens"),
    ("llm.invocation_parameters", "gen_ai.request.model_config"),
    // ...
];
Enter fullscreen mode Exit fullscreen mode

Both translation directions derive from the same table. There is no separate reverse table to maintain.


When this is useful

You instrument with LlamaIndex or Phoenix and ship spans to a standard OTel backend. LlamaIndex emits OpenInference attributes out of the box. If your backend expects OTel GenAI names for its dashboards or alerts to fire, run the bridge in your exporter or processor.

You are migrating from one convention to the other. You have historical spans in object storage tagged with OpenInference keys. You need to backfill them as OTel GenAI for a new analytics setup. The bridge is a pure function. Wrap it in whatever batch runner you already have.

You are writing tests for instrumented code. Your span-attribute assertions can check both conventions without duplicating logic. Translate once, assert once.

You are building an exporter or collector plugin. Accept spans in either convention, normalize to one before forwarding. The bridge handles the rename step without requiring the full OTel SDK in your dependency tree.


When NOT to use it

If you are writing new instrumentation from scratch, pick one convention and stay with it. The bridge is for the boundary problem, not for avoiding the choice.

If you need to handle OTel Resource attributes, scope names, or schema URLs, this library does not cover those. It is scoped to the flat span attribute bag.

If your pipeline is already inside the OTel collector, use the collector's transform processor instead. That is the right place for attribute renaming when you already have the OTel machinery running.


Install

Add to Cargo.toml:

[dependencies]
otel-genai-bridge-rs = "0.1"
Enter fullscreen mode Exit fullscreen mode

Source: github.com/MukundaKatta/otel-genai-bridge-rs


Siblings in the observability stack

Lib Boundary Repo
agenttrace-rs Aggregates LLM calls into runs with cost and latency, produces the spans this library translates MukundaKatta/agenttrace-rs
agentsnap-rs Jest-style snapshots for full agent traces, useful for asserting attribute shape over time MukundaKatta/agentsnap-rs
agenttap Python wire-level capture, intercepts the raw HTTP request before any span is created MukundaKatta/agenttap
otel-genai-bridge-rs This library. Translates attribute names between conventions on the existing span map MukundaKatta/otel-genai-bridge-rs

The layers do not overlap much. agenttap captures what went on the wire. agenttrace-rs aggregates spans into run-level cost and timing. agentsnap-rs snapshots the full trace for regression checks. This bridge sits at the export boundary, just before the spans leave your process.


What is next

The main gap right now is attribute completeness. The initial mapping table covers the core fields: model name, system, token counts, invocation parameters. Richer fields like individual message role attributes, tool call details, and embedding vector dimensions are not yet mapped.

A second gap is partial mapping reporting. If you translate a span and some keys do not have a known translation, you get them back unchanged with no signal. Adding an option to return which keys were translated, which were passed through, and which had no known mapping would make it easier to spot instrumentation gaps. Right now you only discover coverage holes by comparing the input and output maps yourself.

A third area worth exploring is a registry mode. Instead of a hard-coded static table, you could register custom mappings at startup. Teams that have added internal attribute namespaces on top of either convention could extend the bridge without forking it. The static table would remain the default, but the API could accept additional mappings as a slice passed into the translate call.

The library ships with no async dependency and no OTel SDK dependency. That constraint stays. The translation table can grow without changing the public API surface, and the no-SDK rule means it stays usable in any Rust project regardless of whether you have the OTel machinery wired up.


Built for the Hermes Agent Challenge. The library is MIT licensed.

Top comments (0)