langfuse-rb 0.7.0 → 0.9.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.
@@ -3,22 +3,26 @@
3
3
  require "opentelemetry/sdk"
4
4
 
5
5
  module Langfuse
6
- # Span processor that applies default and propagated trace attributes on new spans.
7
- #
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.
6
+ # Batch span processor that owns Langfuse's enrichment and export filtering.
12
7
  #
13
8
  # @api private
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)
9
+ class SpanProcessor < OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor
10
+ # @param config [Langfuse::Config] SDK configuration used for defaults and filtering
11
+ # @param exporter [#export, #force_flush, #shutdown] Span exporter used by the batch processor
12
+ def initialize(config:, exporter:)
13
+ @logger = config.logger
17
14
  @default_trace_attributes = build_default_trace_attributes(config).freeze
18
- super()
15
+ @should_export_span = config.should_export_span || Langfuse.method(:default_export_span?)
16
+
17
+ super(
18
+ exporter,
19
+ max_queue_size: config.batch_size * 2,
20
+ schedule_delay: schedule_delay_for(config),
21
+ max_export_batch_size: config.batch_size
22
+ )
19
23
  end
20
24
 
21
- # Called when a span starts
25
+ # Apply Langfuse trace defaults and propagated attributes before a span records work.
22
26
  #
23
27
  # @param span [OpenTelemetry::SDK::Trace::Span] The span that started
24
28
  # @param parent_context [OpenTelemetry::Context] The parent context
@@ -30,41 +34,28 @@ module Langfuse
30
34
  apply_attributes(span, propagated_attributes(parent_context))
31
35
  end
32
36
 
33
- # Called when a span ends
37
+ # Drop spans when the export filter rejects them or raises.
34
38
  #
35
39
  # @param span [OpenTelemetry::SDK::Trace::Span] The span that ended
36
40
  # @return [void]
37
41
  def on_finish(span)
38
- # No-op - we don't need to do anything when spans finish
39
- end
42
+ return unless should_export_span?(span)
40
43
 
41
- # Shutdown the processor
42
- #
43
- # @param timeout [Integer, nil] Timeout in seconds (unused for this processor)
44
- # @return [Integer] Always returns 0 (no timeout needed for no-op)
45
- def shutdown(timeout: nil)
46
- # No-op - nothing to clean up
47
- # Return 0 to match OpenTelemetry SDK expectation (it finds max timeout from processors)
48
- _ = timeout # Suppress unused argument warning
49
- 0
50
- end
51
-
52
- # Force flush (no-op for this processor)
53
- #
54
- # @param timeout [Integer, nil] Timeout in seconds (unused for this processor)
55
- # @return [Integer] Always returns 0 (no timeout needed for no-op)
56
- def force_flush(timeout: nil)
57
- # No-op - nothing to flush
58
- # Return 0 to match OpenTelemetry SDK expectation (it finds max timeout from processors)
59
- _ = timeout # Suppress unused argument warning
60
- 0
44
+ super
61
45
  end
62
46
 
63
47
  private
64
48
 
65
- def build_default_trace_attributes(config)
66
- return {} unless config
49
+ # Sync mode relies on explicit `force_flush` calls, so keep the background flush
50
+ # interval long enough that it rarely fires on its own.
51
+ SYNC_SCHEDULE_DELAY_MS = 60_000
52
+ private_constant :SYNC_SCHEDULE_DELAY_MS
53
+
54
+ def schedule_delay_for(config)
55
+ config.tracing_async ? config.flush_interval * 1000 : SYNC_SCHEDULE_DELAY_MS
56
+ end
67
57
 
58
+ def build_default_trace_attributes(config)
68
59
  OtelAttributes.create_trace_attributes(
69
60
  { environment: config.environment, release: config.release }
70
61
  )
@@ -79,5 +70,15 @@ module Langfuse
79
70
  def apply_attributes(span, attributes)
80
71
  attributes.each { |key, value| span.set_attribute(key, value) }
81
72
  end
73
+
74
+ def should_export_span?(span)
75
+ @should_export_span.call(span)
76
+ rescue StandardError => e
77
+ @logger.error(
78
+ "Langfuse tracing dropped span '#{span.name}' because should_export_span raised: " \
79
+ "#{e.class}: #{e.message}"
80
+ )
81
+ false
82
+ end
82
83
  end
83
84
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mustache"
3
+ require_relative "prompt_renderer"
4
4
 
5
5
  module Langfuse
6
6
  # Text prompt client for compiling text prompts with variable substitution
@@ -38,11 +38,21 @@ module Langfuse
38
38
  # @return [String] Raw prompt template string
39
39
  attr_reader :prompt
40
40
 
41
+ # @return [String, nil] Optional commit message for this prompt version
42
+ attr_reader :commit_message
43
+
44
+ # @return [Hash, nil] Optional dependency resolution graph for composed prompts
45
+ attr_reader :resolution_graph
46
+
47
+ # @return [Boolean] Whether this client uses caller-provided fallback content
48
+ attr_reader :is_fallback
49
+
41
50
  # Initialize a new text prompt client
42
51
  #
43
52
  # @param prompt_data [Hash] The prompt data from the API
53
+ # @param is_fallback [Boolean] Whether this client wraps caller-provided fallback content
44
54
  # @raise [ArgumentError] if prompt data is invalid
45
- def initialize(prompt_data)
55
+ def initialize(prompt_data, is_fallback: false)
46
56
  validate_prompt_data!(prompt_data)
47
57
 
48
58
  @name = prompt_data["name"]
@@ -51,6 +61,14 @@ module Langfuse
51
61
  @labels = prompt_data["labels"] || []
52
62
  @tags = prompt_data["tags"] || []
53
63
  @config = prompt_data["config"] || {}
64
+ @commit_message = prompt_data["commitMessage"]
65
+ @resolution_graph = prompt_data["resolutionGraph"]
66
+ @is_fallback = is_fallback
67
+ end
68
+
69
+ # @return [String] Prompt type ("text")
70
+ def type
71
+ "text"
54
72
  end
55
73
 
56
74
  # Compile the prompt with variable substitution
@@ -64,7 +82,7 @@ module Langfuse
64
82
  def compile(**kwargs)
65
83
  return prompt if kwargs.empty?
66
84
 
67
- Mustache.render(prompt, kwargs)
85
+ PromptRenderer.render(prompt, kwargs)
68
86
  end
69
87
 
70
88
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Langfuse
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/langfuse.rb CHANGED
@@ -44,6 +44,8 @@ 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"
@@ -52,6 +54,7 @@ require_relative "langfuse/span_processor"
52
54
  require_relative "langfuse/observations"
53
55
  require_relative "langfuse/trace_id"
54
56
  require_relative "langfuse/score_client"
57
+ require_relative "langfuse/prompt_renderer"
55
58
  require_relative "langfuse/text_prompt_client"
56
59
  require_relative "langfuse/chat_prompt_client"
57
60
  require_relative "langfuse/timestamp_parser"
@@ -91,10 +94,6 @@ module Langfuse
91
94
  # end
92
95
  def configure
93
96
  yield(configuration)
94
-
95
- # Auto-initialize OpenTelemetry
96
- OtelSetup.setup(configuration)
97
-
98
97
  configuration
99
98
  end
100
99
 
@@ -105,6 +104,28 @@ module Langfuse
105
104
  @client ||= Client.new(configuration)
106
105
  end
107
106
 
107
+ # Return Langfuse's internal tracer provider for explicit global OpenTelemetry installation.
108
+ #
109
+ # @return [OpenTelemetry::SDK::Trace::TracerProvider]
110
+ # @raise [ConfigurationError] if tracing is not fully configured
111
+ #
112
+ # @example
113
+ # Langfuse.configure do |config|
114
+ # config.public_key = ENV["LANGFUSE_PUBLIC_KEY"]
115
+ # config.secret_key = ENV["LANGFUSE_SECRET_KEY"]
116
+ # end
117
+ #
118
+ # OpenTelemetry.tracer_provider = Langfuse.tracer_provider
119
+ def tracer_provider
120
+ unless tracing_config_ready?
121
+ raise ConfigurationError,
122
+ "Langfuse tracing is disabled until public_key, secret_key, and base_url are configured."
123
+ end
124
+
125
+ OtelSetup.setup(configuration) unless OtelSetup.initialized?
126
+ OtelSetup.tracer_provider
127
+ end
128
+
108
129
  # Shutdown Langfuse and flush any pending traces and scores
109
130
  #
110
131
  # Call this when shutting down your application to ensure
@@ -323,10 +344,14 @@ module Langfuse
323
344
  OtelSetup.shutdown(timeout: 5) if OtelSetup.initialized?
324
345
  @configuration = nil
325
346
  @client = nil
347
+ @noop_tracer = nil
348
+ @tracing_disabled_warning_emitted = false
326
349
  rescue StandardError
327
350
  # Ignore shutdown errors during reset (e.g., in tests)
328
351
  @configuration = nil
329
352
  @client = nil
353
+ @noop_tracer = nil
354
+ @tracing_disabled_warning_emitted = false
330
355
  end
331
356
 
332
357
  # Creates a new observation (root or child)
@@ -478,7 +503,10 @@ module Langfuse
478
503
  #
479
504
  # @return [OpenTelemetry::SDK::Trace::Tracer] The OTel tracer
480
505
  def otel_tracer
481
- OpenTelemetry.tracer_provider.tracer("langfuse-rb", Langfuse::VERSION)
506
+ return tracer_provider.tracer(LANGFUSE_TRACER_NAME, Langfuse::VERSION) if setup_tracing_if_ready
507
+
508
+ warn_tracing_disabled_once
509
+ noop_tracer
482
510
  end
483
511
 
484
512
  # Creates an OpenTelemetry span (root or child)
@@ -514,6 +542,47 @@ module Langfuse
514
542
  observation_class = OBSERVATION_TYPE_REGISTRY[type_str] || Span
515
543
  observation_class.new(otel_span, otel_tracer, attributes: attributes)
516
544
  end
545
+
546
+ # rubocop:disable Naming/PredicateMethod
547
+ def setup_tracing_if_ready
548
+ return true if OtelSetup.initialized?
549
+ return false unless tracing_config_ready?
550
+
551
+ OtelSetup.setup(configuration)
552
+ true
553
+ end
554
+ # rubocop:enable Naming/PredicateMethod
555
+
556
+ def tracing_config_ready?
557
+ configured?(configuration.public_key) &&
558
+ configured?(configuration.secret_key) &&
559
+ configured?(configuration.base_url)
560
+ end
561
+
562
+ def configured?(value)
563
+ !value.nil? && !value.empty?
564
+ end
565
+
566
+ def warn_tracing_disabled_once
567
+ return if @tracing_disabled_warning_emitted
568
+
569
+ tracing_warning_mutex.synchronize do
570
+ return if @tracing_disabled_warning_emitted
571
+
572
+ configuration.logger.warn(
573
+ "Langfuse tracing is disabled until public_key, secret_key, and base_url are configured."
574
+ )
575
+ @tracing_disabled_warning_emitted = true
576
+ end
577
+ end
578
+
579
+ def tracing_warning_mutex
580
+ @tracing_warning_mutex ||= Mutex.new
581
+ end
582
+
583
+ def noop_tracer
584
+ @noop_tracer ||= OpenTelemetry::Trace::TracerProvider.new.tracer(LANGFUSE_TRACER_NAME, Langfuse::VERSION)
585
+ end
517
586
  end
518
587
  # rubocop:enable Metrics/ClassLength
519
588
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: langfuse-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SimplePractice
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-04-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: faraday
@@ -177,9 +176,12 @@ files:
177
176
  - lib/langfuse/otel_attributes.rb
178
177
  - lib/langfuse/otel_setup.rb
179
178
  - lib/langfuse/prompt_cache.rb
179
+ - lib/langfuse/prompt_renderer.rb
180
180
  - lib/langfuse/propagation.rb
181
181
  - lib/langfuse/rails_cache_adapter.rb
182
+ - lib/langfuse/sampling.rb
182
183
  - lib/langfuse/score_client.rb
184
+ - lib/langfuse/span_filter.rb
183
185
  - lib/langfuse/span_processor.rb
184
186
  - lib/langfuse/stale_while_revalidate.rb
185
187
  - lib/langfuse/text_prompt_client.rb
@@ -196,7 +198,6 @@ metadata:
196
198
  source_code_uri: https://github.com/simplepractice/langfuse-rb
197
199
  changelog_uri: https://github.com/simplepractice/langfuse-rb/blob/main/CHANGELOG.md
198
200
  rubygems_mfa_required: 'true'
199
- post_install_message:
200
201
  rdoc_options: []
201
202
  require_paths:
202
203
  - lib
@@ -211,8 +212,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
212
  - !ruby/object:Gem::Version
212
213
  version: '0'
213
214
  requirements: []
214
- rubygems_version: 3.4.1
215
- signing_key:
215
+ rubygems_version: 4.0.8
216
216
  specification_version: 4
217
217
  summary: Ruby SDK for Langfuse - LLM observability and prompt management
218
218
  test_files: []