langfuse-rb 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -2
- data/lib/langfuse/cache_warmer.rb +1 -1
- data/lib/langfuse/client.rb +8 -2
- data/lib/langfuse/config.rb +55 -2
- data/lib/langfuse/masking.rb +29 -0
- data/lib/langfuse/observations.rb +68 -81
- data/lib/langfuse/otel_attributes.rb +56 -11
- data/lib/langfuse/otel_setup.rb +4 -3
- data/lib/langfuse/prompt_cache.rb +1 -0
- data/lib/langfuse/propagation.rb +16 -24
- data/lib/langfuse/score_client.rb +19 -7
- data/lib/langfuse/span_processor.rb +33 -11
- data/lib/langfuse/trace_id.rb +88 -0
- data/lib/langfuse/types.rb +3 -3
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +82 -43
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bf0fdf8f1b31f237397c90d8db3fc61c40bc8a69e20111a949aa0bdffc8dd3e
|
|
4
|
+
data.tar.gz: b6b83329218d23b3ebd53562a4eb5bb1cd01179f07fdbf5d90b2b1c97fc192ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0bda13a371bcc93d37d4ae17548f04c65f1cc8fdf4982756eebac468d280f5b3a44d51ecb5ed5406e2ffee204a958988b3423860916471d8eb1a9e728fcf51c
|
|
7
|
+
data.tar.gz: a2bebff2e84e94c5fa30b6774c89c35f63ed4d2af214a76348a370ddbd9326ff0c8346fac9b3601e9bf9427bdb56f73a3f309f7eb71d439bd31b70426461ae3d
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-04-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Custom/deterministic trace ID support (#74)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Bump faraday, json, and addressable to patch CVEs (#75)
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
- Align docs with implementation (#70, #76)
|
|
20
|
+
|
|
21
|
+
## [0.6.0] - 2026-03-06
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Tracing payload masking via `Config#mask` (#68)
|
|
25
|
+
- Cost details and usage details support on generations (#61)
|
|
26
|
+
- Client-level `environment` and `release` configuration (#52)
|
|
27
|
+
- Configurable parameters when creating scores (#48)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Prompt cache key defaults unlabeled/unversioned fetches to production, matching JS/Python semantics (#63)
|
|
31
|
+
- Tags sent as native arrays instead of JSON strings on OTel span attributes (#66)
|
|
32
|
+
- Enforce 200-character tag length limit on traces (#67)
|
|
33
|
+
- Score API parity between `Langfuse.create_score` and `Client#create_score` (#49)
|
|
34
|
+
- Corrected misleading YARD docstrings for SWR cache config
|
|
35
|
+
|
|
10
36
|
## [0.5.0] - 2026-02-09
|
|
11
37
|
|
|
12
38
|
### Added
|
|
@@ -54,7 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
54
80
|
- OpenTelemetry-based tracing with OTLP export
|
|
55
81
|
- Distributed caching with Rails.cache backend and stampede protection
|
|
56
82
|
- Prompt management (text and chat) with Mustache templating
|
|
57
|
-
- In-memory caching with TTL and
|
|
83
|
+
- In-memory caching with TTL and bounded expiration-ordered eviction
|
|
58
84
|
- Fallback prompt support
|
|
59
85
|
- Global configuration pattern with `Langfuse.configure`
|
|
60
86
|
|
|
@@ -62,7 +88,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
62
88
|
- Migrated from legacy ingestion API to OTLP endpoint
|
|
63
89
|
- Removed `tracing_enabled` configuration flag (#2)
|
|
64
90
|
|
|
65
|
-
[Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.
|
|
91
|
+
[Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.7.0...HEAD
|
|
92
|
+
[0.7.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.6.0...v0.7.0
|
|
93
|
+
[0.6.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.5.0...v0.6.0
|
|
66
94
|
[0.5.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.4.0...v0.5.0
|
|
67
95
|
[0.4.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.3.0...v0.4.0
|
|
68
96
|
[0.3.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.2.0...v0.3.0
|
|
@@ -85,7 +85,7 @@ module Langfuse
|
|
|
85
85
|
# @example Warm with a different default label
|
|
86
86
|
# results = warmer.warm_all(default_label: "staging")
|
|
87
87
|
#
|
|
88
|
-
# @example Warm without any label (
|
|
88
|
+
# @example Warm without any label (API-determined selection)
|
|
89
89
|
# results = warmer.warm_all(default_label: nil)
|
|
90
90
|
#
|
|
91
91
|
# @example With specific versions for some prompts
|
data/lib/langfuse/client.rb
CHANGED
|
@@ -285,10 +285,13 @@ module Langfuse
|
|
|
285
285
|
#
|
|
286
286
|
# @param name [String] Score name (required)
|
|
287
287
|
# @param value [Numeric, Integer, String] Score value (type depends on data_type)
|
|
288
|
+
# @param id [String, nil] Score ID
|
|
288
289
|
# @param trace_id [String, nil] Trace ID to associate with the score
|
|
290
|
+
# @param session_id [String, nil] Session ID to associate with the score
|
|
289
291
|
# @param observation_id [String, nil] Observation ID to associate with the score
|
|
290
292
|
# @param comment [String, nil] Optional comment
|
|
291
293
|
# @param metadata [Hash, nil] Optional metadata hash
|
|
294
|
+
# @param environment [String, nil] Optional environment
|
|
292
295
|
# @param data_type [Symbol] Data type (:numeric, :boolean, :categorical)
|
|
293
296
|
# @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score
|
|
294
297
|
# @param config_id [String, nil] Optional score config ID
|
|
@@ -304,15 +307,18 @@ module Langfuse
|
|
|
304
307
|
# @example Categorical score
|
|
305
308
|
# client.create_score(name: "category", value: "high", trace_id: "abc123", data_type: :categorical)
|
|
306
309
|
# rubocop:disable Metrics/ParameterLists
|
|
307
|
-
def create_score(name:, value:, trace_id: nil,
|
|
308
|
-
data_type: :numeric, dataset_run_id: nil, config_id: nil)
|
|
310
|
+
def create_score(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_id: nil, comment: nil,
|
|
311
|
+
metadata: nil, environment: nil, data_type: :numeric, dataset_run_id: nil, config_id: nil)
|
|
309
312
|
@score_client.create(
|
|
310
313
|
name: name,
|
|
311
314
|
value: value,
|
|
315
|
+
id: id,
|
|
312
316
|
trace_id: trace_id,
|
|
317
|
+
session_id: session_id,
|
|
313
318
|
observation_id: observation_id,
|
|
314
319
|
comment: comment,
|
|
315
320
|
metadata: metadata,
|
|
321
|
+
environment: environment,
|
|
316
322
|
data_type: data_type,
|
|
317
323
|
dataset_run_id: dataset_run_id,
|
|
318
324
|
config_id: config_id
|
data/lib/langfuse/config.rb
CHANGED
|
@@ -46,10 +46,10 @@ module Langfuse
|
|
|
46
46
|
# @return [Integer] Lock timeout in seconds for distributed cache stampede protection
|
|
47
47
|
attr_accessor :cache_lock_timeout
|
|
48
48
|
|
|
49
|
-
# @return [Boolean] Enable stale-while-revalidate caching (
|
|
49
|
+
# @return [Boolean] Enable stale-while-revalidate caching (requires cache_stale_ttl > 0 to activate)
|
|
50
50
|
attr_accessor :cache_stale_while_revalidate
|
|
51
51
|
|
|
52
|
-
# @return [Integer, Symbol] Stale TTL in seconds (grace period for serving stale data, default: 0
|
|
52
|
+
# @return [Integer, Symbol] Stale TTL in seconds (grace period for serving stale data, default: 0)
|
|
53
53
|
# Accepts :indefinite which is automatically normalized to 1000 years (31,536,000,000 seconds) for practical "never expire" behavior.
|
|
54
54
|
attr_accessor :cache_stale_ttl
|
|
55
55
|
|
|
@@ -68,6 +68,16 @@ module Langfuse
|
|
|
68
68
|
# @return [Symbol] ActiveJob queue name for async processing
|
|
69
69
|
attr_accessor :job_queue
|
|
70
70
|
|
|
71
|
+
# @return [String, nil] Default tracing environment applied to new traces/observations
|
|
72
|
+
attr_accessor :environment
|
|
73
|
+
|
|
74
|
+
# @return [String, nil] Default release identifier applied to new traces/observations
|
|
75
|
+
attr_accessor :release
|
|
76
|
+
|
|
77
|
+
# @return [#call, nil] Mask callable applied to input, output, and metadata before serialization.
|
|
78
|
+
# Receives `data:` keyword argument. nil disables masking.
|
|
79
|
+
attr_accessor :mask
|
|
80
|
+
|
|
71
81
|
# @return [String] Default Langfuse API base URL
|
|
72
82
|
DEFAULT_BASE_URL = "https://cloud.langfuse.com"
|
|
73
83
|
|
|
@@ -107,11 +117,26 @@ module Langfuse
|
|
|
107
117
|
# @return [Integer] Number of seconds representing indefinite cache duration (~1000 years)
|
|
108
118
|
INDEFINITE_SECONDS = 1000 * 365 * 24 * 60 * 60
|
|
109
119
|
|
|
120
|
+
# @return [Array<String>] Common CI environment variables that contain a release SHA
|
|
121
|
+
COMMON_RELEASE_ENV_KEYS = %w[
|
|
122
|
+
RENDER_GIT_COMMIT
|
|
123
|
+
CI_COMMIT_SHA
|
|
124
|
+
CIRCLE_SHA1
|
|
125
|
+
SOURCE_VERSION
|
|
126
|
+
TRAVIS_COMMIT
|
|
127
|
+
GIT_COMMIT
|
|
128
|
+
GITHUB_SHA
|
|
129
|
+
BITBUCKET_COMMIT
|
|
130
|
+
BUILD_SOURCEVERSION
|
|
131
|
+
DRONE_COMMIT_SHA
|
|
132
|
+
].freeze
|
|
133
|
+
|
|
110
134
|
# Initialize a new Config object
|
|
111
135
|
#
|
|
112
136
|
# @yield [config] Optional block for configuration
|
|
113
137
|
# @yieldparam config [Config] The config instance
|
|
114
138
|
# @return [Config] a new Config instance
|
|
139
|
+
# rubocop:disable Metrics/AbcSize
|
|
115
140
|
def initialize
|
|
116
141
|
@public_key = ENV.fetch("LANGFUSE_PUBLIC_KEY", nil)
|
|
117
142
|
@secret_key = ENV.fetch("LANGFUSE_SECRET_KEY", nil)
|
|
@@ -128,10 +153,14 @@ module Langfuse
|
|
|
128
153
|
@batch_size = DEFAULT_BATCH_SIZE
|
|
129
154
|
@flush_interval = DEFAULT_FLUSH_INTERVAL
|
|
130
155
|
@job_queue = DEFAULT_JOB_QUEUE
|
|
156
|
+
@environment = env_value("LANGFUSE_TRACING_ENVIRONMENT")
|
|
157
|
+
@release = env_value("LANGFUSE_RELEASE") || detect_release_from_ci_env
|
|
158
|
+
@mask = nil
|
|
131
159
|
@logger = default_logger
|
|
132
160
|
|
|
133
161
|
yield(self) if block_given?
|
|
134
162
|
end
|
|
163
|
+
# rubocop:enable Metrics/AbcSize
|
|
135
164
|
|
|
136
165
|
# Validate the configuration
|
|
137
166
|
#
|
|
@@ -154,6 +183,8 @@ module Langfuse
|
|
|
154
183
|
validate_swr_config!
|
|
155
184
|
|
|
156
185
|
validate_cache_backend!
|
|
186
|
+
|
|
187
|
+
validate_mask!
|
|
157
188
|
end
|
|
158
189
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
159
190
|
|
|
@@ -223,5 +254,27 @@ module Langfuse
|
|
|
223
254
|
|
|
224
255
|
raise ConfigurationError, "cache_refresh_threads must be positive"
|
|
225
256
|
end
|
|
257
|
+
|
|
258
|
+
def validate_mask!
|
|
259
|
+
return if mask.nil? || mask.respond_to?(:call)
|
|
260
|
+
|
|
261
|
+
raise ConfigurationError, "mask must respond to #call"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def detect_release_from_ci_env
|
|
265
|
+
COMMON_RELEASE_ENV_KEYS.each do |key|
|
|
266
|
+
value = env_value(key)
|
|
267
|
+
return value if value
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
nil
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def env_value(key)
|
|
274
|
+
value = ENV.fetch(key, nil)
|
|
275
|
+
return nil if value.nil? || value.empty?
|
|
276
|
+
|
|
277
|
+
value
|
|
278
|
+
end
|
|
226
279
|
end
|
|
227
280
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Langfuse
|
|
4
|
+
# Central masking chokepoint for tracing payload fields.
|
|
5
|
+
#
|
|
6
|
+
# Applies a user-provided mask callable to input, output, and metadata
|
|
7
|
+
# before serialization. Fail-closed: if the mask raises, the entire
|
|
8
|
+
# field is replaced with {FALLBACK}.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Masking
|
|
12
|
+
# Replacement string used when the mask callable raises
|
|
13
|
+
FALLBACK = "<fully masked due to failed mask function>"
|
|
14
|
+
|
|
15
|
+
# Apply the mask callable to a data value.
|
|
16
|
+
#
|
|
17
|
+
# @param data [Object, nil] The value to mask (input, output, or metadata)
|
|
18
|
+
# @param mask [#call, nil] Callable receiving `data:` keyword; nil disables masking
|
|
19
|
+
# @return [Object] Masked data, original data (when mask is nil/data is nil), or {FALLBACK}
|
|
20
|
+
def self.apply(data, mask:)
|
|
21
|
+
return data if mask.nil? || data.nil?
|
|
22
|
+
|
|
23
|
+
mask.call(data: data)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Langfuse.configuration.logger.warn("Langfuse: Mask function failed: #{e.message}")
|
|
26
|
+
FALLBACK
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -111,13 +111,14 @@ module Langfuse
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# Updates trace-level attributes (user_id, session_id, tags, etc.) for the entire trace.
|
|
114
|
+
# Tags exceeding 200 characters are silently dropped with a warning log.
|
|
114
115
|
#
|
|
115
116
|
# @param attrs [Hash, Types::TraceAttributes] Trace attributes to set
|
|
116
117
|
# @return [self]
|
|
117
118
|
def update_trace(attrs)
|
|
118
119
|
return self unless @otel_span.recording?
|
|
119
120
|
|
|
120
|
-
otel_attrs = OtelAttributes.create_trace_attributes(attrs.to_h)
|
|
121
|
+
otel_attrs = OtelAttributes.create_trace_attributes(attrs.to_h, mask: Langfuse.configuration.mask)
|
|
121
122
|
otel_attrs.each { |key, value| @otel_span.set_attribute(key, value) }
|
|
122
123
|
self
|
|
123
124
|
end
|
|
@@ -132,8 +133,7 @@ module Langfuse
|
|
|
132
133
|
# @yield [observation] Optional block that receives the observation object
|
|
133
134
|
# @return [BaseObservation, Object] The child observation (or block return value if block given)
|
|
134
135
|
def start_observation(name, attrs = {}, as_type: :span, &block)
|
|
135
|
-
#
|
|
136
|
-
# Skip validation to allow unknown types to fall back to Span
|
|
136
|
+
# Skip validation so unknown types fall back to Span in the factory.
|
|
137
137
|
child = Langfuse.start_observation(
|
|
138
138
|
name,
|
|
139
139
|
attrs,
|
|
@@ -141,24 +141,9 @@ module Langfuse
|
|
|
141
141
|
parent_span_context: @otel_span.context,
|
|
142
142
|
skip_validation: true
|
|
143
143
|
)
|
|
144
|
+
return child unless block
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
# Block-based API: auto-ends when block completes
|
|
147
|
-
# Set context and execute block
|
|
148
|
-
current_context = OpenTelemetry::Context.current
|
|
149
|
-
result = OpenTelemetry::Context.with_current(
|
|
150
|
-
OpenTelemetry::Trace.context_with_span(child.otel_span, parent_context: current_context)
|
|
151
|
-
) do
|
|
152
|
-
block.call(child)
|
|
153
|
-
end
|
|
154
|
-
# Only end if not already ended (events auto-end in start_observation)
|
|
155
|
-
child.end unless as_type.to_s == OBSERVATION_TYPES[:event]
|
|
156
|
-
result
|
|
157
|
-
else
|
|
158
|
-
# Stateful API - return observation
|
|
159
|
-
# Events already auto-ended in start_observation
|
|
160
|
-
child
|
|
161
|
-
end
|
|
146
|
+
child.send(:run_in_context, &block)
|
|
162
147
|
end
|
|
163
148
|
|
|
164
149
|
# Sets observation-level input attributes.
|
|
@@ -196,9 +181,10 @@ module Langfuse
|
|
|
196
181
|
# @param level [String] Log level (debug, default, warning, error)
|
|
197
182
|
# @return [void]
|
|
198
183
|
def event(name:, input: nil, level: "default")
|
|
184
|
+
masked_input = Masking.apply(input, mask: Langfuse.configuration.mask)
|
|
199
185
|
attributes = {
|
|
200
|
-
|
|
201
|
-
|
|
186
|
+
OtelAttributes::OBSERVATION_INPUT => masked_input&.to_json,
|
|
187
|
+
OtelAttributes::OBSERVATION_LEVEL => level
|
|
202
188
|
}.compact
|
|
203
189
|
|
|
204
190
|
@otel_span.add_event(name, attributes: attributes)
|
|
@@ -242,7 +228,7 @@ module Langfuse
|
|
|
242
228
|
attrs_hash = attrs.to_h.merge(kwargs)
|
|
243
229
|
|
|
244
230
|
# Use @type instance variable set during initialization
|
|
245
|
-
otel_attrs = OtelAttributes.create_observation_attributes(type, attrs_hash)
|
|
231
|
+
otel_attrs = OtelAttributes.create_observation_attributes(type, attrs_hash, mask: Langfuse.configuration.mask)
|
|
246
232
|
otel_attrs.each { |key, value| @otel_span.set_attribute(key, value) }
|
|
247
233
|
end
|
|
248
234
|
|
|
@@ -259,6 +245,27 @@ module Langfuse
|
|
|
259
245
|
prompt
|
|
260
246
|
end
|
|
261
247
|
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
# Runs the block with this observation as the active OTel span,
|
|
252
|
+
# then ends the span in ensure (events excluded — they auto-end).
|
|
253
|
+
# @api private
|
|
254
|
+
def run_in_context
|
|
255
|
+
parent_ctx = OpenTelemetry::Context.current
|
|
256
|
+
span_ctx = OpenTelemetry::Trace.context_with_span(@otel_span, parent_context: parent_ctx)
|
|
257
|
+
OpenTelemetry::Context.with_current(span_ctx) { yield self }
|
|
258
|
+
ensure
|
|
259
|
+
safe_end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Ends the span, swallowing errors so ensure never masks a block exception.
|
|
263
|
+
# @api private
|
|
264
|
+
def safe_end
|
|
265
|
+
self.end unless @type == OBSERVATION_TYPES[:event]
|
|
266
|
+
rescue StandardError
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
262
269
|
end
|
|
263
270
|
|
|
264
271
|
# General-purpose observation for tracking operations, functions, or logical units of work.
|
|
@@ -291,6 +298,35 @@ module Langfuse
|
|
|
291
298
|
end
|
|
292
299
|
end
|
|
293
300
|
|
|
301
|
+
# Shared setters for observation types that interact with a model (Generation, Embedding).
|
|
302
|
+
# @api private
|
|
303
|
+
module ModelSetters
|
|
304
|
+
# @param value [Hash] Usage hash with token counts
|
|
305
|
+
# @return [void]
|
|
306
|
+
# @deprecated Use #usage_details= instead.
|
|
307
|
+
def usage=(value)
|
|
308
|
+
self.usage_details = value
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# @param value [Hash] Usage details hash (preserves key shape as provided)
|
|
312
|
+
# @return [void]
|
|
313
|
+
def usage_details=(value)
|
|
314
|
+
update_observation_attributes(usage_details: value)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @param value [String] Model name (e.g., "gpt-4", "claude-3-opus")
|
|
318
|
+
# @return [void]
|
|
319
|
+
def model=(value)
|
|
320
|
+
update_observation_attributes(model: value)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# @param value [Hash] Model parameters (temperature, max_tokens, etc.)
|
|
324
|
+
# @return [void]
|
|
325
|
+
def model_parameters=(value)
|
|
326
|
+
update_observation_attributes(model_parameters: value)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
294
330
|
# Observation for LLM calls. Provides methods to set output, usage, and other LLM-specific metadata.
|
|
295
331
|
#
|
|
296
332
|
# @example Block-based API
|
|
@@ -299,7 +335,7 @@ module Langfuse
|
|
|
299
335
|
# gen.input = [{ role: "user", content: "Hello" }]
|
|
300
336
|
# response = call_llm(gen.input)
|
|
301
337
|
# gen.output = response
|
|
302
|
-
# gen.
|
|
338
|
+
# gen.usage_details = { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 }
|
|
303
339
|
# end
|
|
304
340
|
#
|
|
305
341
|
# @example Stateful API
|
|
@@ -315,6 +351,8 @@ module Langfuse
|
|
|
315
351
|
# gen.end
|
|
316
352
|
#
|
|
317
353
|
class Generation < BaseObservation
|
|
354
|
+
include ModelSetters
|
|
355
|
+
|
|
318
356
|
# @param otel_span [OpenTelemetry::SDK::Trace::Span] The underlying OTel span
|
|
319
357
|
# @param otel_tracer [OpenTelemetry::SDK::Trace::Tracer] The OTel tracer
|
|
320
358
|
# @param attributes [Hash, Types::GenerationAttributes, nil] Optional initial attributes
|
|
@@ -329,45 +367,10 @@ module Langfuse
|
|
|
329
367
|
self
|
|
330
368
|
end
|
|
331
369
|
|
|
332
|
-
# @param value [Hash]
|
|
333
|
-
# @return [void]
|
|
334
|
-
def usage=(value)
|
|
335
|
-
return unless @otel_span.recording?
|
|
336
|
-
|
|
337
|
-
# Convert to Langfuse API format (camelCase keys)
|
|
338
|
-
usage_hash = {
|
|
339
|
-
promptTokens: value[:prompt_tokens] || value["prompt_tokens"],
|
|
340
|
-
completionTokens: value[:completion_tokens] || value["completion_tokens"],
|
|
341
|
-
totalTokens: value[:total_tokens] || value["total_tokens"]
|
|
342
|
-
}.compact
|
|
343
|
-
|
|
344
|
-
usage_json = usage_hash.to_json
|
|
345
|
-
@otel_span.set_attribute("langfuse.observation.usage", usage_json)
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# @param value [String] Model name (e.g., "gpt-4", "claude-3-opus")
|
|
370
|
+
# @param value [Hash] Cost details hash (prefer :input, :output, :total for aggregate Langfuse cost metrics)
|
|
349
371
|
# @return [void]
|
|
350
|
-
def
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
@otel_span.set_attribute("langfuse.observation.model", value.to_s)
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
# @param value [Hash] Model parameters (temperature, max_tokens, etc.)
|
|
357
|
-
# @return [void]
|
|
358
|
-
def model_parameters=(value)
|
|
359
|
-
return unless @otel_span.recording?
|
|
360
|
-
|
|
361
|
-
# Convert to Langfuse API format (camelCase keys)
|
|
362
|
-
params_hash = {}
|
|
363
|
-
value.each do |k, v|
|
|
364
|
-
key_str = k.to_s
|
|
365
|
-
# Convert snake_case to camelCase
|
|
366
|
-
camel_key = key_str.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
|
|
367
|
-
params_hash[camel_key] = v
|
|
368
|
-
end
|
|
369
|
-
params_json = params_hash.to_json
|
|
370
|
-
@otel_span.set_attribute("langfuse.observation.modelParameters", params_json)
|
|
372
|
+
def cost_details=(value)
|
|
373
|
+
update_observation_attributes(cost_details: value)
|
|
371
374
|
end
|
|
372
375
|
end
|
|
373
376
|
|
|
@@ -630,7 +633,7 @@ module Langfuse
|
|
|
630
633
|
# vectors = embedding_service.generate(embedding.input, model: embedding.model)
|
|
631
634
|
# embedding.update(
|
|
632
635
|
# output: vectors,
|
|
633
|
-
#
|
|
636
|
+
# usage_details: { prompt_tokens: 20, total_tokens: 20 }
|
|
634
637
|
# )
|
|
635
638
|
# end
|
|
636
639
|
#
|
|
@@ -647,6 +650,8 @@ module Langfuse
|
|
|
647
650
|
# embedding.end
|
|
648
651
|
#
|
|
649
652
|
class Embedding < BaseObservation
|
|
653
|
+
include ModelSetters
|
|
654
|
+
|
|
650
655
|
# @param otel_span [OpenTelemetry::SDK::Trace::Span] The underlying OTel span
|
|
651
656
|
# @param otel_tracer [OpenTelemetry::SDK::Trace::Tracer] The OTel tracer
|
|
652
657
|
# @param attributes [Hash, Types::EmbeddingAttributes, nil] Optional initial attributes
|
|
@@ -660,23 +665,5 @@ module Langfuse
|
|
|
660
665
|
update_observation_attributes(attrs)
|
|
661
666
|
self
|
|
662
667
|
end
|
|
663
|
-
|
|
664
|
-
# @param value [Hash] Usage hash with token counts (:prompt_tokens, :total_tokens)
|
|
665
|
-
# @return [void]
|
|
666
|
-
def usage=(value)
|
|
667
|
-
update_observation_attributes(usage_details: value)
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
# @param value [String] Model name (e.g., "text-embedding-ada-002")
|
|
671
|
-
# @return [void]
|
|
672
|
-
def model=(value)
|
|
673
|
-
update_observation_attributes(model: value)
|
|
674
|
-
end
|
|
675
|
-
|
|
676
|
-
# @param value [Hash] Model parameters (temperature, max_tokens, etc.)
|
|
677
|
-
# @return [void]
|
|
678
|
-
def model_parameters=(value)
|
|
679
|
-
update_observation_attributes(model_parameters: value)
|
|
680
|
-
end
|
|
681
668
|
end
|
|
682
669
|
end
|
|
@@ -59,12 +59,16 @@ module Langfuse
|
|
|
59
59
|
RELEASE = "langfuse.release"
|
|
60
60
|
ENVIRONMENT = "langfuse.environment"
|
|
61
61
|
|
|
62
|
+
# Validation limits
|
|
63
|
+
MAX_TAG_LENGTH = 200
|
|
64
|
+
|
|
62
65
|
# Creates OpenTelemetry attributes from Langfuse trace attributes
|
|
63
66
|
#
|
|
64
67
|
# Converts user-friendly trace attributes into the internal OpenTelemetry
|
|
65
68
|
# attribute format required by the span processor.
|
|
66
69
|
#
|
|
67
70
|
# @param attrs [Types::TraceAttributes, Hash] Trace attributes object or hash
|
|
71
|
+
# @param mask [#call, nil] Mask callable applied to input, output, and metadata
|
|
68
72
|
# @return [Hash] OpenTelemetry attributes hash with non-nil values
|
|
69
73
|
#
|
|
70
74
|
# @example
|
|
@@ -77,23 +81,25 @@ module Langfuse
|
|
|
77
81
|
# )
|
|
78
82
|
# otel_attrs = Langfuse::OtelAttributes.create_trace_attributes(attrs)
|
|
79
83
|
#
|
|
80
|
-
def self.create_trace_attributes(attrs)
|
|
84
|
+
def self.create_trace_attributes(attrs, mask: nil)
|
|
81
85
|
# Convert to hash if it's a TraceAttributes object
|
|
82
86
|
attrs = attrs.to_h
|
|
83
87
|
get_value = ->(key) { get_hash_value(attrs, key) }
|
|
84
88
|
|
|
89
|
+
input, output, metadata = mask_payload_fields(get_value, mask: mask)
|
|
90
|
+
|
|
85
91
|
attributes = {
|
|
86
92
|
TRACE_NAME => get_value.call(:name),
|
|
87
93
|
TRACE_USER_ID => get_value.call(:user_id),
|
|
88
94
|
TRACE_SESSION_ID => get_value.call(:session_id),
|
|
89
95
|
VERSION => get_value.call(:version),
|
|
90
96
|
RELEASE => get_value.call(:release),
|
|
91
|
-
TRACE_INPUT => serialize(
|
|
92
|
-
TRACE_OUTPUT => serialize(
|
|
93
|
-
TRACE_TAGS =>
|
|
97
|
+
TRACE_INPUT => serialize(input),
|
|
98
|
+
TRACE_OUTPUT => serialize(output),
|
|
99
|
+
TRACE_TAGS => normalize_tags(get_value.call(:tags)),
|
|
94
100
|
ENVIRONMENT => get_value.call(:environment),
|
|
95
101
|
TRACE_PUBLIC => get_value.call(:public),
|
|
96
|
-
**flatten_metadata(
|
|
102
|
+
**flatten_metadata(metadata, TRACE_METADATA)
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
# Remove nil values
|
|
@@ -107,6 +113,7 @@ module Langfuse
|
|
|
107
113
|
#
|
|
108
114
|
# @param type [String] Observation type (e.g., "generation", "span", "event")
|
|
109
115
|
# @param attrs [Types::SpanAttributes, Types::GenerationAttributes, Hash] Observation attributes
|
|
116
|
+
# @param mask [#call, nil] Mask callable applied to input, output, and metadata
|
|
110
117
|
# @return [Hash] OpenTelemetry attributes hash with non-nil values
|
|
111
118
|
#
|
|
112
119
|
# @example
|
|
@@ -117,11 +124,11 @@ module Langfuse
|
|
|
117
124
|
# )
|
|
118
125
|
# otel_attrs = Langfuse::OtelAttributes.create_observation_attributes("generation", attrs)
|
|
119
126
|
#
|
|
120
|
-
def self.create_observation_attributes(type, attrs)
|
|
127
|
+
def self.create_observation_attributes(type, attrs, mask: nil)
|
|
121
128
|
attrs = attrs.to_h
|
|
122
129
|
get_value = ->(key) { get_hash_value(attrs, key) }
|
|
123
130
|
|
|
124
|
-
otel_attributes = build_observation_base_attributes(type, get_value)
|
|
131
|
+
otel_attributes = build_observation_base_attributes(type, get_value, mask: mask)
|
|
125
132
|
add_prompt_attributes(otel_attributes, get_value.call(:prompt))
|
|
126
133
|
|
|
127
134
|
# Remove nil values
|
|
@@ -155,6 +162,27 @@ module Langfuse
|
|
|
155
162
|
end
|
|
156
163
|
end
|
|
157
164
|
|
|
165
|
+
# Filters tags to String-only elements within 200-char limit, returns nil if empty or nil
|
|
166
|
+
#
|
|
167
|
+
# @param tags [Array, nil] Raw tags array (each tag must be ≤200 characters; oversized tags are dropped with a warning)
|
|
168
|
+
# @return [Array<String>, nil] Filtered tags or nil
|
|
169
|
+
# @api private
|
|
170
|
+
def self.normalize_tags(tags)
|
|
171
|
+
return nil if tags.nil?
|
|
172
|
+
|
|
173
|
+
logger = Langfuse.configuration.logger
|
|
174
|
+
filtered = tags.select do |t|
|
|
175
|
+
next false unless t.is_a?(String)
|
|
176
|
+
|
|
177
|
+
if t.length > MAX_TAG_LENGTH
|
|
178
|
+
logger.warn("Langfuse: Tag exceeds #{MAX_TAG_LENGTH} characters (#{t.length} chars). Dropping.")
|
|
179
|
+
next false
|
|
180
|
+
end
|
|
181
|
+
true
|
|
182
|
+
end
|
|
183
|
+
filtered.empty? ? nil : filtered
|
|
184
|
+
end
|
|
185
|
+
|
|
158
186
|
# Flattens and serializes metadata into OpenTelemetry attribute format
|
|
159
187
|
#
|
|
160
188
|
# Converts nested metadata objects into dot-notation attribute keys.
|
|
@@ -210,6 +238,20 @@ module Langfuse
|
|
|
210
238
|
end
|
|
211
239
|
end
|
|
212
240
|
|
|
241
|
+
# Applies masking to the three payload fields (input, output, metadata)
|
|
242
|
+
#
|
|
243
|
+
# @param get_value [Proc] Lambda to get values from attributes hash
|
|
244
|
+
# @param mask [#call, nil] Mask callable
|
|
245
|
+
# @return [Array(Object, Object, Object)] Masked [input, output, metadata]
|
|
246
|
+
# @api private
|
|
247
|
+
def self.mask_payload_fields(get_value, mask:)
|
|
248
|
+
[
|
|
249
|
+
Masking.apply(get_value.call(:input), mask: mask),
|
|
250
|
+
Masking.apply(get_value.call(:output), mask: mask),
|
|
251
|
+
Masking.apply(get_value.call(:metadata), mask: mask)
|
|
252
|
+
]
|
|
253
|
+
end
|
|
254
|
+
|
|
213
255
|
# Gets a value from a hash supporting both symbol and string keys
|
|
214
256
|
# Handles false values correctly (doesn't treat false as nil)
|
|
215
257
|
#
|
|
@@ -228,23 +270,26 @@ module Langfuse
|
|
|
228
270
|
#
|
|
229
271
|
# @param type [String] Observation type
|
|
230
272
|
# @param get_value [Proc] Lambda to get values from attributes hash
|
|
273
|
+
# @param mask [#call, nil] Mask callable applied to input, output, and metadata
|
|
231
274
|
# @return [Hash] Base observation attributes
|
|
232
275
|
# @api private
|
|
233
|
-
def self.build_observation_base_attributes(type, get_value)
|
|
276
|
+
def self.build_observation_base_attributes(type, get_value, mask: nil)
|
|
277
|
+
input, output, metadata = mask_payload_fields(get_value, mask: mask)
|
|
278
|
+
|
|
234
279
|
{
|
|
235
280
|
OBSERVATION_TYPE => type,
|
|
236
281
|
OBSERVATION_LEVEL => get_value.call(:level),
|
|
237
282
|
OBSERVATION_STATUS_MESSAGE => get_value.call(:status_message),
|
|
238
283
|
VERSION => get_value.call(:version),
|
|
239
|
-
OBSERVATION_INPUT => serialize(
|
|
240
|
-
OBSERVATION_OUTPUT => serialize(
|
|
284
|
+
OBSERVATION_INPUT => serialize(input),
|
|
285
|
+
OBSERVATION_OUTPUT => serialize(output),
|
|
241
286
|
OBSERVATION_MODEL => get_value.call(:model),
|
|
242
287
|
OBSERVATION_USAGE_DETAILS => serialize(get_value.call(:usage_details)),
|
|
243
288
|
OBSERVATION_COST_DETAILS => serialize(get_value.call(:cost_details)),
|
|
244
289
|
OBSERVATION_COMPLETION_START_TIME => serialize(get_value.call(:completion_start_time)),
|
|
245
290
|
OBSERVATION_MODEL_PARAMETERS => serialize(get_value.call(:model_parameters)),
|
|
246
291
|
ENVIRONMENT => get_value.call(:environment),
|
|
247
|
-
**flatten_metadata(
|
|
292
|
+
**flatten_metadata(metadata, OBSERVATION_METADATA)
|
|
248
293
|
}
|
|
249
294
|
end
|
|
250
295
|
|
data/lib/langfuse/otel_setup.rb
CHANGED
|
@@ -56,9 +56,10 @@ module Langfuse
|
|
|
56
56
|
@tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new
|
|
57
57
|
@tracer_provider.add_span_processor(processor)
|
|
58
58
|
|
|
59
|
-
# Add span processor for propagated attributes
|
|
60
|
-
# This must be added AFTER the BatchSpanProcessor
|
|
61
|
-
|
|
59
|
+
# Add span processor for propagated attributes and env/release defaults
|
|
60
|
+
# This must be added AFTER the BatchSpanProcessor so it runs before export and can
|
|
61
|
+
# apply all attributes (propagated IDs, environment, release) to the spans being sent
|
|
62
|
+
span_processor = SpanProcessor.new(config: config)
|
|
62
63
|
@tracer_provider.add_span_processor(span_processor)
|
|
63
64
|
|
|
64
65
|
# Set as global tracer provider
|
data/lib/langfuse/propagation.rb
CHANGED
|
@@ -166,15 +166,12 @@ module Langfuse
|
|
|
166
166
|
span_key = _get_propagated_span_key(key)
|
|
167
167
|
|
|
168
168
|
if key == "metadata" && value.is_a?(Hash)
|
|
169
|
-
# Handle metadata - flatten into individual attributes
|
|
170
169
|
value.each do |k, v|
|
|
171
170
|
metadata_key = "#{OtelAttributes::TRACE_METADATA}.#{k}"
|
|
172
171
|
propagated_attributes[metadata_key] = v.to_s
|
|
173
172
|
end
|
|
174
173
|
elsif key == "tags" && value.is_a?(Array)
|
|
175
|
-
|
|
176
|
-
serialized_tags = OtelAttributes.serialize(value)
|
|
177
|
-
propagated_attributes[span_key] = serialized_tags if serialized_tags
|
|
174
|
+
propagated_attributes[span_key] = value unless value.empty?
|
|
178
175
|
else
|
|
179
176
|
propagated_attributes[span_key] = value.to_s
|
|
180
177
|
end
|
|
@@ -209,7 +206,7 @@ module Langfuse
|
|
|
209
206
|
def self._merge_tags(context, context_key, new_tags)
|
|
210
207
|
existing = context.value(context_key) || []
|
|
211
208
|
existing = existing.to_a if existing.respond_to?(:to_a)
|
|
212
|
-
(existing + new_tags).uniq
|
|
209
|
+
(existing + new_tags).uniq.freeze
|
|
213
210
|
end
|
|
214
211
|
|
|
215
212
|
# Set a propagated attribute in context and on current span
|
|
@@ -229,36 +226,31 @@ module Langfuse
|
|
|
229
226
|
baggage_key = _get_propagated_baggage_key(key)
|
|
230
227
|
|
|
231
228
|
# Merge metadata/tags with existing context values
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
229
|
+
merged = if key == "metadata" && value.is_a?(Hash)
|
|
230
|
+
_merge_metadata(context, context_key, value)
|
|
231
|
+
elsif key == "tags" && value.is_a?(Array)
|
|
232
|
+
_merge_tags(context, context_key, value)
|
|
233
|
+
else
|
|
234
|
+
value
|
|
235
|
+
end
|
|
239
236
|
|
|
240
|
-
|
|
241
|
-
context = context.set_value(context_key, value)
|
|
237
|
+
context = context.set_value(context_key, merged)
|
|
242
238
|
|
|
243
239
|
# Set on current span (if recording)
|
|
244
240
|
if span&.recording?
|
|
245
|
-
if key == "metadata" &&
|
|
246
|
-
|
|
247
|
-
value.each do |k, v|
|
|
241
|
+
if key == "metadata" && merged.is_a?(Hash)
|
|
242
|
+
merged.each do |k, v|
|
|
248
243
|
metadata_key = "#{OtelAttributes::TRACE_METADATA}.#{k}"
|
|
249
244
|
span.set_attribute(metadata_key, v.to_s)
|
|
250
245
|
end
|
|
251
|
-
elsif key == "tags" &&
|
|
252
|
-
|
|
253
|
-
serialized_tags = OtelAttributes.serialize(value)
|
|
254
|
-
span.set_attribute(span_key, serialized_tags) if serialized_tags
|
|
246
|
+
elsif key == "tags" && merged.is_a?(Array)
|
|
247
|
+
span.set_attribute(span_key, merged) unless merged.empty?
|
|
255
248
|
else
|
|
256
|
-
span.set_attribute(span_key,
|
|
249
|
+
span.set_attribute(span_key, merged.to_s)
|
|
257
250
|
end
|
|
258
251
|
end
|
|
259
252
|
|
|
260
253
|
# Set in baggage (if requested and available)
|
|
261
|
-
# Note: Baggage support requires opentelemetry-baggage gem
|
|
262
254
|
if as_baggage
|
|
263
255
|
unless baggage_available?
|
|
264
256
|
Langfuse.configuration.logger.warn(
|
|
@@ -270,7 +262,7 @@ module Langfuse
|
|
|
270
262
|
context = _set_baggage_attribute(
|
|
271
263
|
context: context,
|
|
272
264
|
key: key,
|
|
273
|
-
value:
|
|
265
|
+
value: merged,
|
|
274
266
|
baggage_key: baggage_key
|
|
275
267
|
)
|
|
276
268
|
end
|
|
@@ -51,10 +51,13 @@ module Langfuse
|
|
|
51
51
|
#
|
|
52
52
|
# @param name [String] Score name (required)
|
|
53
53
|
# @param value [Numeric, Integer, String] Score value (type depends on data_type)
|
|
54
|
+
# @param id [String, nil] Score ID
|
|
54
55
|
# @param trace_id [String, nil] Trace ID to associate with the score
|
|
56
|
+
# @param session_id [String, nil] Session ID to associate with the score
|
|
55
57
|
# @param observation_id [String, nil] Observation ID to associate with the score
|
|
56
58
|
# @param comment [String, nil] Optional comment
|
|
57
59
|
# @param metadata [Hash, nil] Optional metadata hash
|
|
60
|
+
# @param environment [String, nil] Optional environment
|
|
58
61
|
# @param data_type [Symbol] Data type (:numeric, :boolean, :categorical)
|
|
59
62
|
# @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score
|
|
60
63
|
# @param config_id [String, nil] Optional score config ID
|
|
@@ -70,19 +73,23 @@ module Langfuse
|
|
|
70
73
|
# @example Categorical score
|
|
71
74
|
# create(name: "category", value: "high", trace_id: "abc123", data_type: :categorical)
|
|
72
75
|
# rubocop:disable Metrics/ParameterLists
|
|
73
|
-
def create(name:, value:, trace_id: nil,
|
|
74
|
-
data_type: :numeric, dataset_run_id: nil, config_id: nil)
|
|
76
|
+
def create(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_id: nil, comment: nil,
|
|
77
|
+
metadata: nil, environment: nil, data_type: :numeric, dataset_run_id: nil, config_id: nil)
|
|
75
78
|
validate_name(name)
|
|
79
|
+
# Keep identifier policy server-side to preserve cross-SDK parity and avoid blocking valid future payloads.
|
|
76
80
|
normalized_value = normalize_value(value, data_type)
|
|
77
81
|
data_type_str = Types::SCORE_DATA_TYPES[data_type] || raise(ArgumentError, "Invalid data_type: #{data_type}")
|
|
78
82
|
|
|
79
83
|
event = build_score_event(
|
|
80
84
|
name: name,
|
|
81
85
|
value: normalized_value,
|
|
86
|
+
id: id,
|
|
82
87
|
trace_id: trace_id,
|
|
88
|
+
session_id: session_id,
|
|
83
89
|
observation_id: observation_id,
|
|
84
90
|
comment: comment,
|
|
85
91
|
metadata: metadata,
|
|
92
|
+
environment: environment,
|
|
86
93
|
data_type: data_type_str,
|
|
87
94
|
dataset_run_id: dataset_run_id,
|
|
88
95
|
config_id: config_id
|
|
@@ -204,25 +211,30 @@ module Langfuse
|
|
|
204
211
|
#
|
|
205
212
|
# @param name [String] Score name
|
|
206
213
|
# @param value [Object] Normalized score value
|
|
214
|
+
# @param id [String, nil] Score ID
|
|
207
215
|
# @param trace_id [String, nil] Trace ID
|
|
216
|
+
# @param session_id [String, nil] Session ID
|
|
208
217
|
# @param observation_id [String, nil] Observation ID
|
|
209
218
|
# @param comment [String, nil] Comment
|
|
210
219
|
# @param metadata [Hash, nil] Metadata
|
|
220
|
+
# @param environment [String, nil] Environment
|
|
211
221
|
# @param data_type [String] Data type string (NUMERIC, BOOLEAN, CATEGORICAL)
|
|
212
222
|
# @return [Hash] Event hash
|
|
213
|
-
# rubocop:disable Metrics/ParameterLists
|
|
214
|
-
def build_score_event(name:, value:, trace_id:, observation_id:, comment:, metadata:,
|
|
215
|
-
dataset_run_id: nil, config_id: nil)
|
|
223
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
224
|
+
def build_score_event(name:, value:, id:, trace_id:, session_id:, observation_id:, comment:, metadata:,
|
|
225
|
+
environment:, data_type:, dataset_run_id: nil, config_id: nil)
|
|
216
226
|
body = {
|
|
217
|
-
id: SecureRandom.uuid,
|
|
227
|
+
id: id || SecureRandom.uuid,
|
|
218
228
|
name: name,
|
|
219
229
|
value: value,
|
|
220
230
|
dataType: data_type
|
|
221
231
|
}
|
|
222
232
|
body[:traceId] = trace_id if trace_id
|
|
233
|
+
body[:sessionId] = session_id if session_id
|
|
223
234
|
body[:observationId] = observation_id if observation_id
|
|
224
235
|
body[:comment] = comment if comment
|
|
225
236
|
body[:metadata] = metadata if metadata
|
|
237
|
+
body[:environment] = environment if environment
|
|
226
238
|
body[:datasetRunId] = dataset_run_id if dataset_run_id
|
|
227
239
|
body[:configId] = config_id if config_id
|
|
228
240
|
|
|
@@ -233,7 +245,7 @@ module Langfuse
|
|
|
233
245
|
body: body
|
|
234
246
|
}
|
|
235
247
|
end
|
|
236
|
-
# rubocop:enable Metrics/ParameterLists
|
|
248
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
237
249
|
|
|
238
250
|
# Normalize and validate score value based on data type
|
|
239
251
|
#
|
|
@@ -3,14 +3,21 @@
|
|
|
3
3
|
require "opentelemetry/sdk"
|
|
4
4
|
|
|
5
5
|
module Langfuse
|
|
6
|
-
# Span processor that
|
|
6
|
+
# Span processor that applies default and propagated trace attributes on new spans.
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# On span start, this processor first applies configured trace defaults
|
|
9
|
+
# (environment/release), then overlays attributes propagated in OpenTelemetry
|
|
10
|
+
# context (user/session/metadata/tags/version). This ensures consistent
|
|
11
|
+
# trace dimensions while still honoring per-request propagation.
|
|
11
12
|
#
|
|
12
13
|
# @api private
|
|
13
14
|
class SpanProcessor < OpenTelemetry::SDK::Trace::SpanProcessor
|
|
15
|
+
# @param config [Langfuse::Config, nil] SDK configuration used to build trace defaults
|
|
16
|
+
def initialize(config: Langfuse.configuration)
|
|
17
|
+
@default_trace_attributes = build_default_trace_attributes(config).freeze
|
|
18
|
+
super()
|
|
19
|
+
end
|
|
20
|
+
|
|
14
21
|
# Called when a span starts
|
|
15
22
|
#
|
|
16
23
|
# @param span [OpenTelemetry::SDK::Trace::Span] The span that started
|
|
@@ -19,13 +26,8 @@ module Langfuse
|
|
|
19
26
|
def on_start(span, parent_context)
|
|
20
27
|
return unless span.recording?
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
# Set attributes on span
|
|
26
|
-
propagated_attrs.each do |key, value|
|
|
27
|
-
span.set_attribute(key, value)
|
|
28
|
-
end
|
|
29
|
+
apply_attributes(span, @default_trace_attributes)
|
|
30
|
+
apply_attributes(span, propagated_attributes(parent_context))
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
# Called when a span ends
|
|
@@ -57,5 +59,25 @@ module Langfuse
|
|
|
57
59
|
_ = timeout # Suppress unused argument warning
|
|
58
60
|
0
|
|
59
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def build_default_trace_attributes(config)
|
|
66
|
+
return {} unless config
|
|
67
|
+
|
|
68
|
+
OtelAttributes.create_trace_attributes(
|
|
69
|
+
{ environment: config.environment, release: config.release }
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def propagated_attributes(parent_context)
|
|
74
|
+
return {} unless parent_context
|
|
75
|
+
|
|
76
|
+
Propagation.get_propagated_attributes_from_context(parent_context)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def apply_attributes(span, attributes)
|
|
80
|
+
attributes.each { |key, value| span.set_attribute(key, value) }
|
|
81
|
+
end
|
|
60
82
|
end
|
|
61
83
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Langfuse
|
|
6
|
+
# Deterministic and random trace/observation ID generation.
|
|
7
|
+
#
|
|
8
|
+
# Mirrors the Python and JS SDK helpers so the same seed produces the same
|
|
9
|
+
# trace ID across all three SDKs. This lets callers correlate Langfuse traces
|
|
10
|
+
# with external system identifiers (database primary keys, request IDs, etc.)
|
|
11
|
+
# and score or reference traces later without having to persist the generated
|
|
12
|
+
# Langfuse ID.
|
|
13
|
+
#
|
|
14
|
+
# @example Deterministic from an external ID
|
|
15
|
+
# trace_id = Langfuse::TraceId.create(seed: "order-12345")
|
|
16
|
+
# Langfuse.observe("process-order", trace_id: trace_id) { |span| ... }
|
|
17
|
+
# Langfuse.create_score(name: "quality", value: 0.9, trace_id: trace_id)
|
|
18
|
+
#
|
|
19
|
+
# @example Random (no seed)
|
|
20
|
+
# trace_id = Langfuse::TraceId.create
|
|
21
|
+
module TraceId
|
|
22
|
+
TRACE_ID_PATTERN = /\A[0-9a-f]{32}\z/
|
|
23
|
+
INVALID_TRACE_ID = ("0" * 32)
|
|
24
|
+
|
|
25
|
+
private_constant :TRACE_ID_PATTERN, :INVALID_TRACE_ID
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Generate a W3C trace ID (32 lowercase hex chars).
|
|
29
|
+
#
|
|
30
|
+
# With no seed, delegates to OpenTelemetry's random trace ID generator.
|
|
31
|
+
# With a seed, takes the first 16 bytes of SHA-256(seed) so the same
|
|
32
|
+
# input always produces the same trace ID.
|
|
33
|
+
#
|
|
34
|
+
# @note Avoid passing PII, secrets, or credentials as seeds — the raw seed
|
|
35
|
+
# value appears in application code and may leak through logs/backtraces.
|
|
36
|
+
# Use stable external identifiers (database PKs, UUIDs, request IDs).
|
|
37
|
+
# @param seed [String, nil] Optional seed for deterministic generation.
|
|
38
|
+
# Must be a String if provided; non-String values raise ArgumentError
|
|
39
|
+
# for cross-SDK parity (Python/JS both reject non-strings).
|
|
40
|
+
# @return [String] 32-character lowercase hex trace ID
|
|
41
|
+
# @raise [ArgumentError] if seed is not nil and not a String
|
|
42
|
+
def create(seed: nil)
|
|
43
|
+
return OpenTelemetry::Trace.generate_trace_id.unpack1("H*") if seed.nil?
|
|
44
|
+
|
|
45
|
+
Digest::SHA256.digest(validate_seed!(seed))[0, 16].unpack1("H*")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @api private
|
|
51
|
+
def validate_seed!(seed)
|
|
52
|
+
raise ArgumentError, "seed must be a String, got #{seed.class}" unless seed.is_a?(String)
|
|
53
|
+
|
|
54
|
+
# ASCII-8BIT strings (binary) often already hold valid UTF-8 bytes
|
|
55
|
+
# but can't be transcoded — re-tag them instead.
|
|
56
|
+
return seed.dup.force_encoding("UTF-8") if seed.encoding == Encoding::ASCII_8BIT
|
|
57
|
+
|
|
58
|
+
seed.encode("UTF-8")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
def valid?(trace_id)
|
|
63
|
+
return false unless trace_id.is_a?(String) && TRACE_ID_PATTERN.match?(trace_id)
|
|
64
|
+
|
|
65
|
+
trace_id != INVALID_TRACE_ID
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Build a sampled OpenTelemetry SpanContext carrying the given hex trace ID.
|
|
69
|
+
#
|
|
70
|
+
# A random span_id is generated as a placeholder — only the trace_id is
|
|
71
|
+
# consumed by the child span that gets created.
|
|
72
|
+
#
|
|
73
|
+
# @api private
|
|
74
|
+
def to_span_context(trace_id)
|
|
75
|
+
raise ArgumentError, "Invalid trace_id: #{trace_id.inspect}" unless valid?(trace_id)
|
|
76
|
+
|
|
77
|
+
OpenTelemetry::Trace::SpanContext.new(
|
|
78
|
+
trace_id: [trace_id].pack("H*"),
|
|
79
|
+
span_id: OpenTelemetry::Trace.generate_span_id,
|
|
80
|
+
trace_flags: OpenTelemetry::Trace::TraceFlags::SAMPLED,
|
|
81
|
+
# Cross-SDK parity: Python uses is_remote=False (_create_remote_parent_span).
|
|
82
|
+
# Changing this would alter ParentBased sampler behavior across SDKs.
|
|
83
|
+
remote: false
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/langfuse/types.rb
CHANGED
|
@@ -173,7 +173,7 @@ module Langfuse
|
|
|
173
173
|
# @return [Hash, nil] Token usage and other model-specific usage metrics
|
|
174
174
|
attr_accessor :usage_details
|
|
175
175
|
|
|
176
|
-
# @return [Hash, nil] Cost breakdown for the generation (
|
|
176
|
+
# @return [Hash, nil] Cost breakdown for the generation (prefer :input, :output, :total)
|
|
177
177
|
attr_accessor :cost_details
|
|
178
178
|
|
|
179
179
|
# @return [Hash, nil] Information about the prompt used from Langfuse prompt management
|
|
@@ -186,7 +186,7 @@ module Langfuse
|
|
|
186
186
|
# @param model [String, nil] Model name
|
|
187
187
|
# @param model_parameters [Hash, nil] Model parameters
|
|
188
188
|
# @param usage_details [Hash, nil] Usage metrics
|
|
189
|
-
# @param cost_details [Hash, nil] Cost breakdown
|
|
189
|
+
# @param cost_details [Hash, nil] Cost breakdown (prefer :input, :output, :total)
|
|
190
190
|
# @param prompt [Hash, nil] Prompt information with :name, :version, :is_fallback keys
|
|
191
191
|
# @param kwargs [Hash] Additional keyword arguments passed to SpanAttributes
|
|
192
192
|
# rubocop:disable Metrics/ParameterLists
|
|
@@ -287,7 +287,7 @@ module Langfuse
|
|
|
287
287
|
# @param input [Object, nil] Input data
|
|
288
288
|
# @param output [Object, nil] Output data
|
|
289
289
|
# @param metadata [Hash, nil] Additional metadata
|
|
290
|
-
# @param tags [Array<String>, nil] Tags array
|
|
290
|
+
# @param tags [Array<String>, nil] Tags array (each tag must be ≤200 characters; oversized tags are dropped)
|
|
291
291
|
# @param public [Boolean, nil] Public visibility flag
|
|
292
292
|
# @param environment [String, nil] Environment identifier
|
|
293
293
|
# rubocop:disable Metrics/ParameterLists
|
data/lib/langfuse/version.rb
CHANGED
data/lib/langfuse.rb
CHANGED
|
@@ -45,10 +45,12 @@ require_relative "langfuse/rails_cache_adapter"
|
|
|
45
45
|
require_relative "langfuse/cache_warmer"
|
|
46
46
|
require_relative "langfuse/api_client"
|
|
47
47
|
require_relative "langfuse/otel_setup"
|
|
48
|
+
require_relative "langfuse/masking"
|
|
48
49
|
require_relative "langfuse/otel_attributes"
|
|
49
50
|
require_relative "langfuse/propagation"
|
|
50
51
|
require_relative "langfuse/span_processor"
|
|
51
52
|
require_relative "langfuse/observations"
|
|
53
|
+
require_relative "langfuse/trace_id"
|
|
52
54
|
require_relative "langfuse/score_client"
|
|
53
55
|
require_relative "langfuse/text_prompt_client"
|
|
54
56
|
require_relative "langfuse/chat_prompt_client"
|
|
@@ -190,11 +192,16 @@ module Langfuse
|
|
|
190
192
|
#
|
|
191
193
|
# @param name [String] Score name (required)
|
|
192
194
|
# @param value [Numeric, Integer, String] Score value (type depends on data_type)
|
|
195
|
+
# @param id [String, nil] Score ID
|
|
193
196
|
# @param trace_id [String, nil] Trace ID to associate with the score
|
|
197
|
+
# @param session_id [String, nil] Session ID to associate with the score
|
|
194
198
|
# @param observation_id [String, nil] Observation ID to associate with the score
|
|
195
199
|
# @param comment [String, nil] Optional comment
|
|
196
200
|
# @param metadata [Hash, nil] Optional metadata hash
|
|
201
|
+
# @param environment [String, nil] Optional environment
|
|
197
202
|
# @param data_type [Symbol] Data type (:numeric, :boolean, :categorical)
|
|
203
|
+
# @param dataset_run_id [String, nil] Optional dataset run ID to associate with the score
|
|
204
|
+
# @param config_id [String, nil] Optional score config ID
|
|
198
205
|
# @return [void]
|
|
199
206
|
# @raise [ArgumentError] if validation fails
|
|
200
207
|
#
|
|
@@ -207,16 +214,21 @@ module Langfuse
|
|
|
207
214
|
# @example Categorical score
|
|
208
215
|
# Langfuse.create_score(name: "category", value: "high", trace_id: "abc123", data_type: :categorical)
|
|
209
216
|
# rubocop:disable Metrics/ParameterLists
|
|
210
|
-
def create_score(name:, value:, trace_id: nil,
|
|
211
|
-
data_type: :numeric)
|
|
217
|
+
def create_score(name:, value:, id: nil, trace_id: nil, session_id: nil, observation_id: nil, comment: nil,
|
|
218
|
+
metadata: nil, environment: nil, data_type: :numeric, dataset_run_id: nil, config_id: nil)
|
|
212
219
|
client.create_score(
|
|
213
220
|
name: name,
|
|
214
221
|
value: value,
|
|
222
|
+
id: id,
|
|
215
223
|
trace_id: trace_id,
|
|
224
|
+
session_id: session_id,
|
|
216
225
|
observation_id: observation_id,
|
|
217
226
|
comment: comment,
|
|
218
227
|
metadata: metadata,
|
|
219
|
-
|
|
228
|
+
environment: environment,
|
|
229
|
+
data_type: data_type,
|
|
230
|
+
dataset_run_id: dataset_run_id,
|
|
231
|
+
config_id: config_id
|
|
220
232
|
)
|
|
221
233
|
end
|
|
222
234
|
# rubocop:enable Metrics/ParameterLists
|
|
@@ -285,6 +297,24 @@ module Langfuse
|
|
|
285
297
|
client.flush_scores if @client
|
|
286
298
|
end
|
|
287
299
|
|
|
300
|
+
# Generate a trace ID (deterministic when seeded, random otherwise).
|
|
301
|
+
#
|
|
302
|
+
# Use this to correlate Langfuse traces with external identifiers. The
|
|
303
|
+
# same seed always produces the same trace ID across the Ruby, Python,
|
|
304
|
+
# and JS SDKs (SHA-256 of the seed, first 16 bytes, as 32 hex chars).
|
|
305
|
+
#
|
|
306
|
+
# @note Avoid PII or secrets as seeds. See {TraceId.create} for details.
|
|
307
|
+
# @param seed [String, nil] Optional deterministic seed
|
|
308
|
+
# @return [String] 32-character lowercase hex trace ID
|
|
309
|
+
# @raise [ArgumentError] if seed is not nil and not a String
|
|
310
|
+
#
|
|
311
|
+
# @example
|
|
312
|
+
# trace_id = Langfuse.create_trace_id(seed: "order-12345")
|
|
313
|
+
# Langfuse.observe("process", trace_id: trace_id) { |span| ... }
|
|
314
|
+
def create_trace_id(seed: nil)
|
|
315
|
+
TraceId.create(seed: seed)
|
|
316
|
+
end
|
|
317
|
+
|
|
288
318
|
# Reset global configuration and client (useful for testing)
|
|
289
319
|
#
|
|
290
320
|
# @return [void]
|
|
@@ -308,11 +338,14 @@ module Langfuse
|
|
|
308
338
|
# @param name [String] Descriptive name for the observation
|
|
309
339
|
# @param attrs [Hash, Types::SpanAttributes, Types::GenerationAttributes, nil] Observation attributes
|
|
310
340
|
# @param as_type [Symbol, String] Observation type (:span, :generation, :event, etc.)
|
|
341
|
+
# @param trace_id [String, nil] Optional 32-char lowercase hex trace ID to attach the observation to.
|
|
342
|
+
# Mutually exclusive with `parent_span_context`. Use {Langfuse.create_trace_id} to generate one.
|
|
311
343
|
# @param parent_span_context [OpenTelemetry::Trace::SpanContext, nil] Parent span context for child observations
|
|
312
344
|
# @param start_time [Time, Integer, nil] Optional start time (Time object or Unix timestamp in nanoseconds)
|
|
313
345
|
# @param skip_validation [Boolean] Skip validation (for internal use). Defaults to false.
|
|
314
346
|
# @return [BaseObservation] The observation wrapper (Span, Generation, or Event)
|
|
315
|
-
# @raise [ArgumentError] if an invalid observation type is provided
|
|
347
|
+
# @raise [ArgumentError] if an invalid observation type is provided, an invalid `trace_id` is given,
|
|
348
|
+
# or both `trace_id` and `parent_span_context` are provided
|
|
316
349
|
#
|
|
317
350
|
# @example Create root span
|
|
318
351
|
# span = Langfuse.start_observation("root-operation", { input: {...} })
|
|
@@ -321,14 +354,16 @@ module Langfuse
|
|
|
321
354
|
# child = Langfuse.start_observation("llm-call", { model: "gpt-4" },
|
|
322
355
|
# as_type: :generation,
|
|
323
356
|
# parent_span_context: parent.otel_span.context)
|
|
324
|
-
|
|
325
|
-
|
|
357
|
+
#
|
|
358
|
+
# @example Attach to a deterministic trace ID
|
|
359
|
+
# trace_id = Langfuse.create_trace_id(seed: "order-123")
|
|
360
|
+
# root = Langfuse.start_observation("process-order", trace_id: trace_id)
|
|
361
|
+
# rubocop:disable Metrics/ParameterLists
|
|
362
|
+
def start_observation(name, attrs = {}, as_type: :span, trace_id: nil, parent_span_context: nil,
|
|
363
|
+
start_time: nil, skip_validation: false)
|
|
364
|
+
parent_span_context = resolve_trace_context(trace_id, parent_span_context)
|
|
326
365
|
type_str = as_type.to_s
|
|
327
|
-
|
|
328
|
-
unless skip_validation || valid_observation_type?(as_type)
|
|
329
|
-
valid_types = OBSERVATION_TYPES.values.sort.join(", ")
|
|
330
|
-
raise ArgumentError, "Invalid observation type: #{type_str}. Valid types: #{valid_types}"
|
|
331
|
-
end
|
|
366
|
+
validate_observation_type!(as_type, type_str) unless skip_validation
|
|
332
367
|
|
|
333
368
|
otel_tracer = otel_tracer()
|
|
334
369
|
otel_span = create_otel_span(
|
|
@@ -337,32 +372,27 @@ module Langfuse
|
|
|
337
372
|
parent_span_context: parent_span_context,
|
|
338
373
|
otel_tracer: otel_tracer
|
|
339
374
|
)
|
|
375
|
+
apply_observation_attributes(otel_span, type_str, attrs)
|
|
340
376
|
|
|
341
|
-
|
|
342
|
-
# Only set attributes if span is still recording (should always be true here, but guard for safety)
|
|
343
|
-
if otel_span.recording?
|
|
344
|
-
otel_attrs = OtelAttributes.create_observation_attributes(type_str, attrs.to_h)
|
|
345
|
-
otel_attrs.each { |key, value| otel_span.set_attribute(key, value) }
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# Wrap in appropriate class
|
|
349
|
-
observation = wrap_otel_span(otel_span, type_str, otel_tracer, attributes: attrs)
|
|
350
|
-
|
|
377
|
+
observation = wrap_otel_span(otel_span, type_str, otel_tracer)
|
|
351
378
|
# Events auto-end immediately when created
|
|
352
379
|
observation.end if type_str == OBSERVATION_TYPES[:event]
|
|
353
|
-
|
|
354
380
|
observation
|
|
355
381
|
end
|
|
382
|
+
# rubocop:enable Metrics/ParameterLists
|
|
356
383
|
|
|
357
384
|
# User-facing convenience method for creating root observations
|
|
358
385
|
#
|
|
359
386
|
# @param name [String] Descriptive name for the observation
|
|
360
387
|
# @param attrs [Hash] Observation attributes (optional positional or keyword)
|
|
361
388
|
# @param as_type [Symbol, String] Observation type (:span, :generation, :event, etc.)
|
|
389
|
+
# @param trace_id [String, nil] Optional 32-char lowercase hex trace ID to attach the observation to.
|
|
390
|
+
# Use {Langfuse.create_trace_id} to generate one. Forwarded to {.start_observation}.
|
|
362
391
|
# @param kwargs [Hash] Additional keyword arguments merged into observation attributes (e.g., input:, output:, metadata:)
|
|
363
392
|
# @yield [observation] Optional block that receives the observation object
|
|
364
393
|
# @yieldparam observation [BaseObservation] The observation object
|
|
365
394
|
# @return [BaseObservation, Object] The observation (or block return value if block given)
|
|
395
|
+
# @raise [ArgumentError] if an invalid `trace_id` is provided
|
|
366
396
|
#
|
|
367
397
|
# @example Block-based API (auto-ends)
|
|
368
398
|
# Langfuse.observe("operation") do |obs|
|
|
@@ -374,28 +404,12 @@ module Langfuse
|
|
|
374
404
|
# obs = Langfuse.observe("operation", input: { data: "test" })
|
|
375
405
|
# obs.update(output: { result: "success" })
|
|
376
406
|
# obs.end
|
|
377
|
-
def observe(name, attrs = {}, as_type: :span, **kwargs, &block)
|
|
378
|
-
# Merge positional attrs and keyword kwargs
|
|
407
|
+
def observe(name, attrs = {}, as_type: :span, trace_id: nil, **kwargs, &block)
|
|
379
408
|
merged_attrs = attrs.to_h.merge(kwargs)
|
|
380
|
-
observation = start_observation(name, merged_attrs, as_type: as_type)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# Set context and execute block
|
|
385
|
-
current_context = OpenTelemetry::Context.current
|
|
386
|
-
result = OpenTelemetry::Context.with_current(
|
|
387
|
-
OpenTelemetry::Trace.context_with_span(observation.otel_span, parent_context: current_context)
|
|
388
|
-
) do
|
|
389
|
-
block.call(observation)
|
|
390
|
-
end
|
|
391
|
-
# Only end if not already ended (events auto-end in start_observation)
|
|
392
|
-
observation.end unless as_type.to_s == OBSERVATION_TYPES[:event]
|
|
393
|
-
result
|
|
394
|
-
else
|
|
395
|
-
# Stateful API - return observation
|
|
396
|
-
# Events already auto-ended in start_observation
|
|
397
|
-
observation
|
|
398
|
-
end
|
|
409
|
+
observation = start_observation(name, merged_attrs, as_type: as_type, trace_id: trace_id)
|
|
410
|
+
return observation unless block
|
|
411
|
+
|
|
412
|
+
observation.send(:run_in_context, &block)
|
|
399
413
|
end
|
|
400
414
|
|
|
401
415
|
# Registry mapping observation type strings to their wrapper classes
|
|
@@ -414,6 +428,31 @@ module Langfuse
|
|
|
414
428
|
|
|
415
429
|
private
|
|
416
430
|
|
|
431
|
+
# @api private
|
|
432
|
+
def resolve_trace_context(trace_id, parent_span_context)
|
|
433
|
+
return parent_span_context unless trace_id
|
|
434
|
+
raise ArgumentError, "Cannot specify both trace_id and parent_span_context" if parent_span_context
|
|
435
|
+
|
|
436
|
+
TraceId.send(:to_span_context, trace_id)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# @api private
|
|
440
|
+
def validate_observation_type!(as_type, type_str)
|
|
441
|
+
return if valid_observation_type?(as_type)
|
|
442
|
+
|
|
443
|
+
valid_types = OBSERVATION_TYPES.values.sort.join(", ")
|
|
444
|
+
raise ArgumentError, "Invalid observation type: #{type_str}. Valid types: #{valid_types}"
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# @api private
|
|
448
|
+
def apply_observation_attributes(otel_span, type_str, attrs)
|
|
449
|
+
# Guard against ended spans — should always be recording here, but safe.
|
|
450
|
+
return unless otel_span.recording?
|
|
451
|
+
|
|
452
|
+
otel_attrs = OtelAttributes.create_observation_attributes(type_str, attrs.to_h, mask: configuration.mask)
|
|
453
|
+
otel_attrs.each { |key, value| otel_span.set_attribute(key, value) }
|
|
454
|
+
end
|
|
455
|
+
|
|
417
456
|
# Validates that an observation type is valid
|
|
418
457
|
#
|
|
419
458
|
# Checks if the provided type (symbol or string) matches a valid observation type
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: langfuse-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- SimplePractice
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-04-14 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: faraday
|
|
@@ -171,6 +172,7 @@ files:
|
|
|
171
172
|
- lib/langfuse/experiment_result.rb
|
|
172
173
|
- lib/langfuse/experiment_runner.rb
|
|
173
174
|
- lib/langfuse/item_result.rb
|
|
175
|
+
- lib/langfuse/masking.rb
|
|
174
176
|
- lib/langfuse/observations.rb
|
|
175
177
|
- lib/langfuse/otel_attributes.rb
|
|
176
178
|
- lib/langfuse/otel_setup.rb
|
|
@@ -182,6 +184,7 @@ files:
|
|
|
182
184
|
- lib/langfuse/stale_while_revalidate.rb
|
|
183
185
|
- lib/langfuse/text_prompt_client.rb
|
|
184
186
|
- lib/langfuse/timestamp_parser.rb
|
|
187
|
+
- lib/langfuse/trace_id.rb
|
|
185
188
|
- lib/langfuse/traced_execution.rb
|
|
186
189
|
- lib/langfuse/types.rb
|
|
187
190
|
- lib/langfuse/version.rb
|
|
@@ -193,6 +196,7 @@ metadata:
|
|
|
193
196
|
source_code_uri: https://github.com/simplepractice/langfuse-rb
|
|
194
197
|
changelog_uri: https://github.com/simplepractice/langfuse-rb/blob/main/CHANGELOG.md
|
|
195
198
|
rubygems_mfa_required: 'true'
|
|
199
|
+
post_install_message:
|
|
196
200
|
rdoc_options: []
|
|
197
201
|
require_paths:
|
|
198
202
|
- lib
|
|
@@ -207,7 +211,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
207
211
|
- !ruby/object:Gem::Version
|
|
208
212
|
version: '0'
|
|
209
213
|
requirements: []
|
|
210
|
-
rubygems_version: 4.
|
|
214
|
+
rubygems_version: 3.4.1
|
|
215
|
+
signing_key:
|
|
211
216
|
specification_version: 4
|
|
212
217
|
summary: Ruby SDK for Langfuse - LLM observability and prompt management
|
|
213
218
|
test_files: []
|