langfuse-rb 0.6.0 → 0.8.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 +27 -2
- data/README.md +28 -73
- data/lib/langfuse/api_client.rb +72 -0
- data/lib/langfuse/cache_warmer.rb +1 -1
- data/lib/langfuse/client.rb +70 -7
- data/lib/langfuse/config.rb +59 -6
- data/lib/langfuse/observations.rb +24 -19
- data/lib/langfuse/otel_setup.rb +140 -80
- data/lib/langfuse/sampling.rb +20 -0
- data/lib/langfuse/score_client.rb +43 -18
- data/lib/langfuse/span_filter.rb +81 -0
- data/lib/langfuse/span_processor.rb +37 -36
- data/lib/langfuse/trace_id.rb +88 -0
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +140 -44
- metadata +9 -3
data/lib/langfuse.rb
CHANGED
|
@@ -44,12 +44,15 @@ require_relative "langfuse/prompt_cache"
|
|
|
44
44
|
require_relative "langfuse/rails_cache_adapter"
|
|
45
45
|
require_relative "langfuse/cache_warmer"
|
|
46
46
|
require_relative "langfuse/api_client"
|
|
47
|
+
require_relative "langfuse/span_filter"
|
|
48
|
+
require_relative "langfuse/sampling"
|
|
47
49
|
require_relative "langfuse/otel_setup"
|
|
48
50
|
require_relative "langfuse/masking"
|
|
49
51
|
require_relative "langfuse/otel_attributes"
|
|
50
52
|
require_relative "langfuse/propagation"
|
|
51
53
|
require_relative "langfuse/span_processor"
|
|
52
54
|
require_relative "langfuse/observations"
|
|
55
|
+
require_relative "langfuse/trace_id"
|
|
53
56
|
require_relative "langfuse/score_client"
|
|
54
57
|
require_relative "langfuse/text_prompt_client"
|
|
55
58
|
require_relative "langfuse/chat_prompt_client"
|
|
@@ -90,10 +93,6 @@ module Langfuse
|
|
|
90
93
|
# end
|
|
91
94
|
def configure
|
|
92
95
|
yield(configuration)
|
|
93
|
-
|
|
94
|
-
# Auto-initialize OpenTelemetry
|
|
95
|
-
OtelSetup.setup(configuration)
|
|
96
|
-
|
|
97
96
|
configuration
|
|
98
97
|
end
|
|
99
98
|
|
|
@@ -104,6 +103,28 @@ module Langfuse
|
|
|
104
103
|
@client ||= Client.new(configuration)
|
|
105
104
|
end
|
|
106
105
|
|
|
106
|
+
# Return Langfuse's internal tracer provider for explicit global OpenTelemetry installation.
|
|
107
|
+
#
|
|
108
|
+
# @return [OpenTelemetry::SDK::Trace::TracerProvider]
|
|
109
|
+
# @raise [ConfigurationError] if tracing is not fully configured
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# Langfuse.configure do |config|
|
|
113
|
+
# config.public_key = ENV["LANGFUSE_PUBLIC_KEY"]
|
|
114
|
+
# config.secret_key = ENV["LANGFUSE_SECRET_KEY"]
|
|
115
|
+
# end
|
|
116
|
+
#
|
|
117
|
+
# OpenTelemetry.tracer_provider = Langfuse.tracer_provider
|
|
118
|
+
def tracer_provider
|
|
119
|
+
unless tracing_config_ready?
|
|
120
|
+
raise ConfigurationError,
|
|
121
|
+
"Langfuse tracing is disabled until public_key, secret_key, and base_url are configured."
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
OtelSetup.setup(configuration) unless OtelSetup.initialized?
|
|
125
|
+
OtelSetup.tracer_provider
|
|
126
|
+
end
|
|
127
|
+
|
|
107
128
|
# Shutdown Langfuse and flush any pending traces and scores
|
|
108
129
|
#
|
|
109
130
|
# Call this when shutting down your application to ensure
|
|
@@ -296,6 +317,24 @@ module Langfuse
|
|
|
296
317
|
client.flush_scores if @client
|
|
297
318
|
end
|
|
298
319
|
|
|
320
|
+
# Generate a trace ID (deterministic when seeded, random otherwise).
|
|
321
|
+
#
|
|
322
|
+
# Use this to correlate Langfuse traces with external identifiers. The
|
|
323
|
+
# same seed always produces the same trace ID across the Ruby, Python,
|
|
324
|
+
# and JS SDKs (SHA-256 of the seed, first 16 bytes, as 32 hex chars).
|
|
325
|
+
#
|
|
326
|
+
# @note Avoid PII or secrets as seeds. See {TraceId.create} for details.
|
|
327
|
+
# @param seed [String, nil] Optional deterministic seed
|
|
328
|
+
# @return [String] 32-character lowercase hex trace ID
|
|
329
|
+
# @raise [ArgumentError] if seed is not nil and not a String
|
|
330
|
+
#
|
|
331
|
+
# @example
|
|
332
|
+
# trace_id = Langfuse.create_trace_id(seed: "order-12345")
|
|
333
|
+
# Langfuse.observe("process", trace_id: trace_id) { |span| ... }
|
|
334
|
+
def create_trace_id(seed: nil)
|
|
335
|
+
TraceId.create(seed: seed)
|
|
336
|
+
end
|
|
337
|
+
|
|
299
338
|
# Reset global configuration and client (useful for testing)
|
|
300
339
|
#
|
|
301
340
|
# @return [void]
|
|
@@ -304,10 +343,14 @@ module Langfuse
|
|
|
304
343
|
OtelSetup.shutdown(timeout: 5) if OtelSetup.initialized?
|
|
305
344
|
@configuration = nil
|
|
306
345
|
@client = nil
|
|
346
|
+
@noop_tracer = nil
|
|
347
|
+
@tracing_disabled_warning_emitted = false
|
|
307
348
|
rescue StandardError
|
|
308
349
|
# Ignore shutdown errors during reset (e.g., in tests)
|
|
309
350
|
@configuration = nil
|
|
310
351
|
@client = nil
|
|
352
|
+
@noop_tracer = nil
|
|
353
|
+
@tracing_disabled_warning_emitted = false
|
|
311
354
|
end
|
|
312
355
|
|
|
313
356
|
# Creates a new observation (root or child)
|
|
@@ -319,11 +362,14 @@ module Langfuse
|
|
|
319
362
|
# @param name [String] Descriptive name for the observation
|
|
320
363
|
# @param attrs [Hash, Types::SpanAttributes, Types::GenerationAttributes, nil] Observation attributes
|
|
321
364
|
# @param as_type [Symbol, String] Observation type (:span, :generation, :event, etc.)
|
|
365
|
+
# @param trace_id [String, nil] Optional 32-char lowercase hex trace ID to attach the observation to.
|
|
366
|
+
# Mutually exclusive with `parent_span_context`. Use {Langfuse.create_trace_id} to generate one.
|
|
322
367
|
# @param parent_span_context [OpenTelemetry::Trace::SpanContext, nil] Parent span context for child observations
|
|
323
368
|
# @param start_time [Time, Integer, nil] Optional start time (Time object or Unix timestamp in nanoseconds)
|
|
324
369
|
# @param skip_validation [Boolean] Skip validation (for internal use). Defaults to false.
|
|
325
370
|
# @return [BaseObservation] The observation wrapper (Span, Generation, or Event)
|
|
326
|
-
# @raise [ArgumentError] if an invalid observation type is provided
|
|
371
|
+
# @raise [ArgumentError] if an invalid observation type is provided, an invalid `trace_id` is given,
|
|
372
|
+
# or both `trace_id` and `parent_span_context` are provided
|
|
327
373
|
#
|
|
328
374
|
# @example Create root span
|
|
329
375
|
# span = Langfuse.start_observation("root-operation", { input: {...} })
|
|
@@ -332,14 +378,16 @@ module Langfuse
|
|
|
332
378
|
# child = Langfuse.start_observation("llm-call", { model: "gpt-4" },
|
|
333
379
|
# as_type: :generation,
|
|
334
380
|
# parent_span_context: parent.otel_span.context)
|
|
335
|
-
|
|
336
|
-
|
|
381
|
+
#
|
|
382
|
+
# @example Attach to a deterministic trace ID
|
|
383
|
+
# trace_id = Langfuse.create_trace_id(seed: "order-123")
|
|
384
|
+
# root = Langfuse.start_observation("process-order", trace_id: trace_id)
|
|
385
|
+
# rubocop:disable Metrics/ParameterLists
|
|
386
|
+
def start_observation(name, attrs = {}, as_type: :span, trace_id: nil, parent_span_context: nil,
|
|
387
|
+
start_time: nil, skip_validation: false)
|
|
388
|
+
parent_span_context = resolve_trace_context(trace_id, parent_span_context)
|
|
337
389
|
type_str = as_type.to_s
|
|
338
|
-
|
|
339
|
-
unless skip_validation || valid_observation_type?(as_type)
|
|
340
|
-
valid_types = OBSERVATION_TYPES.values.sort.join(", ")
|
|
341
|
-
raise ArgumentError, "Invalid observation type: #{type_str}. Valid types: #{valid_types}"
|
|
342
|
-
end
|
|
390
|
+
validate_observation_type!(as_type, type_str) unless skip_validation
|
|
343
391
|
|
|
344
392
|
otel_tracer = otel_tracer()
|
|
345
393
|
otel_span = create_otel_span(
|
|
@@ -348,32 +396,27 @@ module Langfuse
|
|
|
348
396
|
parent_span_context: parent_span_context,
|
|
349
397
|
otel_tracer: otel_tracer
|
|
350
398
|
)
|
|
399
|
+
apply_observation_attributes(otel_span, type_str, attrs)
|
|
351
400
|
|
|
352
|
-
# Serialize attributes
|
|
353
|
-
# Only set attributes if span is still recording (should always be true here, but guard for safety)
|
|
354
|
-
if otel_span.recording?
|
|
355
|
-
otel_attrs = OtelAttributes.create_observation_attributes(type_str, attrs.to_h, mask: configuration.mask)
|
|
356
|
-
otel_attrs.each { |key, value| otel_span.set_attribute(key, value) }
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# Wrap in appropriate class (attributes already set on span above — pass nil to avoid double-masking)
|
|
360
401
|
observation = wrap_otel_span(otel_span, type_str, otel_tracer)
|
|
361
|
-
|
|
362
402
|
# Events auto-end immediately when created
|
|
363
403
|
observation.end if type_str == OBSERVATION_TYPES[:event]
|
|
364
|
-
|
|
365
404
|
observation
|
|
366
405
|
end
|
|
406
|
+
# rubocop:enable Metrics/ParameterLists
|
|
367
407
|
|
|
368
408
|
# User-facing convenience method for creating root observations
|
|
369
409
|
#
|
|
370
410
|
# @param name [String] Descriptive name for the observation
|
|
371
411
|
# @param attrs [Hash] Observation attributes (optional positional or keyword)
|
|
372
412
|
# @param as_type [Symbol, String] Observation type (:span, :generation, :event, etc.)
|
|
413
|
+
# @param trace_id [String, nil] Optional 32-char lowercase hex trace ID to attach the observation to.
|
|
414
|
+
# Use {Langfuse.create_trace_id} to generate one. Forwarded to {.start_observation}.
|
|
373
415
|
# @param kwargs [Hash] Additional keyword arguments merged into observation attributes (e.g., input:, output:, metadata:)
|
|
374
416
|
# @yield [observation] Optional block that receives the observation object
|
|
375
417
|
# @yieldparam observation [BaseObservation] The observation object
|
|
376
418
|
# @return [BaseObservation, Object] The observation (or block return value if block given)
|
|
419
|
+
# @raise [ArgumentError] if an invalid `trace_id` is provided
|
|
377
420
|
#
|
|
378
421
|
# @example Block-based API (auto-ends)
|
|
379
422
|
# Langfuse.observe("operation") do |obs|
|
|
@@ -385,28 +428,12 @@ module Langfuse
|
|
|
385
428
|
# obs = Langfuse.observe("operation", input: { data: "test" })
|
|
386
429
|
# obs.update(output: { result: "success" })
|
|
387
430
|
# obs.end
|
|
388
|
-
def observe(name, attrs = {}, as_type: :span, **kwargs, &block)
|
|
389
|
-
# Merge positional attrs and keyword kwargs
|
|
431
|
+
def observe(name, attrs = {}, as_type: :span, trace_id: nil, **kwargs, &block)
|
|
390
432
|
merged_attrs = attrs.to_h.merge(kwargs)
|
|
391
|
-
observation = start_observation(name, merged_attrs, as_type: as_type)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
# Set context and execute block
|
|
396
|
-
current_context = OpenTelemetry::Context.current
|
|
397
|
-
result = OpenTelemetry::Context.with_current(
|
|
398
|
-
OpenTelemetry::Trace.context_with_span(observation.otel_span, parent_context: current_context)
|
|
399
|
-
) do
|
|
400
|
-
block.call(observation)
|
|
401
|
-
end
|
|
402
|
-
# Only end if not already ended (events auto-end in start_observation)
|
|
403
|
-
observation.end unless as_type.to_s == OBSERVATION_TYPES[:event]
|
|
404
|
-
result
|
|
405
|
-
else
|
|
406
|
-
# Stateful API - return observation
|
|
407
|
-
# Events already auto-ended in start_observation
|
|
408
|
-
observation
|
|
409
|
-
end
|
|
433
|
+
observation = start_observation(name, merged_attrs, as_type: as_type, trace_id: trace_id)
|
|
434
|
+
return observation unless block
|
|
435
|
+
|
|
436
|
+
observation.send(:run_in_context, &block)
|
|
410
437
|
end
|
|
411
438
|
|
|
412
439
|
# Registry mapping observation type strings to their wrapper classes
|
|
@@ -425,6 +452,31 @@ module Langfuse
|
|
|
425
452
|
|
|
426
453
|
private
|
|
427
454
|
|
|
455
|
+
# @api private
|
|
456
|
+
def resolve_trace_context(trace_id, parent_span_context)
|
|
457
|
+
return parent_span_context unless trace_id
|
|
458
|
+
raise ArgumentError, "Cannot specify both trace_id and parent_span_context" if parent_span_context
|
|
459
|
+
|
|
460
|
+
TraceId.send(:to_span_context, trace_id)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# @api private
|
|
464
|
+
def validate_observation_type!(as_type, type_str)
|
|
465
|
+
return if valid_observation_type?(as_type)
|
|
466
|
+
|
|
467
|
+
valid_types = OBSERVATION_TYPES.values.sort.join(", ")
|
|
468
|
+
raise ArgumentError, "Invalid observation type: #{type_str}. Valid types: #{valid_types}"
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# @api private
|
|
472
|
+
def apply_observation_attributes(otel_span, type_str, attrs)
|
|
473
|
+
# Guard against ended spans — should always be recording here, but safe.
|
|
474
|
+
return unless otel_span.recording?
|
|
475
|
+
|
|
476
|
+
otel_attrs = OtelAttributes.create_observation_attributes(type_str, attrs.to_h, mask: configuration.mask)
|
|
477
|
+
otel_attrs.each { |key, value| otel_span.set_attribute(key, value) }
|
|
478
|
+
end
|
|
479
|
+
|
|
428
480
|
# Validates that an observation type is valid
|
|
429
481
|
#
|
|
430
482
|
# Checks if the provided type (symbol or string) matches a valid observation type
|
|
@@ -450,7 +502,10 @@ module Langfuse
|
|
|
450
502
|
#
|
|
451
503
|
# @return [OpenTelemetry::SDK::Trace::Tracer] The OTel tracer
|
|
452
504
|
def otel_tracer
|
|
453
|
-
|
|
505
|
+
return tracer_provider.tracer(LANGFUSE_TRACER_NAME, Langfuse::VERSION) if setup_tracing_if_ready
|
|
506
|
+
|
|
507
|
+
warn_tracing_disabled_once
|
|
508
|
+
noop_tracer
|
|
454
509
|
end
|
|
455
510
|
|
|
456
511
|
# Creates an OpenTelemetry span (root or child)
|
|
@@ -486,6 +541,47 @@ module Langfuse
|
|
|
486
541
|
observation_class = OBSERVATION_TYPE_REGISTRY[type_str] || Span
|
|
487
542
|
observation_class.new(otel_span, otel_tracer, attributes: attributes)
|
|
488
543
|
end
|
|
544
|
+
|
|
545
|
+
# rubocop:disable Naming/PredicateMethod
|
|
546
|
+
def setup_tracing_if_ready
|
|
547
|
+
return true if OtelSetup.initialized?
|
|
548
|
+
return false unless tracing_config_ready?
|
|
549
|
+
|
|
550
|
+
OtelSetup.setup(configuration)
|
|
551
|
+
true
|
|
552
|
+
end
|
|
553
|
+
# rubocop:enable Naming/PredicateMethod
|
|
554
|
+
|
|
555
|
+
def tracing_config_ready?
|
|
556
|
+
configured?(configuration.public_key) &&
|
|
557
|
+
configured?(configuration.secret_key) &&
|
|
558
|
+
configured?(configuration.base_url)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def configured?(value)
|
|
562
|
+
!value.nil? && !value.empty?
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def warn_tracing_disabled_once
|
|
566
|
+
return if @tracing_disabled_warning_emitted
|
|
567
|
+
|
|
568
|
+
tracing_warning_mutex.synchronize do
|
|
569
|
+
return if @tracing_disabled_warning_emitted
|
|
570
|
+
|
|
571
|
+
configuration.logger.warn(
|
|
572
|
+
"Langfuse tracing is disabled until public_key, secret_key, and base_url are configured."
|
|
573
|
+
)
|
|
574
|
+
@tracing_disabled_warning_emitted = true
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def tracing_warning_mutex
|
|
579
|
+
@tracing_warning_mutex ||= Mutex.new
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def noop_tracer
|
|
583
|
+
@noop_tracer ||= OpenTelemetry::Trace::TracerProvider.new.tracer(LANGFUSE_TRACER_NAME, Langfuse::VERSION)
|
|
584
|
+
end
|
|
489
585
|
end
|
|
490
586
|
# rubocop:enable Metrics/ClassLength
|
|
491
587
|
end
|
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.8.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-24 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: faraday
|
|
@@ -178,11 +179,14 @@ files:
|
|
|
178
179
|
- lib/langfuse/prompt_cache.rb
|
|
179
180
|
- lib/langfuse/propagation.rb
|
|
180
181
|
- lib/langfuse/rails_cache_adapter.rb
|
|
182
|
+
- lib/langfuse/sampling.rb
|
|
181
183
|
- lib/langfuse/score_client.rb
|
|
184
|
+
- lib/langfuse/span_filter.rb
|
|
182
185
|
- lib/langfuse/span_processor.rb
|
|
183
186
|
- lib/langfuse/stale_while_revalidate.rb
|
|
184
187
|
- lib/langfuse/text_prompt_client.rb
|
|
185
188
|
- lib/langfuse/timestamp_parser.rb
|
|
189
|
+
- lib/langfuse/trace_id.rb
|
|
186
190
|
- lib/langfuse/traced_execution.rb
|
|
187
191
|
- lib/langfuse/types.rb
|
|
188
192
|
- lib/langfuse/version.rb
|
|
@@ -194,6 +198,7 @@ metadata:
|
|
|
194
198
|
source_code_uri: https://github.com/simplepractice/langfuse-rb
|
|
195
199
|
changelog_uri: https://github.com/simplepractice/langfuse-rb/blob/main/CHANGELOG.md
|
|
196
200
|
rubygems_mfa_required: 'true'
|
|
201
|
+
post_install_message:
|
|
197
202
|
rdoc_options: []
|
|
198
203
|
require_paths:
|
|
199
204
|
- lib
|
|
@@ -208,7 +213,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
208
213
|
- !ruby/object:Gem::Version
|
|
209
214
|
version: '0'
|
|
210
215
|
requirements: []
|
|
211
|
-
rubygems_version: 4.
|
|
216
|
+
rubygems_version: 3.4.1
|
|
217
|
+
signing_key:
|
|
212
218
|
specification_version: 4
|
|
213
219
|
summary: Ruby SDK for Langfuse - LLM observability and prompt management
|
|
214
220
|
test_files: []
|