INSTRUMENTATION

Point your existing OTLP exporter at us and your traces appear

Standard OpenTelemetry ingest, current GenAI semantic conventions and the legacy attributes people actually have in production — with no application change.

OpenTelemetry 8 min read Key figure · 0 lines of application code All posts

If you already instrument your agents with OpenTelemetry, you should not have to rewrite anything to get business-level accountability on top of it. Point your existing OTLP exporter at our collector endpoint and your traces show up — spans, tokens, costs, tool calls and all. No SDK swap, no decorator sprinkled through your codebase, no vendor lock on the way in.

This post covers what we accept, how OTLP concepts map onto ours, and the two conventions problems that bite everyone who tries this.

The endpoint

Standard OTLP over HTTP with a JSON payload, at the path the specification names.

# Any OTLP-compatible exporter. No application change. export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-lense-host/otlp" export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf" export OTEL_EXPORTER_OTLP_HEADERS="authorization=Bearer $LENSE_TOKEN" export OTEL_SERVICE_NAME="claims-agent" # Traces land at POST /otlp/v1/traces and are queryable within seconds.

That is the whole integration for teams already on OTel. If you use OpenLLMetry, OpenInference, the Vercel AI SDK's telemetry, LangChain's OTel callback or a hand-rolled tracer, all of them export OTLP and all of them work.

The mapping that matters

OpenTelemetry has two levels: a trace, and the spans inside it. Agent systems have two levels too, but people name them inconsistently, which is why traces from one tool look wrong in another.

Our mapping is deliberately boring:

OTLP conceptBecomesMeaning
traceId session_id One agent run from start to finish — the whole conversation or task
spanId trace_id One step inside that run — a single model call, tool call or retrieval
parentSpanId parent / causation What directly caused this step, so the run renders as a tree
span.name step name What the step was, shown in the run view
resource attributes service, environment Which deployment produced it

An OTLP trace is a run. An OTLP span is a step. Everything else follows from holding that line consistently.

The naming collision — their spanId becoming our trace_id — is unfortunate but honest. It reflects a real difference: in generic distributed tracing a "trace" is one request; in agent work the unit people care about is the whole run, which may span many requests and several minutes.

The conventions problem

Here is the part that trips people up. The OpenTelemetry GenAI semantic conventions have changed, and most production code in the world is emitting the older attribute names. A collector that only accepts the current spelling will silently drop the token counts of nearly every real exporter.

So we accept both, plus the two vendor prefixes that are widespread in practice.

# Token counts -- all four of these are read gen_ai.usage.input_tokens # current convention gen_ai.usage.prompt_tokens # earlier convention llm.usage.prompt_tokens # OpenLLMetry style traceloop.usage.prompt_tokens # Traceloop style # Provider / model -- all of these are read gen_ai.provider.name # current convention gen_ai.system # earlier convention gen_ai.request.model gen_ai.response.model llm.model_name
Why this matters more than it sounds

A dropped attribute does not raise an error. Your traces arrive, the UI shows them, and the token column reads zero. Teams discover this weeks later when a cost report is obviously wrong. If you are evaluating any tracing backend, send it one span using the older attribute names and check the tokens land. It is a thirty-second test that tells you a lot.

Where the extra layers attach

OTLP gives you the run. It does not have a concept of authority or of a business record changing, because it was designed for distributed systems generally, not for software that is allowed to move money. Those attach alongside, keyed on the same thread.

flowchart LR
    subgraph yours["Your application, unchanged"]
        APP["Agent code"] --> SDK["OTel SDK
any flavour"] end SDK -->|OTLP/HTTP| ING["/otlp/v1/traces"] subgraph lense["InsightLense"] ING --> RUN["Runs and steps
from OTLP"] AUTH["Authority decisions
from your permission layer"] BIZ["Business events
from your commit hook"] RUN --- TH(("correlation_id")) AUTH --- TH BIZ --- TH TH --> Q["One query answers
what happened to this account"] end style ING fill:#efecfe,stroke:#4b34e0 style RUN fill:#efecfe,stroke:#4b34e0 style AUTH fill:#fbede3,stroke:#b4531b style BIZ fill:#e2f5f9,stroke:#0e97b0 style TH fill:#14162b,stroke:#14162b,color:#fff
OTLP carries the run; two small additions carry the accountability. You can adopt the first on day one and the others later — they join on the correlation id, so nothing has to be rebuilt when you add them.

Practically, that means the adoption path is incremental. Repoint your exporter this afternoon and you have run tracing, token accounting and cost. Add a header to your outbound calls and your runs start threading to business references. Add an after_commit hook on the two or three tables that matter and you have a ledger.

Worked example: an existing LangChain service

Say you have a retrieval agent already exporting OTel to a generic backend, and you want business threading without touching agent code.

# 1. Repoint the exporter (env only, no code) OTEL_EXPORTER_OTLP_ENDPOINT=https://lense.internal/otlp # 2. Put the business reference on the root span as a resource or span attribute. # One line, wherever you already start the run. with tracer.start_as_current_span("handle_submission") as span: span.set_attribute("correlation.id", submission_ref) # VS-2027-0832950 result = agent.invoke(payload) # 3. That is it. Runs now group under the submission reference, # so an underwriter searching VS-2027-0832950 finds the agent work # next to the documents and the queue item.

Step 2 is the only code change, it is one line, and it is the line that turns a tracing tool into something an underwriter can use.

What we do not do

Two deliberate non-features, because the alternative is worse.

We do not require our SDK. A proprietary SDK is a migration cost on the way in and a hostage situation on the way out. If you can export OTLP you can leave whenever you like, and that is the point.

We do not silently normalise away your attribute names. Unrecognised attributes are kept on the span rather than dropped, so a custom attribute your team relies on survives the round trip even though we do not interpret it.

What this looks like in InsightLense

After repointing the exporter, runs appear in the run view with the step tree intact and tokens, latency and cost on every step. InsightLense also shows which attribute spelling it matched, which is how you confirm the ingest read your convention rather than silently zeroing it.

Run service = claims-agent ingest: OTLP/HTTP
StepNameModelInOutCostAttribute source
1planclaude-sonnet-4-51,240380$0.019gen_ai.usage.input_tokens
2retrieve_policy— tool —
3assessclaude-sonnet-4-53,110640$0.047llm.usage.prompt_tokens
4post_reserve— tool —
Step 3 arrived on the older attribute spelling and still counted. A backend reading only the current convention would show 0 tokens and $0.00 on that row — no error, no warning, just a cost report that is quietly short.

Once the correlation attribute is on the root span, the same runs also appear under the business reference, which is where the other InsightLense layers attach. That is the whole adoption path: OTLP gives you this view today, and the authority and ledger layers join onto it later without any of it being rebuilt.

Checking it worked

Three things to verify after repointing, in order:

  • Spans arrive and nest correctly. If everything is flat, your exporter is not propagating parentSpanId — usually a context-propagation bug in your own code, not in transport.
  • Token counts are non-zero. If they are zero, you are on an older convention and the backend is only reading the new one. We read both; many do not.
  • Cost is populated. Cost is derived from tokens and model, so a missing model attribute produces zero cost even with correct tokens. See why we derive cost rather than store it.

If all three pass, you have run-level observability with no application rewrite — and the two layers that make it accountable are additive from there.

Where do your agents already act on real records?

Tell us that, and what you would need to prove about those actions to an auditor. We will set up a hands-on walkthrough within two weeks.

Request a demo →