Module: Langfuse::OtelAttributes

Defined in:
lib/langfuse/otel_attributes.rb

Overview

Serialization layer that converts Langfuse domain models to OpenTelemetry span attributes format

This module provides methods to convert user-friendly Langfuse attribute objects into the internal OpenTelemetry attribute format required by the span processor.

rubocop:disable Metrics/ModuleLength

Examples:

Converting trace attributes

attrs = Langfuse::Types::TraceAttributes.new(
  name: "user-checkout-flow",
  user_id: "user-123",
  tags: ["checkout", "payment"],
  metadata: { version: "2.1.0" }
)
otel_attrs = Langfuse::OtelAttributes.create_trace_attributes(attrs)
span.set_attributes(otel_attrs)

Converting observation attributes

attrs = Langfuse::Types::GenerationAttributes.new(
  model: "gpt-4",
  input: { messages: [...] },
  usage_details: { prompt_tokens: 100 }
)
otel_attrs = Langfuse::OtelAttributes.create_observation_attributes("generation", attrs)
span.set_attributes(otel_attrs)

Constant Summary collapse

TRACE_NAME =

Trace attributes

"langfuse.trace.name"
TRACE_USER_ID =

TRACE_USER_ID and TRACE_SESSION_ID are without langfuse prefix because they follow OpenTelemetry semantic conventions

"user.id"
TRACE_SESSION_ID =
"session.id"
TRACE_INPUT =
"langfuse.trace.input"
TRACE_OUTPUT =
"langfuse.trace.output"
TRACE_METADATA =
"langfuse.trace.metadata"
TRACE_TAGS =
"langfuse.trace.tags"
TRACE_PUBLIC =
"langfuse.trace.public"
OBSERVATION_TYPE =

Observation attributes

"langfuse.observation.type"
OBSERVATION_INPUT =
"langfuse.observation.input"
OBSERVATION_OUTPUT =
"langfuse.observation.output"
OBSERVATION_METADATA =
"langfuse.observation.metadata"
OBSERVATION_LEVEL =
"langfuse.observation.level"
OBSERVATION_STATUS_MESSAGE =
"langfuse.observation.status_message"
OBSERVATION_MODEL =
"langfuse.observation.model.name"
OBSERVATION_MODEL_PARAMETERS =
"langfuse.observation.model.parameters"
OBSERVATION_USAGE_DETAILS =
"langfuse.observation.usage_details"
OBSERVATION_COST_DETAILS =
"langfuse.observation.cost_details"
OBSERVATION_PROMPT_NAME =
"langfuse.observation.prompt.name"
OBSERVATION_PROMPT_VERSION =
"langfuse.observation.prompt.version"
OBSERVATION_COMPLETION_START_TIME =
"langfuse.observation.completion_start_time"
VERSION =

Common attributes

"langfuse.version"
RELEASE =
"langfuse.release"
ENVIRONMENT =
"langfuse.environment"
MAX_TAG_LENGTH =

Validation limits

200

Class Method Summary collapse

Class Method Details

.add_prompt_attributes(otel_attributes, prompt) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Adds prompt attributes if prompt is present and not a fallback

rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

Parameters:

  • otel_attributes (Hash)

    Attributes hash to modify

  • prompt (Hash, Object, nil)

    Prompt hash or object



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/langfuse/otel_attributes.rb', line 303

def self.add_prompt_attributes(otel_attributes, prompt)
  return unless prompt

  # Handle hash-like prompts
  if prompt.is_a?(Hash) || prompt.respond_to?(:[])
    return if prompt[:is_fallback] || prompt["is_fallback"]

    otel_attributes[OBSERVATION_PROMPT_NAME] = prompt[:name] || prompt["name"]
    otel_attributes[OBSERVATION_PROMPT_VERSION] = prompt[:version] || prompt["version"]
  # Handle objects with name/version methods (already converted in Trace#generation)
  elsif prompt.respond_to?(:name) && prompt.respond_to?(:version)
    otel_attributes[OBSERVATION_PROMPT_NAME] = prompt.name
    otel_attributes[OBSERVATION_PROMPT_VERSION] = prompt.version
  end
end

.build_observation_base_attributes(type, get_value, mask: nil) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds base observation attributes (without prompt)

Parameters:

  • type (String)

    Observation type

  • get_value (Proc)

    Lambda to get values from attributes hash

  • mask (#call, nil) (defaults to: nil)

    Mask callable applied to input, output, and metadata

Returns:

  • (Hash)

    Base observation attributes



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/langfuse/otel_attributes.rb', line 276

def self.build_observation_base_attributes(type, get_value, mask: nil)
  input, output,  = mask_payload_fields(get_value, mask: mask)

  {
    OBSERVATION_TYPE => type,
    OBSERVATION_LEVEL => get_value.call(:level),
    OBSERVATION_STATUS_MESSAGE => get_value.call(:status_message),
    VERSION => get_value.call(:version),
    OBSERVATION_INPUT => serialize(input),
    OBSERVATION_OUTPUT => serialize(output),
    OBSERVATION_MODEL => get_value.call(:model),
    OBSERVATION_USAGE_DETAILS => serialize(get_value.call(:usage_details)),
    OBSERVATION_COST_DETAILS => serialize(get_value.call(:cost_details)),
    OBSERVATION_COMPLETION_START_TIME => serialize(get_value.call(:completion_start_time)),
    OBSERVATION_MODEL_PARAMETERS => serialize(get_value.call(:model_parameters)),
    ENVIRONMENT => get_value.call(:environment),
    **(, OBSERVATION_METADATA)
  }
end

.create_observation_attributes(type, attrs, mask: nil) ⇒ Hash

Creates OpenTelemetry attributes from Langfuse observation attributes

Converts user-friendly observation attributes into the internal OpenTelemetry attribute format required by the span processor.

Examples:

attrs = Langfuse::Types::GenerationAttributes.new(
  model: "gpt-4",
  input: { messages: [...] },
  usage_details: { prompt_tokens: 100 }
)
otel_attrs = Langfuse::OtelAttributes.create_observation_attributes("generation", attrs)

Parameters:

  • type (String)

    Observation type (e.g., “generation”, “span”, “event”)

  • attrs (Types::SpanAttributes, Types::GenerationAttributes, Hash)

    Observation attributes

  • mask (#call, nil) (defaults to: nil)

    Mask callable applied to input, output, and metadata

Returns:

  • (Hash)

    OpenTelemetry attributes hash with non-nil values



127
128
129
130
131
132
133
134
135
136
# File 'lib/langfuse/otel_attributes.rb', line 127

def self.create_observation_attributes(type, attrs, mask: nil)
  attrs = attrs.to_h
  get_value = ->(key) { get_hash_value(attrs, key) }

  otel_attributes = build_observation_base_attributes(type, get_value, mask: mask)
  add_prompt_attributes(otel_attributes, get_value.call(:prompt))

  # Remove nil values
  otel_attributes.compact
end

.create_trace_attributes(attrs, mask: nil) ⇒ Hash

Creates OpenTelemetry attributes from Langfuse trace attributes

Converts user-friendly trace attributes into the internal OpenTelemetry attribute format required by the span processor.

Examples:

attrs = Langfuse::Types::TraceAttributes.new(
  name: "user-checkout-flow",
  user_id: "user-123",
  session_id: "session-456",
  tags: ["checkout", "payment"],
  metadata: { version: "2.1.0" }
)
otel_attrs = Langfuse::OtelAttributes.create_trace_attributes(attrs)

Parameters:

  • attrs (Types::TraceAttributes, Hash)

    Trace attributes object or hash

  • mask (#call, nil) (defaults to: nil)

    Mask callable applied to input, output, and metadata

Returns:

  • (Hash)

    OpenTelemetry attributes hash with non-nil values



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/langfuse/otel_attributes.rb', line 84

def self.create_trace_attributes(attrs, mask: nil)
  # Convert to hash if it's a TraceAttributes object
  attrs = attrs.to_h
  get_value = ->(key) { get_hash_value(attrs, key) }

  input, output,  = mask_payload_fields(get_value, mask: mask)

  attributes = {
    TRACE_NAME => get_value.call(:name),
    TRACE_USER_ID => get_value.call(:user_id),
    TRACE_SESSION_ID => get_value.call(:session_id),
    VERSION => get_value.call(:version),
    RELEASE => get_value.call(:release),
    TRACE_INPUT => serialize(input),
    TRACE_OUTPUT => serialize(output),
    TRACE_TAGS => normalize_tags(get_value.call(:tags)),
    ENVIRONMENT => get_value.call(:environment),
    TRACE_PUBLIC => get_value.call(:public),
    **(, TRACE_METADATA)
  }

  # Remove nil values
  attributes.compact
end

.flatten_hash_value(value, key) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Flattens a single hash value (recursively if it’s a hash, serializes otherwise)

Parameters:

  • value (Object)

    Value to flatten

  • key (String)

    Attribute key prefix

Returns:

  • (Hash)

    Flattened attributes hash



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/langfuse/otel_attributes.rb', line 227

def self.flatten_hash_value(value, key)
  if value.is_a?(Hash)
    # Recursively flatten nested hashes
    (value, key)
  elsif value.is_a?(Array)
    # Serialize arrays to JSON
    serialized = serialize(value, preserve_strings: true)
    serialized ? { key => serialized } : {}
  else
    # Convert simple values (strings, numbers, booleans) to strings
    { key => value.to_s }
  end
end

.flatten_metadata(metadata, prefix) ⇒ Hash

Flattens and serializes metadata into OpenTelemetry attribute format

Converts nested metadata objects into dot-notation attribute keys. For example, ‘{ database: { host: ’localhost’ } }‘ becomes `{ ’langfuse.trace.metadata.database.host’: ‘localhost’ }‘.

Examples:

({ user: { id: 123 } }, "langfuse.trace.metadata")
# => { "langfuse.trace.metadata.user.id" => "123" }

Parameters:

  • metadata (Hash, Array, Object, nil)

    Metadata to flatten

  • prefix (String)

    Prefix for attribute keys (e.g., “langfuse.trace.metadata”)

Returns:

  • (Hash)

    Flattened metadata attributes



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/langfuse/otel_attributes.rb', line 200

def self.(, prefix)
  return {} if .nil?

  # Handle non-hash metadata (arrays, primitives, etc.)
  unless .is_a?(Hash)
    serialized = serialize(, preserve_strings: true)
    return serialized ? { prefix => serialized } : {}
  end

  # Recursively flatten hash metadata
  result = {}
  .each do |key, value|
    next if value.nil?

    new_key = "#{prefix}.#{key}"
    result.merge!(flatten_hash_value(value, new_key))
  end

  result
end

.get_hash_value(hash, key) ⇒ Object?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Gets a value from a hash supporting both symbol and string keys Handles false values correctly (doesn’t treat false as nil)

Parameters:

  • hash (Hash)

    Hash to get value from

  • key (Symbol, String)

    Key to look up

Returns:

  • (Object, nil)

    Value from hash or nil



262
263
264
265
266
267
# File 'lib/langfuse/otel_attributes.rb', line 262

def self.get_hash_value(hash, key)
  return hash[key] if hash.key?(key)
  return hash[key.to_s] if hash.key?(key.to_s)

  nil
end

.mask_payload_fields(get_value, mask:) ⇒ Array(Object, Object, Object)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Applies masking to the three payload fields (input, output, metadata)

Parameters:

  • get_value (Proc)

    Lambda to get values from attributes hash

  • mask (#call, nil)

    Mask callable

Returns:

  • (Array(Object, Object, Object))

    Masked [input, output, metadata]



247
248
249
250
251
252
253
# File 'lib/langfuse/otel_attributes.rb', line 247

def self.mask_payload_fields(get_value, mask:)
  [
    Masking.apply(get_value.call(:input), mask: mask),
    Masking.apply(get_value.call(:output), mask: mask),
    Masking.apply(get_value.call(:metadata), mask: mask)
  ]
end

.normalize_tags(tags) ⇒ Array<String>?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Filters tags to String-only elements within 200-char limit, returns nil if empty or nil

Parameters:

  • tags (Array, nil)

    Raw tags array (each tag must be ≤200 characters; oversized tags are dropped with a warning)

Returns:

  • (Array<String>, nil)

    Filtered tags or nil



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/langfuse/otel_attributes.rb', line 170

def self.normalize_tags(tags)
  return nil if tags.nil?

  logger = Langfuse.configuration.logger
  filtered = tags.select do |t|
    next false unless t.is_a?(String)

    if t.length > MAX_TAG_LENGTH
      logger.warn("Langfuse: Tag exceeds #{MAX_TAG_LENGTH} characters (#{t.length} chars). Dropping.")
      next false
    end
    true
  end
  filtered.empty? ? nil : filtered
end

.serialize(obj, preserve_strings: false) ⇒ String?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Safely serializes an object to JSON string

Examples:

Always JSON-serialize (default)

serialize({ key: "value" }) # => '{"key":"value"}'
serialize("string") # => '"string"'
serialize(nil) # => nil

Preserve strings

serialize("already a string", preserve_strings: true) # => "already a string"
serialize([1, 2, 3], preserve_strings: true) # => "[1,2,3]"

Parameters:

  • obj (Object, nil)

    Object to serialize

  • preserve_strings (Boolean) (defaults to: false)

    If true, preserves strings as-is; if false, JSON-serializes everything including strings

Returns:

  • (String, nil)

    JSON string, original string (if preserve_strings is true), or nil if nil/undefined



154
155
156
157
158
159
160
161
162
163
# File 'lib/langfuse/otel_attributes.rb', line 154

def self.serialize(obj, preserve_strings: false)
  return nil if obj.nil?
  return obj if preserve_strings && obj.is_a?(String)

  begin
    obj.to_json
  rescue StandardError
    nil
  end
end