Module: PlatformSdk::Observability::Langfuse::Recorder

Defined in:
lib/platform_sdk/observability/langfuse/recorder.rb

Constant Summary collapse

MAX_ATTRIBUTE_BYTES =

256KB

262_144
TRUNCATION_SUFFIX =
"…[truncated]"

Class Method Summary collapse

Class Method Details

.build_attributes(model, input, output, prompt_name, prompt_version, usage, provider = nil) ⇒ Object

rubocop:disable Metrics/ParameterLists



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 29

def self.build_attributes(model, input, output, prompt_name, prompt_version, usage, provider = nil)
  attrs = {
    'langfuse.observation.type' => 'generation',
    # `gen_ai.operation.name` is a required attribute in the OTel
    # GenAI semantic conventions. We only emit chat-style
    # completions today, so this is a constant.
    'gen_ai.operation.name' => 'chat',
    'gen_ai.request.model' => coerce_string(model),
    'gen_ai.prompt' => stringify(input),
    'gen_ai.completion' => stringify(output)
  }
  attrs['gen_ai.provider.name'] = coerce_string(provider) if provider
  if prompt_name && prompt_version
    attrs['langfuse.observation.prompt.name'] = coerce_string(prompt_name)
    attrs['langfuse.observation.prompt.version'] = coerce_simple(prompt_version)
  end
  if usage.is_a?(Hash)
    usage = usage.transform_keys(&:to_sym)
    input_tokens = coerce_integer(usage[:input_tokens])
    output_tokens = coerce_integer(usage[:output_tokens])
    attrs['gen_ai.usage.input_tokens'] = input_tokens unless input_tokens.nil?
    attrs['gen_ai.usage.output_tokens'] = output_tokens unless output_tokens.nil?
  end
  attrs.compact
end

.coerce_integer(value) ⇒ Object



69
70
71
72
73
74
75
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 69

def self.coerce_integer(value)
  return nil if value.nil?

  Integer(value)
rescue ArgumentError, TypeError
  nil
end

.coerce_simple(value) ⇒ Object

Pass numerics/booleans through; coerce everything else to plain String.



61
62
63
64
65
66
67
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 61

def self.coerce_simple(value)
  return nil if value.nil?
  return value if value.instance_of?(Integer) || value.instance_of?(Float)
  return value if value.instance_of?(TrueClass) || value.instance_of?(FalseClass)

  String.new(value.to_s)
end

.coerce_string(value) ⇒ Object

rubocop:enable Metrics/ParameterLists



56
57
58
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 56

def self.coerce_string(value)
  Coercions.coerce_string(value)
end

.current_span_active?Boolean

rubocop:enable Metrics/ParameterLists

Returns:

  • (Boolean)


24
25
26
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 24

def self.current_span_active?
  OpenTelemetry::Trace.current_span.context.valid?
end

.record_generation(name:, model:, input:, output:, prompt_name: nil, prompt_version: nil, usage: nil, provider: nil) ⇒ Object

rubocop:disable Metrics/ParameterLists



13
14
15
16
17
18
19
20
21
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 13

def self.record_generation(name:, model:, input:, output:, prompt_name: nil, prompt_version: nil, usage: nil, provider: nil)
  return unless Langfuse.enabled?
  return unless current_span_active?

  tracer = Langfuse.tracer
  tracer.in_span(name, attributes: build_attributes(model, input, output, prompt_name, prompt_version, usage, provider)) do
    # span body intentionally empty — attributes carry the data
  end
end

.stringify(value) ⇒ Object



77
78
79
80
81
82
83
84
85
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 77

def self.stringify(value)
  str = value.is_a?(String) ? value : value.to_json
  truncated = truncate(str)
  # Force a plain String — value.to_json could be a String subclass
  # in some odd cases, and value.is_a?(String) lets subclasses through.
  String.new(truncated)
rescue StandardError
  String.new(truncate(value.to_s))
end

.truncate(str) ⇒ Object



87
88
89
90
91
92
93
# File 'lib/platform_sdk/observability/langfuse/recorder.rb', line 87

def self.truncate(str)
  return str if str.bytesize <= MAX_ATTRIBUTE_BYTES

  # Slice on byte boundary then strip any partial multibyte sequence by encoding-coercing.
  truncated = str.byteslice(0, MAX_ATTRIBUTE_BYTES).scrub('')
  "#{truncated}#{TRUNCATION_SUFFIX}"
end