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"
IS_APP_ROOT =
"langfuse.internal.is_app_root"
EXPERIMENT_ID =

Experiment attributes

"langfuse.experiment.id"
EXPERIMENT_NAME =
"langfuse.experiment.name"
EXPERIMENT_DESCRIPTION =
"langfuse.experiment.description"
EXPERIMENT_METADATA =
"langfuse.experiment.metadata"
EXPERIMENT_DATASET_ID =
"langfuse.experiment.dataset.id"
EXPERIMENT_ITEM_ID =
"langfuse.experiment.item.id"
EXPERIMENT_ITEM_EXPECTED_OUTPUT =
"langfuse.experiment.item.expected_output"
EXPERIMENT_ITEM_METADATA =
"langfuse.experiment.item.metadata"
EXPERIMENT_ITEM_ROOT_OBSERVATION_ID =
"langfuse.experiment.item.root_observation_id"
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:

  • Attributes hash to modify

  • Prompt hash or object

API:

  • private



315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/langfuse/otel_attributes.rb', line 315

def self.add_prompt_attributes(otel_attributes, prompt)
  return unless prompt
  return if fallback_prompt?(prompt)

  if prompt.is_a?(Hash) || prompt.respond_to?(:[])
    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:

  • Observation type

  • Lambda to get values from attributes hash

  • (defaults to: nil)

    Mask callable applied to input, output, and metadata

Returns:

  • Base observation attributes

API:

  • private



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/langfuse/otel_attributes.rb', line 288

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:

  • Observation type (e.g., "generation", "span", "event")

  • Observation attributes

  • (defaults to: nil)

    Mask callable applied to input, output, and metadata

Returns:

  • OpenTelemetry attributes hash with non-nil values



139
140
141
142
143
144
145
146
147
148
# File 'lib/langfuse/otel_attributes.rb', line 139

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:

  • Trace attributes object or hash

  • (defaults to: nil)

    Mask callable applied to input, output, and metadata

Returns:

  • OpenTelemetry attributes hash with non-nil values



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/langfuse/otel_attributes.rb', line 96

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 to flatten

  • Attribute key prefix

Returns:

  • Flattened attributes hash

API:

  • private



239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/langfuse/otel_attributes.rb', line 239

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 to flatten

  • Prefix for attribute keys (e.g., "langfuse.trace.metadata")

Returns:

  • Flattened metadata attributes



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/langfuse/otel_attributes.rb', line 212

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 to get value from

  • Key to look up

Returns:

  • Value from hash or nil

API:

  • private



274
275
276
277
278
279
# File 'lib/langfuse/otel_attributes.rb', line 274

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:

  • Lambda to get values from attributes hash

  • Mask callable

Returns:

  • Masked [input, output, metadata]

API:

  • private



259
260
261
262
263
264
265
# File 'lib/langfuse/otel_attributes.rb', line 259

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:

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

Returns:

  • Filtered tags or nil

API:

  • private



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/langfuse/otel_attributes.rb', line 182

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:

  • Object to serialize

  • (defaults to: false)

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

Returns:

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

API:

  • private



166
167
168
169
170
171
172
173
174
175
# File 'lib/langfuse/otel_attributes.rb', line 166

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