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.
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
- def start_observation(name, attrs = {}, as_type: :span, parent_span_context: nil, start_time: nil,
336
- skip_validation: false)
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
- if block
394
- # Block-based API: auto-ends when block completes
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
- OpenTelemetry.tracer_provider.tracer("langfuse-rb", Langfuse::VERSION)
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.6.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: 1980-01-02 00:00:00.000000000 Z
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.0.3
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: []