Skip to main content
This page is the reference for setting Laminar-recognized OpenTelemetry attributes from a non-SDK exporter: foreign-language SDKs, browser code, custom instrumentations, internal trace adapters. If you are using @lmnr-ai/lmnr or lmnr (Python), the SDK already sets these attributes for you. Use this page when you ship OTLP directly to Laminar without the SDK and need the literal wire keys.

How attributes flow into Laminar

Spans arrive at Laminar as standard OTLP. Laminar reads three layers:
  • Resource attributes on the tracer provider (service.name, service.version, deployment.environment).
  • Span attributes on each span: span shape, LLM telemetry, GenAI semconv content.
  • Trace-association attributes, which Laminar lifts off any span in the trace and stores once on the trace record (session, user, metadata, tags, trace type).
Trace-association attributes use an “any-span wins” merge: the first non-empty value Laminar sees during ingest is what the trace gets. The most predictable place to set them is the root span, set them once.

Span-shape attributes

Set on the span you want to control. All keys are namespaced under lmnr.span.*. Setting lmnr.span.type = "LLM" is required for LLM-specific UI and cost rollups to render at all. A span with LLM type but no gen_ai.* content shows up in the LLM section but with an empty conversation panel. See LLM attributes below.

Trace-association attributes

These describe the trace, not the individual span. Set them once on the root span: Laminar lifts them onto the trace record at ingest time. Setting them on every span works but is wasteful. The metadata key shape mirrors what the Laminar SDK emits. To attach {environment: "prod", featureFlag: "new-algo", abVariant: {bucket: 3}} to a trace, set three attributes on the root span:

LLM attributes

Set these on a span where lmnr.span.type = "LLM".

Provider, model, tokens, cost

Cost is calculated from gen_ai.system + gen_ai.request.model + token counts. Without all three, cost stays at zero. Setting the explicit gen_ai.usage.*_cost keys overrides the calculated values. See LLM cost tracking for the supported provider names.

Prompts and completions

Use the OTel GenAI semconv message-array form. This is the convention Laminar recommends for new exporters. Each parts entry is one of {type: "text", content}, {type: "thinking", content}, {type: "tool_call", id, name, arguments}, {type: "tool_call_response", id, response}, {type: "uri", uri}, {type: "blob", blob, mimeType}. Laminar preserves the message shape end-to-end and the frontend renders each part inline.
The indexed gen_ai.prompt.{i}.* / gen_ai.completion.{i}.* shape (older OpenLLMetry / OpenInference convention) is still ingested for backwards compatibility but is deprecated. New exporters should emit the semconv form above.

Tool definitions

The older llm.request.functions.{i}.{name,description,parameters} indexed form is still ingested for backwards compatibility but is deprecated. New exporters should emit gen_ai.tool.definitions.

Where to put each attribute

Resource-level attributes are sent once per batch and apply to every span in that batch; putting service.name on each span instead is wasteful and harder to filter. For trace-association attributes, root-span placement is simpler than every-span placement and produces the same result.

Worked example: a minimal correct trace without the SDK

The example below uses only @opentelemetry/api and @opentelemetry/sdk-trace-*, no @lmnr-ai/lmnr import. The Python snippet uses the same plain OTel APIs. Both build the same trace shape: an outer agent.run span with session metadata, a child llm.chat LLM span with full GenAI attributes, and a child tool.execute TOOL span.
GenAI keys (gen_ai.*) are part of the OpenTelemetry GenAI semantic conventions; everything under lmnr.* is Laminar-specific.

Anti-patterns

  • Setting lmnr.association.properties.session_id on every span. Set it once on the root; Laminar lifts it onto the trace at ingest. Repeating it everywhere wastes attribute bytes.
  • Putting service.name or app version on each span. These are Resource attributes. Set them on the tracer provider’s Resource so they’re emitted once per batch.
  • Setting lmnr.span.type = "EXECUTOR" on application code. EXECUTOR is reserved for auto-instrumentation and the evaluations framework. Use DEFAULT or TOOL.
  • Setting LLM token counts without gen_ai.system + gen_ai.request.model. Cost stays at zero. The provider name and request model are required to look up pricing.
  • Setting lmnr.span.type = "LLM" but omitting prompt and completion attributes. The span renders as an LLM call but the conversation panel is empty. Emit gen_ai.input.messages and gen_ai.output.messages.
  • Passing non-primitive attribute values directly. OTel attribute values are limited to string | number | boolean | string[] | number[] | boolean[]. For lmnr.span.input, lmnr.span.output, per-message content, or non-primitive metadata values, JSON.stringify first.

Transports for /v1/traces

The Laminar cloud (api.lmnr.ai) and self-hosted backends accept three OTLP transports:
  • OTLP/gRPC on :8443 (cloud) / :8001 (self-hosted): the recommended path.
  • OTLP/HTTP+protobuf on :443 (cloud) / :8000 (self-hosted): Content-Type: application/x-protobuf.
  • OTLP/HTTP+JSON on :443 (cloud) / :8000 (self-hosted): Content-Type: application/json. Useful for browser SDKs and other runtimes that don’t have a protobuf encoder available; spec quirks (hex or base64 IDs, decimal-stringified fixed64, enum-name strings) are accepted.
Pick whichever your runtime supports best. Attribute semantics on the wire are identical across transports.

What’s next

OpenTelemetry transport setup

Endpoints, ports, headers, and the gRPC vs HTTP comparison.

Span types

What DEFAULT, LLM, and TOOL render to in the transcript view.

LLM cost tracking

Required gen_ai.* attributes and supported provider names.

Metadata

Trace-level metadata, including the wire key for non-SDK exporters.