langfuse-rb 0.10.0 → 0.11.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 +44 -1
- data/lib/langfuse/api_client.rb +175 -509
- data/lib/langfuse/app_root_tracking.rb +164 -0
- data/lib/langfuse/cache_warmer.rb +21 -13
- data/lib/langfuse/chat_prompt_client.rb +21 -4
- data/lib/langfuse/client.rb +174 -229
- data/lib/langfuse/config.rb +308 -65
- data/lib/langfuse/evaluation.rb +8 -4
- data/lib/langfuse/exit_hook.rb +77 -0
- data/lib/langfuse/fork_safety.rb +71 -0
- data/lib/langfuse/masking_exporter.rb +98 -0
- data/lib/langfuse/observations.rb +2 -1
- data/lib/langfuse/otel_attributes.rb +1 -0
- data/lib/langfuse/otel_setup.rb +32 -44
- data/lib/langfuse/otel_span_batch.rb +113 -0
- data/lib/langfuse/otel_span_masking.rb +89 -0
- data/lib/langfuse/otel_span_patch_applier.rb +97 -0
- data/lib/langfuse/pending_score_queue.rb +62 -0
- data/lib/langfuse/prompt_cache.rb +11 -0
- data/lib/langfuse/prompt_cache_coordinator.rb +288 -0
- data/lib/langfuse/prompt_cache_events.rb +31 -10
- data/lib/langfuse/prompt_variables.rb +54 -0
- data/lib/langfuse/propagation.rb +101 -34
- data/lib/langfuse/rails_cache_adapter.rb +27 -2
- data/lib/langfuse/read_api.rb +242 -0
- data/lib/langfuse/resilient_metrics_reporter.rb +60 -0
- data/lib/langfuse/score_client.rb +209 -89
- data/lib/langfuse/score_value.rb +58 -0
- data/lib/langfuse/span_processor.rb +53 -4
- data/lib/langfuse/stale_while_revalidate.rb +3 -4
- data/lib/langfuse/text_prompt_client.rb +15 -6
- data/lib/langfuse/trace_export_guard.rb +47 -0
- data/lib/langfuse/traced_execution.rb +18 -12
- data/lib/langfuse/types.rb +15 -1
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +157 -44
- metadata +16 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "prompt_cache"
|
|
4
4
|
require_relative "stale_while_revalidate"
|
|
5
|
+
require_relative "fork_safety"
|
|
5
6
|
|
|
6
7
|
module Langfuse
|
|
7
8
|
# Rails.cache adapter for distributed caching with Redis
|
|
@@ -20,6 +21,14 @@ module Langfuse
|
|
|
20
21
|
|
|
21
22
|
GENERATION_MEMO_TTL_SECONDS = 1.0
|
|
22
23
|
|
|
24
|
+
# Check whether Rails has a configured cache store.
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
# @return [Boolean]
|
|
28
|
+
def self.available?
|
|
29
|
+
!!(defined?(Rails) && Rails.respond_to?(:cache) && !Rails.cache.nil?)
|
|
30
|
+
end
|
|
31
|
+
|
|
23
32
|
# @return [Integer] Time-to-live in seconds
|
|
24
33
|
attr_reader :ttl
|
|
25
34
|
|
|
@@ -61,6 +70,7 @@ module Langfuse
|
|
|
61
70
|
@generation_memo = {}
|
|
62
71
|
@generation_memo_mutex = Mutex.new
|
|
63
72
|
initialize_swr(refresh_threads: refresh_threads) if swr_enabled?
|
|
73
|
+
ForkSafety.register(self)
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
# Get a value from the cache
|
|
@@ -162,7 +172,10 @@ module Langfuse
|
|
|
162
172
|
current_generation_entries: nil,
|
|
163
173
|
orphaned_entries: nil,
|
|
164
174
|
total_entries: nil,
|
|
165
|
-
|
|
175
|
+
ttl: ttl,
|
|
176
|
+
size: size,
|
|
177
|
+
max_size: nil,
|
|
178
|
+
global_generation: safe_generation_value(global_generation_key),
|
|
166
179
|
unsupported_counts: CacheBackend::UNSUPPORTED_COUNT_KEYS
|
|
167
180
|
}
|
|
168
181
|
end
|
|
@@ -246,6 +259,12 @@ module Langfuse
|
|
|
246
259
|
|
|
247
260
|
private
|
|
248
261
|
|
|
262
|
+
# Replace inherited synchronization and worker state in the child process.
|
|
263
|
+
def reset_after_fork
|
|
264
|
+
@generation_memo = {}
|
|
265
|
+
@generation_memo_mutex = Mutex.new
|
|
266
|
+
end
|
|
267
|
+
|
|
249
268
|
# Implementation of StaleWhileRevalidate abstract methods
|
|
250
269
|
|
|
251
270
|
# Get value from cache (SWR interface)
|
|
@@ -351,6 +370,12 @@ module Langfuse
|
|
|
351
370
|
end
|
|
352
371
|
end
|
|
353
372
|
|
|
373
|
+
def safe_generation_value(key)
|
|
374
|
+
return nil unless Rails.cache.respond_to?(:read)
|
|
375
|
+
|
|
376
|
+
generation_value(key)
|
|
377
|
+
end
|
|
378
|
+
|
|
354
379
|
def bump_generation(key)
|
|
355
380
|
incremented = increment_generation(key)
|
|
356
381
|
if incremented
|
|
@@ -407,7 +432,7 @@ module Langfuse
|
|
|
407
432
|
# @raise [ConfigurationError] if Rails.cache is not available
|
|
408
433
|
# @return [void]
|
|
409
434
|
def validate_rails_cache!
|
|
410
|
-
return if
|
|
435
|
+
return if self.class.available?
|
|
411
436
|
|
|
412
437
|
raise ConfigurationError,
|
|
413
438
|
"Rails.cache is not available. Rails cache backend requires Rails with a configured cache store."
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Langfuse
|
|
7
|
+
# Read endpoints for the current Langfuse query surface.
|
|
8
|
+
#
|
|
9
|
+
# Implements v2 observation and metrics reads plus the v3 scores read.
|
|
10
|
+
# Mixed into {ApiClient}, whose private +request+ helper provides HTTP
|
|
11
|
+
# transport and error handling.
|
|
12
|
+
#
|
|
13
|
+
# @note The v2 observations and metrics endpoints require Langfuse v4. There
|
|
14
|
+
# is no fallback to legacy endpoints because their response and pagination
|
|
15
|
+
# semantics differ.
|
|
16
|
+
module ReadApi
|
|
17
|
+
# Ruby keyword argument -> camelCase query parameter mappings. Start-time
|
|
18
|
+
# bounds are handled separately because they need ISO 8601 formatting.
|
|
19
|
+
OBSERVATION_QUERY_PARAMS = {
|
|
20
|
+
trace_id: :traceId, fields: :fields, cursor: :cursor, limit: :limit,
|
|
21
|
+
filter: :filter, name: :name, user_id: :userId, type: :type,
|
|
22
|
+
level: :level, parent_observation_id: :parentObservationId,
|
|
23
|
+
is_root_observation: :isRootObservation, environment: :environment,
|
|
24
|
+
session_id: :sessionId, version: :version,
|
|
25
|
+
expand_metadata: :expandMetadata
|
|
26
|
+
}.freeze
|
|
27
|
+
private_constant :OBSERVATION_QUERY_PARAMS
|
|
28
|
+
|
|
29
|
+
SCORE_QUERY_PARAMS = {
|
|
30
|
+
limit: :limit, cursor: :cursor, fields: :fields, id: :id, name: :name,
|
|
31
|
+
source: :source, data_type: :dataType, environment: :environment,
|
|
32
|
+
config_id: :configId, queue_id: :queueId, author_user_id: :authorUserId,
|
|
33
|
+
value: :value, value_min: :valueMin, value_max: :valueMax,
|
|
34
|
+
trace_id: :traceId, session_id: :sessionId,
|
|
35
|
+
observation_id: :observationId, experiment_id: :experimentId
|
|
36
|
+
}.freeze
|
|
37
|
+
private_constant :SCORE_QUERY_PARAMS
|
|
38
|
+
|
|
39
|
+
# List observations with cursor-based pagination and field selection
|
|
40
|
+
#
|
|
41
|
+
# Delegates to +GET /api/public/v2/observations+ on Langfuse v4.
|
|
42
|
+
# Returns observation rows, not reconstructed trace objects. The full
|
|
43
|
+
# response envelope is preserved: +"data"+ holds the observation rows and
|
|
44
|
+
# +"meta"+ holds the pagination cursor for the next page.
|
|
45
|
+
#
|
|
46
|
+
# Broad reads must be bounded: unless +trace_id+ narrows the query, both
|
|
47
|
+
# +from_start_time+ and +to_start_time+ are required.
|
|
48
|
+
#
|
|
49
|
+
# @param from_start_time [Time, String, nil] Inclusive lower bound on observation start time
|
|
50
|
+
# @param to_start_time [Time, String, nil] Exclusive upper bound on observation start time
|
|
51
|
+
# @param trace_id [String, nil] Filter by trace ID
|
|
52
|
+
# @param fields [String, nil] Comma-separated field groups to include
|
|
53
|
+
# (core, basic, time, io, metadata, model, usage, prompt, metrics, trace_context)
|
|
54
|
+
# @param cursor [String, nil] Cursor from the previous response's meta for the next page
|
|
55
|
+
# @param limit [Integer, nil] Items per page (max 1000, default 50)
|
|
56
|
+
# @param filter [String, nil] JSON string with structured filter conditions;
|
|
57
|
+
# takes precedence over individual query parameter filters
|
|
58
|
+
# @param name [String, nil] Filter by observation name
|
|
59
|
+
# @param user_id [String, nil] Filter by user ID
|
|
60
|
+
# @param type [String, nil] Filter by observation type (e.g. "GENERATION", "SPAN")
|
|
61
|
+
# @param level [String, nil] Filter by level (e.g. "DEFAULT", "ERROR")
|
|
62
|
+
# @param parent_observation_id [String, nil] Filter by parent observation ID
|
|
63
|
+
# @param is_root_observation [Boolean, nil] Filter by logical root status
|
|
64
|
+
# @param environment [Array<String>, nil] Filter by one or more environments
|
|
65
|
+
# @param session_id [String, nil] Filter by session ID
|
|
66
|
+
# @param version [String, nil] Filter by observation version
|
|
67
|
+
# @param expand_metadata [String, nil] Comma-separated metadata keys to return non-truncated
|
|
68
|
+
# @return [Hash] Full response hash with "data" rows and "meta" cursor info
|
|
69
|
+
# @raise [ArgumentError] if the read is unbounded (no trace_id and missing start-time bounds)
|
|
70
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
71
|
+
# @raise [ApiError] for other API errors (including deployments without Langfuse v4)
|
|
72
|
+
#
|
|
73
|
+
# @example Bounded read of recent generations
|
|
74
|
+
# page = api_client.list_observations(
|
|
75
|
+
# from_start_time: Time.now - 3600,
|
|
76
|
+
# to_start_time: Time.now,
|
|
77
|
+
# type: "GENERATION",
|
|
78
|
+
# fields: "core,basic,usage"
|
|
79
|
+
# )
|
|
80
|
+
# page["data"].each { |obs| puts obs["id"] }
|
|
81
|
+
# next_cursor = page.dig("meta", "cursor")
|
|
82
|
+
# rubocop:disable Metrics/ParameterLists
|
|
83
|
+
def list_observations(from_start_time: nil, to_start_time: nil, trace_id: nil,
|
|
84
|
+
fields: nil, cursor: nil, limit: nil, filter: nil,
|
|
85
|
+
name: nil, user_id: nil, type: nil, level: nil,
|
|
86
|
+
parent_observation_id: nil, is_root_observation: nil,
|
|
87
|
+
environment: nil, session_id: nil, version: nil,
|
|
88
|
+
expand_metadata: nil)
|
|
89
|
+
validate_bounded_observation_read!(trace_id, from_start_time, to_start_time)
|
|
90
|
+
params = build_observations_params(
|
|
91
|
+
from_start_time: from_start_time, to_start_time: to_start_time,
|
|
92
|
+
trace_id: trace_id, fields: fields, cursor: cursor, limit: limit,
|
|
93
|
+
filter: filter, name: name, user_id: user_id, type: type, level: level,
|
|
94
|
+
parent_observation_id: parent_observation_id, environment: environment,
|
|
95
|
+
is_root_observation: is_root_observation, session_id: session_id,
|
|
96
|
+
version: version, expand_metadata: expand_metadata
|
|
97
|
+
)
|
|
98
|
+
request(
|
|
99
|
+
:get,
|
|
100
|
+
"/api/public/v2/observations",
|
|
101
|
+
params: params,
|
|
102
|
+
params_encoder: Faraday::FlatParamsEncoder
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
# rubocop:enable Metrics/ParameterLists
|
|
106
|
+
|
|
107
|
+
# Query aggregate metrics on Langfuse v4
|
|
108
|
+
#
|
|
109
|
+
# Delegates to +GET /api/public/v2/metrics+. Supports the +observations+,
|
|
110
|
+
# +scores-numeric+, +scores-categorical+, and +scores-boolean+ views.
|
|
111
|
+
#
|
|
112
|
+
# @param query [Hash, String] Metrics query. A Hash is JSON-encoded into
|
|
113
|
+
# the endpoint's +query+ parameter; a pre-encoded JSON String is passed
|
|
114
|
+
# through unchanged.
|
|
115
|
+
# @return [Hash] The parsed metrics response
|
|
116
|
+
# @raise [ArgumentError] if query is neither a Hash nor a String
|
|
117
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
118
|
+
# @raise [ApiError] for other API errors (including deployments without Langfuse v4)
|
|
119
|
+
#
|
|
120
|
+
# @example Count observations by name
|
|
121
|
+
# api_client.query_metrics(query: {
|
|
122
|
+
# view: "observations",
|
|
123
|
+
# metrics: [{ measure: "count", aggregation: "count" }],
|
|
124
|
+
# dimensions: [{ field: "name" }],
|
|
125
|
+
# fromTimestamp: "2026-07-01T00:00:00Z",
|
|
126
|
+
# toTimestamp: "2026-07-02T00:00:00Z"
|
|
127
|
+
# })
|
|
128
|
+
def query_metrics(query:)
|
|
129
|
+
request(:get, "/api/public/v2/metrics", params: { query: encode_metrics_query(query) })
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# List scores with polymorphic values (v3)
|
|
133
|
+
#
|
|
134
|
+
# Delegates to +GET /api/public/v3/scores+. The full response envelope is
|
|
135
|
+
# preserved: +"data"+ holds score rows and +"meta"+ holds the pagination
|
|
136
|
+
# cursor. Score values are polymorphic by +dataType+: NUMERIC scores return
|
|
137
|
+
# numbers, BOOLEAN scores return booleans, and CATEGORICAL, TEXT, and
|
|
138
|
+
# CORRECTION scores return strings.
|
|
139
|
+
#
|
|
140
|
+
# @param limit [Integer, nil] Items per page (max 100, default 50)
|
|
141
|
+
# @param cursor [String, nil] Cursor from the previous response's meta for the next page
|
|
142
|
+
# @param fields [String, nil] Comma-separated field groups in addition to core
|
|
143
|
+
# (details, subject, annotation)
|
|
144
|
+
# @param id [String, nil] Comma-separated score IDs to filter by
|
|
145
|
+
# @param name [String, nil] Comma-separated score names to filter by
|
|
146
|
+
# @param source [String, nil] Comma-separated score sources (e.g. API, ANNOTATION, EVAL)
|
|
147
|
+
# @param data_type [String, nil] Comma-separated data types
|
|
148
|
+
# (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION)
|
|
149
|
+
# @param environment [String, nil] Comma-separated environments to filter by
|
|
150
|
+
# @param config_id [String, nil] Comma-separated score config IDs
|
|
151
|
+
# @param queue_id [String, nil] Comma-separated annotation queue IDs
|
|
152
|
+
# @param author_user_id [String, nil] Comma-separated author user IDs
|
|
153
|
+
# @param value [String, nil] Comma-separated exact values (requires a single
|
|
154
|
+
# NUMERIC, BOOLEAN, or CATEGORICAL data_type)
|
|
155
|
+
# @param value_min [Numeric, nil] Inclusive lower bound (requires data_type: "NUMERIC")
|
|
156
|
+
# @param value_max [Numeric, nil] Inclusive upper bound (requires data_type: "NUMERIC")
|
|
157
|
+
# @param trace_id [String, nil] Comma-separated trace IDs (mutually exclusive
|
|
158
|
+
# with session_id and experiment_id)
|
|
159
|
+
# @param session_id [String, nil] Comma-separated session IDs
|
|
160
|
+
# @param observation_id [String, nil] Comma-separated observation IDs (requires trace_id)
|
|
161
|
+
# @param experiment_id [String, nil] Comma-separated dataset run (experiment) IDs
|
|
162
|
+
# @param from_timestamp [Time, String, nil] Inclusive lower bound on score timestamp
|
|
163
|
+
# @param to_timestamp [Time, String, nil] Exclusive upper bound on score timestamp
|
|
164
|
+
# @return [Hash] Full response hash with "data" rows and "meta" cursor info
|
|
165
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
166
|
+
# @raise [ApiError] for other API errors
|
|
167
|
+
#
|
|
168
|
+
# @example Read corrections for a trace
|
|
169
|
+
# page = api_client.list_scores(trace_id: trace_id, data_type: "CORRECTION", fields: "subject,details")
|
|
170
|
+
# page["data"].each { |score| puts score["value"] }
|
|
171
|
+
# rubocop:disable Metrics/ParameterLists
|
|
172
|
+
def list_scores(limit: nil, cursor: nil, fields: nil, id: nil, name: nil,
|
|
173
|
+
source: nil, data_type: nil, environment: nil, config_id: nil,
|
|
174
|
+
queue_id: nil, author_user_id: nil, value: nil, value_min: nil,
|
|
175
|
+
value_max: nil, trace_id: nil, session_id: nil,
|
|
176
|
+
observation_id: nil, experiment_id: nil,
|
|
177
|
+
from_timestamp: nil, to_timestamp: nil)
|
|
178
|
+
params = build_scores_params(
|
|
179
|
+
limit: limit, cursor: cursor, fields: fields, id: id, name: name,
|
|
180
|
+
source: source, data_type: data_type, environment: environment,
|
|
181
|
+
config_id: config_id, queue_id: queue_id, author_user_id: author_user_id,
|
|
182
|
+
value: value, value_min: value_min, value_max: value_max,
|
|
183
|
+
trace_id: trace_id, session_id: session_id, observation_id: observation_id,
|
|
184
|
+
experiment_id: experiment_id, from_timestamp: from_timestamp, to_timestamp: to_timestamp
|
|
185
|
+
)
|
|
186
|
+
request(:get, "/api/public/v3/scores", params: params)
|
|
187
|
+
end
|
|
188
|
+
# rubocop:enable Metrics/ParameterLists
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# v2 observation reads must always be bounded; an unbounded scan over the
|
|
193
|
+
# events table is rejected here rather than issued silently. Only values
|
|
194
|
+
# matching the documented query contract can satisfy the bound.
|
|
195
|
+
def validate_bounded_observation_read!(trace_id, from_start_time, to_start_time)
|
|
196
|
+
return if non_empty_string?(trace_id)
|
|
197
|
+
return if valid_query_time?(from_start_time) && valid_query_time?(to_start_time)
|
|
198
|
+
|
|
199
|
+
raise ArgumentError,
|
|
200
|
+
"from_start_time and to_start_time are required unless trace_id is provided"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def valid_query_time?(value)
|
|
204
|
+
non_empty_string?(format_query_time(value))
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def non_empty_string?(value)
|
|
208
|
+
value.is_a?(String) && !value.strip.empty?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def build_observations_params(**options)
|
|
212
|
+
map_query_params(OBSERVATION_QUERY_PARAMS, options).merge(
|
|
213
|
+
fromStartTime: format_query_time(options[:from_start_time]),
|
|
214
|
+
toStartTime: format_query_time(options[:to_start_time])
|
|
215
|
+
).compact
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_scores_params(**options)
|
|
219
|
+
map_query_params(SCORE_QUERY_PARAMS, options).merge(
|
|
220
|
+
fromTimestamp: format_query_time(options[:from_timestamp]),
|
|
221
|
+
toTimestamp: format_query_time(options[:to_timestamp])
|
|
222
|
+
).compact
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def map_query_params(mapping, options)
|
|
226
|
+
mapping.to_h { |ruby_key, api_key| [api_key, options[ruby_key]] }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def encode_metrics_query(query)
|
|
230
|
+
case query
|
|
231
|
+
when Hash then JSON.generate(query)
|
|
232
|
+
when String then query
|
|
233
|
+
else raise ArgumentError, "query must be a Hash or JSON String, got #{query.class}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Accepts Time-like values or pre-formatted ISO 8601 strings.
|
|
238
|
+
def format_query_time(value)
|
|
239
|
+
value.respond_to?(:iso8601) ? value.iso8601 : value
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/atomic/atomic_boolean"
|
|
4
|
+
|
|
5
|
+
module Langfuse
|
|
6
|
+
# Protects trace processing from failures in an application-owned metrics reporter.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
class ResilientMetricsReporter
|
|
10
|
+
def self.wrap(reporter, logger:)
|
|
11
|
+
return if reporter.nil?
|
|
12
|
+
|
|
13
|
+
new(reporter, logger: logger)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(reporter, logger:)
|
|
17
|
+
@reporter = reporter
|
|
18
|
+
@logger = logger
|
|
19
|
+
@warning_emitted = Concurrent::AtomicBoolean.new(false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_to_counter(metric, increment: 1, labels: {})
|
|
23
|
+
safely(:add_to_counter) do
|
|
24
|
+
@reporter.add_to_counter(metric, increment: increment, labels: labels)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def record_value(metric, value:, labels: {})
|
|
29
|
+
safely(:record_value) do
|
|
30
|
+
@reporter.record_value(metric, value: value, labels: labels)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def observe_value(metric, value:, labels: {})
|
|
35
|
+
safely(:observe_value) do
|
|
36
|
+
@reporter.observe_value(metric, value: value, labels: labels)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def safely(method_name)
|
|
43
|
+
yield
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
warn_once(method_name, e.class)
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def warn_once(method_name, error_class)
|
|
50
|
+
return unless @warning_emitted.make_true
|
|
51
|
+
|
|
52
|
+
@logger.warn(
|
|
53
|
+
"Langfuse metrics_reporter ##{method_name} failed with #{error_class}; " \
|
|
54
|
+
"future reporter failures will be suppressed"
|
|
55
|
+
)
|
|
56
|
+
rescue StandardError
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|