LangChain.OpenTelemetry.Attributes (LangChain v0.9.5)

Copy Markdown View Source

Builds OpenTelemetry span attribute maps from LangChain telemetry metadata, following a subset of the GenAI Semantic Conventions (v1.40+).

Attribute key constants are defined as string literals because the Hex opentelemetry_semantic_conventions package lags behind the latest spec.

Coverage

This integration emits the following semantic-convention attributes:

  • gen_ai.operation.name, gen_ai.provider.name, gen_ai.output.type ("json" when the model requests structured output, else "text")
  • gen_ai.request.model, gen_ai.response.model
  • server.address, server.port (derived from the model's request endpoint)
  • Request parameters (when the model sets them): gen_ai.request.temperature, gen_ai.request.max_tokens, gen_ai.request.top_p, gen_ai.request.top_k, gen_ai.request.frequency_penalty, gen_ai.request.presence_penalty, gen_ai.request.seed, gen_ai.request.choice.count, gen_ai.request.stream, gen_ai.request.stop_sequences, gen_ai.request.reasoning.level
  • gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, and — best-effort from the provider-specific TokenUsage.rawgen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokens, gen_ai.usage.reasoning.output_tokens
  • gen_ai.response.finish_reasons (best-effort from Message.status)
  • gen_ai.input.messages, gen_ai.output.messages (opt-in — see Config)
  • gen_ai.tool.name, gen_ai.tool.call.id, gen_ai.tool.type, gen_ai.tool.description, gen_ai.tool.call.arguments / gen_ai.tool.call.result (opt-in)
  • gen_ai.agent.name, gen_ai.agent.id, gen_ai.conversation.id (from custom_context)
  • error.type (on failed operations)
  • Any attributes the caller supplies via custom_context[:otel_attributes] — see passthrough_attributes/1

It does not currently emit gen_ai.response.id or a distinct gen_ai.response.model (LangChain keeps no response id and normalizes the response model to the requested one). Treat the output as a useful subset rather than full conformance.

Streaming time-to-first-token is captured, but by LangChain.OpenTelemetry.SpanHandler (as a gen_ai.response.time_to_first_token attribute and a gen_ai.first_token span event) rather than here, because it is derived from a streaming lifecycle event rather than the call metadata this module maps.

See: https://opentelemetry.io/docs/specs/semconv/gen-ai/

Summary

Functions

Coerces a caller-supplied value into something safe to set as a span attribute.

Builds attributes for a chain execution event.

Builds attributes for a chain execution stop event.

Extracts Langfuse-specific attributes from a custom_context map.

Builds attributes for an LLM call start event.

Builds attributes for an LLM call stop event (token usage and response model).

Merges two attribute lists, with override winning on duplicate keys.

Returns the gen_ai.operation.name attribute key.

Extracts caller-supplied span attributes from custom_context[:otel_attributes].

Builds attributes for a tool call start event.

Builds attributes for a tool call stop event.

Functions

attribute_value(value)

@spec attribute_value(term()) :: term()

Coerces a caller-supplied value into something safe to set as a span attribute.

Values the OpenTelemetry spec supports natively — strings, integers, floats, booleans, and homogeneous lists of those — pass through unchanged, so numeric attributes stay numeric and remain filterable as numbers in the backend. Anything else is JSON-encoded, falling back to inspect/1 when it cannot be encoded.

This exists because an uncoerced value is not a loud failure. A nested map has no String.Chars implementation, so the SDK's stringification raises; the span handler traps that exception (it must, since a raising :telemetry handler is detached VM-wide) and the result is that the whole span silently disappears. Every caller-supplied value must pass through here.

nil returns nil so callers can drop the attribute entirely; there is no meaningful "null" span attribute.

chain_start(metadata)

@spec chain_start(map()) :: [{String.t(), term()}]

Builds attributes for a chain execution event.

Sets gen_ai.conversation.id, gen_ai.agent.name, and gen_ai.agent.id from custom_context, extracts Langfuse-specific attributes when present, and applies the caller's :otel_attributes passthrough (see passthrough_attributes/1).

chain_stop(metadata, config)

@spec chain_stop(map(), LangChain.OpenTelemetry.Config.t()) :: [{String.t(), term()}]

Builds attributes for a chain execution stop event.

Extracts the first user message as input and the last assistant message as output so they appear on the trace-level span in Langfuse (and other OTEL backends).

custom_context_attributes(context)

@spec custom_context_attributes(map() | nil) :: [{String.t(), term()}]

Extracts Langfuse-specific attributes from a custom_context map.

Supported keys:

  • :langfuse_trace_name -> langfuse.trace.name
  • :langfuse_user_id -> langfuse.user.id
  • :langfuse_session_id -> langfuse.session.id
  • :langfuse_tags -> langfuse.trace.tags
  • :langfuse_metadata -> langfuse.trace.metadata.* (flattened)

llm_call_start(metadata, config \\ %Config{})

@spec llm_call_start(map(), LangChain.OpenTelemetry.Config.t()) :: [
  {String.t(), term()}
]

Builds attributes for an LLM call start event.

Returns operation name, output type, model, provider, and request-parameter attributes (gen_ai.request.*, sourced from metadata[:request_options]). Input message capture is handled separately by the prompt event handler in SpanHandler.

llm_call_stop(metadata, config \\ %Config{})

@spec llm_call_stop(map(), LangChain.OpenTelemetry.Config.t()) :: [
  {String.t(), term()}
]

Builds attributes for an LLM call stop event (token usage and response model).

When config.capture_output_messages is true and metadata[:result] contains a message, serializes output messages into gen_ai.output.messages.

merge(base, override)

@spec merge(list(), list()) :: list()

Merges two attribute lists, with override winning on duplicate keys.

Attribute lists are keyword-like lists of {key, value} tuples, and the SDK's own de-duplication order is an implementation detail we should not depend on. Resolving collisions here keeps precedence explicit and testable at the two places it matters:

  • caller-supplied :otel_attributes override LangChain-derived values on the same span, because a caller who names a key means it;
  • inherited attributes (see LangChain.OpenTelemetry.SpanHandler) lose to the values an event derives, so an inherited gen_ai.request.model can never overwrite the real per-call model on a chat span.

operation_name_key()

Returns the gen_ai.operation.name attribute key.

passthrough_attributes(arg1)

@spec passthrough_attributes(map() | nil) :: [{String.t(), term()}]

Extracts caller-supplied span attributes from custom_context[:otel_attributes].

:otel_attributes is a reserved key in LLMChain.custom_context: a flat map of attribute name to value that LangChain copies onto the spans it creates.

chain =
  %{llm: llm, messages: messages}
  |> LLMChain.new!()
  |> Map.put(:custom_context, %{
    otel_attributes: %{
      "user.id" => user.id,
      "organization.id" => org.id,
      "myapp.feature" => "support_chat"
    }
  })

The key is reserved and explicit rather than LangChain forwarding all of custom_context, because custom_context routinely holds structs, PIDs, and whole conversation states. Copying it wholesale would produce enormous spans and leak data the caller never intended to export.

Keys may be strings or atoms; values are coerced by attribute_value/1. Entries with an unusable key or a nil value are dropped rather than exported as empty attributes.

Namespace application-specific names (myapp.*) per the semantic conventions. A gen_ai.* key is honoured deliberately, so a caller can fill a convention slot LangChain does not populate itself.

tool_call(metadata, config \\ %Config{})

@spec tool_call(map(), LangChain.OpenTelemetry.Config.t()) :: [{String.t(), term()}]

Builds attributes for a tool call start event.

When config.capture_tool_arguments is true and metadata[:arguments] is present, serializes arguments into gen_ai.tool.call.arguments. Also applies the caller's :otel_attributes passthrough (see passthrough_attributes/1), which reaches tool spans because LLMChain includes custom_context in the tool-call telemetry metadata.

tool_call_stop(metadata, config)

@spec tool_call_stop(map(), LangChain.OpenTelemetry.Config.t()) :: [
  {String.t(), term()}
]

Builds attributes for a tool call stop event.

When config.capture_tool_results is true and metadata[:tool_result] is present, extracts the result content into gen_ai.tool.call.result.