strongmind-platform-sdk 3.27.5 → 3.28.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 +10 -0
- data/Gemfile.lock +43 -8
- data/lib/platform_sdk/observability/langfuse/coercions.rb +25 -0
- data/lib/platform_sdk/observability/langfuse/configuration.rb +150 -0
- data/lib/platform_sdk/observability/langfuse/null_span_exporter.rb +30 -0
- data/lib/platform_sdk/observability/langfuse/recorder.rb +97 -0
- data/lib/platform_sdk/observability/langfuse/sidekiq_lifecycle.rb +33 -0
- data/lib/platform_sdk/observability/langfuse/spec_support.rb +88 -0
- data/lib/platform_sdk/observability/langfuse/traceable.rb +105 -0
- data/lib/platform_sdk/observability/langfuse.rb +123 -0
- data/lib/platform_sdk/observability.rb +8 -0
- data/lib/platform_sdk/version.rb +2 -2
- data/lib/platform_sdk.rb +1 -0
- metadata +39 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97b493cd879ebbc714026b5d819ddfe8366453b0fb3d4e037ccf653dd6bf852e
|
|
4
|
+
data.tar.gz: bf03370e3b62cdc6732dcb9d1a0966782afa45722ac6f2bf15e121978c0d1fce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37074d6a5591e41ae57301530ffaed72b49bb21d07298ebd2c2434fcfe50cbd623f24b1fc6db4ee991b7b19c3eaa503014568a8e7b2548766265c8c90589dd0e
|
|
7
|
+
data.tar.gz: a539e7cad181064b9d617c020241a356dafb96102bfb2d1a63398476885c400b6736dfad46ab0b941e314654f6181960a46552bde1a3556ab0ea281b6a97e347
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [3.28.0] - 2026-05-05
|
|
4
|
+
|
|
5
|
+
- Add `PlatformSdk::Observability::Langfuse` module providing OpenTelemetry-based tracing exported to Langfuse Cloud via OTLP.
|
|
6
|
+
- `TracerProvider` is isolated (not installed globally) and coexists safely with `ddtrace`.
|
|
7
|
+
- Add `Traceable` Sidekiq concern (`include PlatformSdk::Observability::Langfuse::Traceable`) that wraps `perform` in a parent span.
|
|
8
|
+
- Add `record_generation` module method emitting `gen_ai.*` child spans inside an active trace.
|
|
9
|
+
- Add `NullSpanExporter` for use in tests (no network calls).
|
|
10
|
+
- Sidekiq shutdown flush hook is installed automatically when `configure` is called with valid keys.
|
|
11
|
+
- Add runtime dependencies: `opentelemetry-sdk`, `opentelemetry-exporter-otlp`.
|
|
12
|
+
|
|
3
13
|
## [0.1.0] - 2022-05-13
|
|
4
14
|
|
|
5
15
|
- Initial release
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
strongmind-platform-sdk (3.
|
|
4
|
+
strongmind-platform-sdk (3.28.0)
|
|
5
5
|
asset_sync
|
|
6
6
|
aws-sdk-cloudwatch
|
|
7
7
|
aws-sdk-secretsmanager (~> 1.66)
|
|
@@ -11,6 +11,8 @@ PATH
|
|
|
11
11
|
jwt
|
|
12
12
|
learnosity-sdk (~> 0.2.2)
|
|
13
13
|
multi_json
|
|
14
|
+
opentelemetry-exporter-otlp (~> 0.27)
|
|
15
|
+
opentelemetry-sdk (~> 1.10)
|
|
14
16
|
rails (>= 7.1)
|
|
15
17
|
sentry-ruby
|
|
16
18
|
sidekiq
|
|
@@ -97,8 +99,9 @@ GEM
|
|
|
97
99
|
tzinfo (~> 2.0)
|
|
98
100
|
addressable (2.8.7)
|
|
99
101
|
public_suffix (>= 2.0.2, < 7.0)
|
|
100
|
-
asset_sync (2.19.
|
|
102
|
+
asset_sync (2.19.3)
|
|
101
103
|
activemodel (>= 4.1.0)
|
|
104
|
+
cgi
|
|
102
105
|
fog-core
|
|
103
106
|
mime-types (>= 2.99)
|
|
104
107
|
unf
|
|
@@ -121,6 +124,7 @@ GEM
|
|
|
121
124
|
base64 (0.2.0)
|
|
122
125
|
bigdecimal (3.1.8)
|
|
123
126
|
builder (3.3.0)
|
|
127
|
+
cgi (0.5.1)
|
|
124
128
|
coderay (1.1.3)
|
|
125
129
|
concurrent-ruby (1.3.3)
|
|
126
130
|
connection_pool (2.4.1)
|
|
@@ -134,7 +138,7 @@ GEM
|
|
|
134
138
|
erubi (1.13.0)
|
|
135
139
|
ethon (0.16.0)
|
|
136
140
|
ffi (>= 1.15.0)
|
|
137
|
-
excon (1.
|
|
141
|
+
excon (1.4.2)
|
|
138
142
|
logger
|
|
139
143
|
factory_bot (6.4.6)
|
|
140
144
|
activesupport (>= 5.0.0)
|
|
@@ -150,7 +154,7 @@ GEM
|
|
|
150
154
|
ffi (1.17.0)
|
|
151
155
|
ffi (1.17.0-x86_64-darwin)
|
|
152
156
|
ffi (1.17.0-x86_64-linux-gnu)
|
|
153
|
-
fog-aws (3.33.
|
|
157
|
+
fog-aws (3.33.1)
|
|
154
158
|
base64 (>= 0.2, < 0.4)
|
|
155
159
|
fog-core (~> 2.6)
|
|
156
160
|
fog-json (~> 1.1)
|
|
@@ -166,10 +170,21 @@ GEM
|
|
|
166
170
|
fog-xml (0.1.5)
|
|
167
171
|
fog-core
|
|
168
172
|
nokogiri (>= 1.5.11, < 2.0.0)
|
|
169
|
-
formatador (1.2.
|
|
173
|
+
formatador (1.2.3)
|
|
170
174
|
reline
|
|
171
175
|
globalid (1.3.0)
|
|
172
176
|
activesupport (>= 6.1)
|
|
177
|
+
google-protobuf (4.33.6)
|
|
178
|
+
bigdecimal
|
|
179
|
+
rake (>= 13)
|
|
180
|
+
google-protobuf (4.33.6-x86_64-darwin)
|
|
181
|
+
bigdecimal
|
|
182
|
+
rake (>= 13)
|
|
183
|
+
google-protobuf (4.33.6-x86_64-linux-gnu)
|
|
184
|
+
bigdecimal
|
|
185
|
+
rake (>= 13)
|
|
186
|
+
googleapis-common-protos-types (1.22.0)
|
|
187
|
+
google-protobuf (~> 4.26)
|
|
173
188
|
hashdiff (1.1.0)
|
|
174
189
|
i18n (1.14.5)
|
|
175
190
|
concurrent-ruby (~> 1.0)
|
|
@@ -198,7 +213,7 @@ GEM
|
|
|
198
213
|
mime-types (3.7.0)
|
|
199
214
|
logger
|
|
200
215
|
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
|
201
|
-
mime-types-data (3.
|
|
216
|
+
mime-types-data (3.2026.0317)
|
|
202
217
|
mini_mime (1.1.5)
|
|
203
218
|
mini_portile2 (2.8.7)
|
|
204
219
|
minitest (5.24.1)
|
|
@@ -206,7 +221,7 @@ GEM
|
|
|
206
221
|
mutex_m (0.2.0)
|
|
207
222
|
net-http (0.4.1)
|
|
208
223
|
uri
|
|
209
|
-
net-imap (0.
|
|
224
|
+
net-imap (0.6.3)
|
|
210
225
|
date
|
|
211
226
|
net-protocol
|
|
212
227
|
net-pop (0.1.2)
|
|
@@ -223,6 +238,26 @@ GEM
|
|
|
223
238
|
racc (~> 1.4)
|
|
224
239
|
nokogiri (1.16.7-x86_64-linux)
|
|
225
240
|
racc (~> 1.4)
|
|
241
|
+
opentelemetry-api (1.8.0)
|
|
242
|
+
logger
|
|
243
|
+
opentelemetry-common (0.23.0)
|
|
244
|
+
opentelemetry-api (~> 1.0)
|
|
245
|
+
opentelemetry-exporter-otlp (0.32.0)
|
|
246
|
+
google-protobuf (>= 3.18)
|
|
247
|
+
googleapis-common-protos-types (~> 1.3)
|
|
248
|
+
opentelemetry-api (~> 1.1)
|
|
249
|
+
opentelemetry-common (~> 0.20)
|
|
250
|
+
opentelemetry-sdk (~> 1.10)
|
|
251
|
+
opentelemetry-semantic_conventions
|
|
252
|
+
opentelemetry-registry (0.4.0)
|
|
253
|
+
opentelemetry-api (~> 1.1)
|
|
254
|
+
opentelemetry-sdk (1.10.0)
|
|
255
|
+
opentelemetry-api (~> 1.1)
|
|
256
|
+
opentelemetry-common (~> 0.20)
|
|
257
|
+
opentelemetry-registry (~> 0.2)
|
|
258
|
+
opentelemetry-semantic_conventions
|
|
259
|
+
opentelemetry-semantic_conventions (1.36.0)
|
|
260
|
+
opentelemetry-api (~> 1.0)
|
|
226
261
|
parallel (1.25.1)
|
|
227
262
|
parser (3.3.4.0)
|
|
228
263
|
ast (~> 2.4.1)
|
|
@@ -342,7 +377,7 @@ GEM
|
|
|
342
377
|
ffi (~> 1.1)
|
|
343
378
|
thor (1.3.1)
|
|
344
379
|
timecop (0.9.10)
|
|
345
|
-
timeout (0.
|
|
380
|
+
timeout (0.6.1)
|
|
346
381
|
typhoeus (1.4.1)
|
|
347
382
|
ethon (>= 0.9.0)
|
|
348
383
|
tzinfo (2.0.6)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlatformSdk
|
|
4
|
+
module Observability
|
|
5
|
+
module Langfuse
|
|
6
|
+
# Shared coercion helpers used by Configuration, Recorder, and the
|
|
7
|
+
# Traceable PerformWrapper. Extracted to a single home so the
|
|
8
|
+
# OpenTelemetry attribute-validator workaround (`String.new` to
|
|
9
|
+
# produce a plain String rather than a subclass) lives in one place.
|
|
10
|
+
module Coercions
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Returns `nil` for nil; otherwise a plain `String` (not a
|
|
14
|
+
# subclass). OTel's attribute validator uses `instance_of?(String)`
|
|
15
|
+
# — subclasses like `ActiveSupport::SafeBuffer` fail validation and
|
|
16
|
+
# crash `Resource.create`.
|
|
17
|
+
def coerce_string(value)
|
|
18
|
+
return nil if value.nil?
|
|
19
|
+
|
|
20
|
+
String.new(value.to_s)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'opentelemetry/sdk'
|
|
4
|
+
require 'opentelemetry/exporter/otlp'
|
|
5
|
+
require 'base64'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
module PlatformSdk
|
|
9
|
+
module Observability
|
|
10
|
+
module Langfuse
|
|
11
|
+
class Configuration
|
|
12
|
+
DEFAULT_BASE_URL = 'https://us.cloud.langfuse.com' # US region; set LANGFUSE_BASE_URL for eu.cloud.langfuse.com, jp.cloud.langfuse.com, etc.
|
|
13
|
+
DEFAULT_PROMPT_LABEL = 'latest'
|
|
14
|
+
DEFAULT_BATCH_INTERVAL_MS = 5_000
|
|
15
|
+
OTLP_TRACES_PATH = '/api/public/otel/v1/traces'
|
|
16
|
+
|
|
17
|
+
attr_reader :app_name, :environment, :prompt_label, :exporter
|
|
18
|
+
|
|
19
|
+
def initialize(app_name:, environment: nil, prompt_label: nil, exporter: nil)
|
|
20
|
+
@app_name = coerce_string(app_name)
|
|
21
|
+
@environment = coerce_string(environment || ENV.fetch('RAILS_ENV', 'development'))
|
|
22
|
+
@prompt_label = coerce_string(prompt_label || ENV.fetch('LANGFUSE_PROMPT_LABEL', DEFAULT_PROMPT_LABEL))
|
|
23
|
+
@exporter = exporter
|
|
24
|
+
@enabled = compute_enabled
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def enabled?
|
|
28
|
+
@enabled
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def otlp_endpoint
|
|
32
|
+
"#{base_url}#{OTLP_TRACES_PATH}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def default_resource_attributes
|
|
36
|
+
{
|
|
37
|
+
'service.name' => app_name,
|
|
38
|
+
'deployment.environment' => environment
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def tracer
|
|
43
|
+
@tracer ||= tracer_provider.tracer('platform_sdk.observability.langfuse', PlatformSdk::VERSION)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tracer_provider
|
|
47
|
+
@tracer_provider ||= build_tracer_provider
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def force_flush_and_shutdown
|
|
51
|
+
return if @shut_down
|
|
52
|
+
return unless @tracer_provider
|
|
53
|
+
|
|
54
|
+
@tracer_provider.force_flush(timeout: 5)
|
|
55
|
+
@tracer_provider.shutdown(timeout: 5)
|
|
56
|
+
@shut_down = true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Snapshot the enabled state at construction time so credentials
|
|
62
|
+
# mutated in ENV after `configure` don't silently flip behavior
|
|
63
|
+
# mid-process. Callers who need to roll keys must re-`configure`.
|
|
64
|
+
def compute_enabled
|
|
65
|
+
return false if ENV['LANGFUSE_ENABLED'] == 'false'
|
|
66
|
+
|
|
67
|
+
public_key.to_s != '' && secret_key.to_s != ''
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def coerce_string(value)
|
|
71
|
+
Coercions.coerce_string(value)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def public_key
|
|
75
|
+
ENV['LANGFUSE_PUBLIC_KEY']
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def secret_key
|
|
79
|
+
ENV['LANGFUSE_SECRET_KEY']
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def base_url
|
|
83
|
+
@base_url ||= validated_base_url
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validated_base_url
|
|
87
|
+
url = ENV.fetch('LANGFUSE_BASE_URL', DEFAULT_BASE_URL)
|
|
88
|
+
uri = URI.parse(url)
|
|
89
|
+
unless %w[http https].include?(uri.scheme) && !uri.host.nil?
|
|
90
|
+
raise ConfigurationError, "LANGFUSE_BASE_URL must be an http(s) URL with a host (got: #{url.inspect})"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
url
|
|
94
|
+
rescue URI::InvalidURIError => e
|
|
95
|
+
raise ConfigurationError, "LANGFUSE_BASE_URL is not a valid URI: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def batch_interval_ms
|
|
99
|
+
ENV.fetch('LANGFUSE_BATCH_INTERVAL_MS', DEFAULT_BATCH_INTERVAL_MS).to_i
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Builds an isolated TracerProvider that is intentionally NOT installed
|
|
103
|
+
# as the OpenTelemetry global (no `OpenTelemetry.tracer_provider =`,
|
|
104
|
+
# no `OpenTelemetry::SDK.configure`). Datadog APM (ddtrace) hooks the
|
|
105
|
+
# global tracer provider via `require 'datadog/opentelemetry'`; keeping
|
|
106
|
+
# this provider isolated lets Langfuse and ddtrace coexist without
|
|
107
|
+
# fighting over the same global state. See:
|
|
108
|
+
# https://github.com/DataDog/dd-trace-rb/blob/master/docs/OpenTelemetry.md
|
|
109
|
+
def build_tracer_provider
|
|
110
|
+
provider = OpenTelemetry::SDK::Trace::TracerProvider.new(
|
|
111
|
+
resource: OpenTelemetry::SDK::Resources::Resource.create(default_resource_attributes),
|
|
112
|
+
span_limits: span_limits
|
|
113
|
+
)
|
|
114
|
+
provider.add_span_processor(span_processor)
|
|
115
|
+
provider
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def span_limits
|
|
119
|
+
OpenTelemetry::SDK::Trace::SpanLimits.new(
|
|
120
|
+
attribute_length_limit: Recorder::MAX_ATTRIBUTE_BYTES
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def span_processor
|
|
125
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
|
126
|
+
exporter || build_otlp_exporter,
|
|
127
|
+
schedule_delay: batch_interval_ms,
|
|
128
|
+
max_queue_size: 2048,
|
|
129
|
+
max_export_batch_size: 512
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_otlp_exporter
|
|
134
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new(
|
|
135
|
+
endpoint: otlp_endpoint,
|
|
136
|
+
headers: {
|
|
137
|
+
'Authorization' => basic_auth_header,
|
|
138
|
+
'x-langfuse-ingestion-version' => '4'
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def basic_auth_header
|
|
144
|
+
token = Base64.strict_encode64("#{public_key}:#{secret_key}")
|
|
145
|
+
"Basic #{token}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'opentelemetry/sdk'
|
|
4
|
+
|
|
5
|
+
module PlatformSdk
|
|
6
|
+
module Observability
|
|
7
|
+
module Langfuse
|
|
8
|
+
# Span exporter for tests and dev. Wraps OTel's `InMemorySpanExporter`
|
|
9
|
+
# with two minor adjustments:
|
|
10
|
+
#
|
|
11
|
+
# 1. `shutdown` is a no-op (the base class clears `@finished_spans`
|
|
12
|
+
# on shutdown). Our tests pervasively call
|
|
13
|
+
# `configuration.force_flush_and_shutdown` and then assert on
|
|
14
|
+
# exported spans — preserving the buffer across shutdown is
|
|
15
|
+
# what makes that ergonomics work.
|
|
16
|
+
# 2. `exported_spans` / `clear` aliases for `finished_spans` /
|
|
17
|
+
# `reset` so the names line up with how we talk about this
|
|
18
|
+
# thing elsewhere in the SDK (and don't change call sites in
|
|
19
|
+
# consumer apps).
|
|
20
|
+
class NullSpanExporter < OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter
|
|
21
|
+
alias exported_spans finished_spans
|
|
22
|
+
alias clear reset
|
|
23
|
+
|
|
24
|
+
def shutdown(timeout: nil)
|
|
25
|
+
OpenTelemetry::SDK::Trace::Export::SUCCESS
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module PlatformSdk
|
|
6
|
+
module Observability
|
|
7
|
+
module Langfuse
|
|
8
|
+
module Recorder
|
|
9
|
+
MAX_ATTRIBUTE_BYTES = 262_144 # 256KB
|
|
10
|
+
TRUNCATION_SUFFIX = "…[truncated]"
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/ParameterLists
|
|
13
|
+
def self.record_generation(name:, model:, input:, output:, prompt_name: nil, prompt_version: nil, usage: nil, provider: nil)
|
|
14
|
+
return unless Langfuse.enabled?
|
|
15
|
+
return unless current_span_active?
|
|
16
|
+
|
|
17
|
+
tracer = Langfuse.tracer
|
|
18
|
+
tracer.in_span(name, attributes: build_attributes(model, input, output, prompt_name, prompt_version, usage, provider)) do
|
|
19
|
+
# span body intentionally empty — attributes carry the data
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
# rubocop:enable Metrics/ParameterLists
|
|
23
|
+
|
|
24
|
+
def self.current_span_active?
|
|
25
|
+
OpenTelemetry::Trace.current_span.context.valid?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# rubocop:disable Metrics/ParameterLists
|
|
29
|
+
def self.build_attributes(model, input, output, prompt_name, prompt_version, usage, provider = nil)
|
|
30
|
+
attrs = {
|
|
31
|
+
'langfuse.observation.type' => 'generation',
|
|
32
|
+
# `gen_ai.operation.name` is a required attribute in the OTel
|
|
33
|
+
# GenAI semantic conventions. We only emit chat-style
|
|
34
|
+
# completions today, so this is a constant.
|
|
35
|
+
'gen_ai.operation.name' => 'chat',
|
|
36
|
+
'gen_ai.request.model' => coerce_string(model),
|
|
37
|
+
'gen_ai.prompt' => stringify(input),
|
|
38
|
+
'gen_ai.completion' => stringify(output)
|
|
39
|
+
}
|
|
40
|
+
attrs['gen_ai.provider.name'] = coerce_string(provider) if provider
|
|
41
|
+
if prompt_name && prompt_version
|
|
42
|
+
attrs['langfuse.observation.prompt.name'] = coerce_string(prompt_name)
|
|
43
|
+
attrs['langfuse.observation.prompt.version'] = coerce_simple(prompt_version)
|
|
44
|
+
end
|
|
45
|
+
if usage.is_a?(Hash)
|
|
46
|
+
usage = usage.transform_keys(&:to_sym)
|
|
47
|
+
input_tokens = coerce_integer(usage[:input_tokens])
|
|
48
|
+
output_tokens = coerce_integer(usage[:output_tokens])
|
|
49
|
+
attrs['gen_ai.usage.input_tokens'] = input_tokens unless input_tokens.nil?
|
|
50
|
+
attrs['gen_ai.usage.output_tokens'] = output_tokens unless output_tokens.nil?
|
|
51
|
+
end
|
|
52
|
+
attrs.compact
|
|
53
|
+
end
|
|
54
|
+
# rubocop:enable Metrics/ParameterLists
|
|
55
|
+
|
|
56
|
+
def self.coerce_string(value)
|
|
57
|
+
Coercions.coerce_string(value)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Pass numerics/booleans through; coerce everything else to plain String.
|
|
61
|
+
def self.coerce_simple(value)
|
|
62
|
+
return nil if value.nil?
|
|
63
|
+
return value if value.instance_of?(Integer) || value.instance_of?(Float)
|
|
64
|
+
return value if value.instance_of?(TrueClass) || value.instance_of?(FalseClass)
|
|
65
|
+
|
|
66
|
+
String.new(value.to_s)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.coerce_integer(value)
|
|
70
|
+
return nil if value.nil?
|
|
71
|
+
|
|
72
|
+
Integer(value)
|
|
73
|
+
rescue ArgumentError, TypeError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.stringify(value)
|
|
78
|
+
str = value.is_a?(String) ? value : value.to_json
|
|
79
|
+
truncated = truncate(str)
|
|
80
|
+
# Force a plain String — value.to_json could be a String subclass
|
|
81
|
+
# in some odd cases, and value.is_a?(String) lets subclasses through.
|
|
82
|
+
String.new(truncated)
|
|
83
|
+
rescue StandardError
|
|
84
|
+
String.new(truncate(value.to_s))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.truncate(str)
|
|
88
|
+
return str if str.bytesize <= MAX_ATTRIBUTE_BYTES
|
|
89
|
+
|
|
90
|
+
# Slice on byte boundary then strip any partial multibyte sequence by encoding-coercing.
|
|
91
|
+
truncated = str.byteslice(0, MAX_ATTRIBUTE_BYTES).scrub('')
|
|
92
|
+
"#{truncated}#{TRUNCATION_SUFFIX}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'sidekiq'
|
|
4
|
+
|
|
5
|
+
module PlatformSdk
|
|
6
|
+
module Observability
|
|
7
|
+
module Langfuse
|
|
8
|
+
module SidekiqLifecycle
|
|
9
|
+
@installed = false
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def install!
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
return if @installed
|
|
16
|
+
|
|
17
|
+
@installed = true
|
|
18
|
+
::Sidekiq.configure_server do |config|
|
|
19
|
+
config.on(:shutdown) do
|
|
20
|
+
PlatformSdk::Observability::Langfuse.configuration&.force_flush_and_shutdown
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def reset!
|
|
27
|
+
@mutex.synchronize { @installed = false }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Test-environment wiring for PlatformSdk::Observability::Langfuse.
|
|
4
|
+
#
|
|
5
|
+
# Consuming apps add this one line to their `spec/rails_helper.rb` (or
|
|
6
|
+
# `spec_helper.rb`):
|
|
7
|
+
#
|
|
8
|
+
# require 'platform_sdk/observability/langfuse/spec_support'
|
|
9
|
+
# PlatformSdk::Observability::Langfuse::SpecSupport.install!(app_name: 'my-app-test')
|
|
10
|
+
#
|
|
11
|
+
# What it does:
|
|
12
|
+
# * Configures the Langfuse module with a NullSpanExporter so no live network
|
|
13
|
+
# calls happen during tests.
|
|
14
|
+
# * Clears the exporter between examples so spans don't bleed across tests.
|
|
15
|
+
# * Mixes a `langfuse_exported_spans` helper into every example so specs can
|
|
16
|
+
# flush + read the exported spans for assertions.
|
|
17
|
+
|
|
18
|
+
module PlatformSdk
|
|
19
|
+
module Observability
|
|
20
|
+
module Langfuse
|
|
21
|
+
module SpecSupport
|
|
22
|
+
DEFAULT_TEST_PUBLIC_KEY = 'pk-test'
|
|
23
|
+
DEFAULT_TEST_SECRET_KEY = 'sk-test'
|
|
24
|
+
DEFAULT_TEST_ENVIRONMENT = 'test'
|
|
25
|
+
|
|
26
|
+
# Register the RSpec lifecycle hooks. Call once from `rails_helper.rb`
|
|
27
|
+
# (or `spec_helper.rb`). The actual NullSpanExporter wiring happens in
|
|
28
|
+
# `before(:suite)`; this method just sets up the hooks.
|
|
29
|
+
def self.install!(app_name:, environment: DEFAULT_TEST_ENVIRONMENT)
|
|
30
|
+
require 'rspec/core'
|
|
31
|
+
|
|
32
|
+
RSpec.configure do |config|
|
|
33
|
+
config.before(:suite) do
|
|
34
|
+
PlatformSdk::Observability::Langfuse::SpecSupport.apply!(
|
|
35
|
+
app_name: app_name,
|
|
36
|
+
environment: environment
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
config.before(:each) do
|
|
40
|
+
PlatformSdk::Observability::Langfuse::SpecSupport.exporter&.clear
|
|
41
|
+
end
|
|
42
|
+
config.include TestHelpers
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Configure the Langfuse module with a NullSpanExporter. Separate from
|
|
47
|
+
# `install!` so it can be invoked directly in specs that need to test
|
|
48
|
+
# SpecSupport itself, or by apps that drive their own lifecycle.
|
|
49
|
+
def self.apply!(app_name:, environment: DEFAULT_TEST_ENVIRONMENT)
|
|
50
|
+
ENV['LANGFUSE_PUBLIC_KEY'] ||= DEFAULT_TEST_PUBLIC_KEY
|
|
51
|
+
ENV['LANGFUSE_SECRET_KEY'] ||= DEFAULT_TEST_SECRET_KEY
|
|
52
|
+
|
|
53
|
+
PlatformSdk::Observability::Langfuse.reset!
|
|
54
|
+
PlatformSdk::Observability::Langfuse.configure(
|
|
55
|
+
app_name: app_name,
|
|
56
|
+
environment: environment,
|
|
57
|
+
exporter: NullSpanExporter.new
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The currently-configured NullSpanExporter, or nil if `install!` was
|
|
62
|
+
# not called or the configuration was reset.
|
|
63
|
+
def self.exporter
|
|
64
|
+
config = PlatformSdk::Observability::Langfuse.configuration
|
|
65
|
+
return nil unless config
|
|
66
|
+
|
|
67
|
+
config.exporter
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
module TestHelpers
|
|
71
|
+
# Force-flush the BatchSpanProcessor and return every span the
|
|
72
|
+
# NullSpanExporter has captured during the current example. Use this
|
|
73
|
+
# to assert that a code path produced the expected spans.
|
|
74
|
+
#
|
|
75
|
+
# Example:
|
|
76
|
+
# spans = langfuse_exported_spans
|
|
77
|
+
# parent = spans.find { |s| s.name == 'MyJob' }
|
|
78
|
+
# expect(parent.attributes['langfuse.trace.tags']).to include('test')
|
|
79
|
+
def langfuse_exported_spans
|
|
80
|
+
config = PlatformSdk::Observability::Langfuse.configuration
|
|
81
|
+
config.tracer_provider.force_flush(timeout: 5)
|
|
82
|
+
PlatformSdk::Observability::Langfuse::SpecSupport.exporter.exported_spans
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
module PlatformSdk
|
|
6
|
+
module Observability
|
|
7
|
+
module Langfuse
|
|
8
|
+
module Traceable
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
# PerformWrapper is prepended into the including class so that its
|
|
12
|
+
# perform override runs before the class's own perform definition,
|
|
13
|
+
# regardless of the order in which include and def appear.
|
|
14
|
+
module PerformWrapper
|
|
15
|
+
REENTRY_KEY = :platform_sdk_langfuse_traceable_active
|
|
16
|
+
|
|
17
|
+
def perform(*args)
|
|
18
|
+
return super(*args) unless PlatformSdk::Observability::Langfuse.enabled?
|
|
19
|
+
|
|
20
|
+
# Re-entry guard: subclass `inherited` re-prepends PerformWrapper at
|
|
21
|
+
# every level of the ancestry, so a subclass `perform` that calls
|
|
22
|
+
# `super` would otherwise hit the wrapper at each intermediate class
|
|
23
|
+
# and open one span per level. Only the outermost wrapper opens a
|
|
24
|
+
# span; inner re-entries just delegate.
|
|
25
|
+
return super(*args) if Thread.current[REENTRY_KEY]
|
|
26
|
+
|
|
27
|
+
Thread.current[REENTRY_KEY] = true
|
|
28
|
+
begin
|
|
29
|
+
tracer = PlatformSdk::Observability::Langfuse.tracer
|
|
30
|
+
tracer.in_span(self.class.langfuse_span_name, attributes: safe_langfuse_span_attributes(args)) do
|
|
31
|
+
super(*args)
|
|
32
|
+
end
|
|
33
|
+
ensure
|
|
34
|
+
Thread.current[REENTRY_KEY] = false
|
|
35
|
+
PlatformSdk::Observability::Langfuse.clear_tracked_thread_locals!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Build attributes, but never raise out — a bad value should degrade tracing,
|
|
42
|
+
# not crash the wrapped job.
|
|
43
|
+
def safe_langfuse_span_attributes(args)
|
|
44
|
+
langfuse_span_attributes(args)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
OpenTelemetry.handle_error(message: "Langfuse attribute build failed: #{e.message}") if defined?(::OpenTelemetry)
|
|
47
|
+
{}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def langfuse_span_attributes(args)
|
|
51
|
+
attrs = {
|
|
52
|
+
'sidekiq.jid' => coerce_string(jid_if_available),
|
|
53
|
+
'langfuse.trace.input' => coerce_string(args.to_json)
|
|
54
|
+
}.compact
|
|
55
|
+
tags = combined_langfuse_tags
|
|
56
|
+
attrs['langfuse.trace.tags'] = tags if tags.any?
|
|
57
|
+
attrs
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def jid_if_available
|
|
61
|
+
respond_to?(:jid) ? jid : nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def combined_langfuse_tags
|
|
65
|
+
config = PlatformSdk::Observability::Langfuse.configuration
|
|
66
|
+
base_tags = config ? [config.environment, config.prompt_label].compact : []
|
|
67
|
+
(base_tags + Array(self.class.langfuse_extra_tags)).uniq.map { |t| coerce_string(t) }.compact
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def coerce_string(value)
|
|
71
|
+
Coercions.coerce_string(value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
included do
|
|
76
|
+
prepend PerformWrapper
|
|
77
|
+
|
|
78
|
+
# Subclasses that define their own perform would shadow PerformWrapper
|
|
79
|
+
# (which sits in the parent class's chain). Re-prepend on every subclass
|
|
80
|
+
# so the wrapper sits above each subclass's own perform definition.
|
|
81
|
+
def self.inherited(subclass)
|
|
82
|
+
super
|
|
83
|
+
subclass.prepend PerformWrapper
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class_methods do
|
|
88
|
+
# Optional override hook so consumers can change the span name or pre-set tags.
|
|
89
|
+
def traced_with_langfuse(name: nil, tags: [])
|
|
90
|
+
@langfuse_span_name = name
|
|
91
|
+
@langfuse_extra_tags = Array(tags)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def langfuse_span_name
|
|
95
|
+
@langfuse_span_name || name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def langfuse_extra_tags
|
|
99
|
+
@langfuse_extra_tags || []
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'platform_sdk/observability/langfuse/coercions'
|
|
4
|
+
require 'platform_sdk/observability/langfuse/configuration'
|
|
5
|
+
require 'platform_sdk/observability/langfuse/null_span_exporter'
|
|
6
|
+
require 'platform_sdk/observability/langfuse/recorder'
|
|
7
|
+
require 'platform_sdk/observability/langfuse/traceable'
|
|
8
|
+
require 'platform_sdk/observability/langfuse/sidekiq_lifecycle'
|
|
9
|
+
|
|
10
|
+
module PlatformSdk
|
|
11
|
+
module Observability
|
|
12
|
+
module Langfuse
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
class ConfigurationError < Error; end
|
|
15
|
+
|
|
16
|
+
OWNED_THREAD_LOCALS_KEY = :platform_sdk_langfuse_owned_thread_locals
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
attr_reader :configuration
|
|
20
|
+
|
|
21
|
+
def configure(app_name:, environment: nil, prompt_label: nil, exporter: nil)
|
|
22
|
+
@configuration&.force_flush_and_shutdown
|
|
23
|
+
@configuration = Configuration.new(
|
|
24
|
+
app_name: app_name,
|
|
25
|
+
environment: environment,
|
|
26
|
+
prompt_label: prompt_label,
|
|
27
|
+
exporter: exporter
|
|
28
|
+
)
|
|
29
|
+
SidekiqLifecycle.install! if @configuration.enabled?
|
|
30
|
+
@configuration
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def enabled?
|
|
34
|
+
!@configuration.nil? && @configuration.enabled?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def tracer
|
|
38
|
+
return nil unless enabled?
|
|
39
|
+
|
|
40
|
+
@configuration.tracer
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def record_generation(**kwargs)
|
|
44
|
+
Recorder.record_generation(**kwargs)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Set the trace-level output on the active parent span. Application
|
|
48
|
+
# code calls this from inside a Traceable job (or any block running
|
|
49
|
+
# under an active span) to record a meaningful summary of the job's
|
|
50
|
+
# result.
|
|
51
|
+
#
|
|
52
|
+
# Why this is opt-in rather than auto-captured from `perform`'s return
|
|
53
|
+
# value: Sidekiq's `perform` return is incidental — it's often nil, an
|
|
54
|
+
# AR transaction callback array, or otherwise unrelated to anything a
|
|
55
|
+
# human reading a trace would find useful. Only the application knows
|
|
56
|
+
# what's meaningful (e.g. a generated record's ID).
|
|
57
|
+
#
|
|
58
|
+
# Example:
|
|
59
|
+
# class MyJob
|
|
60
|
+
# include Sidekiq::Job
|
|
61
|
+
# include PlatformSdk::Observability::Langfuse::Traceable
|
|
62
|
+
#
|
|
63
|
+
# def perform(args)
|
|
64
|
+
# component = generate_component(args)
|
|
65
|
+
# PlatformSdk::Observability::Langfuse.set_trace_output(
|
|
66
|
+
# { component_id: component.id, status: component.status }
|
|
67
|
+
# )
|
|
68
|
+
# end
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# No-ops when the SDK is disabled or no span is currently active.
|
|
72
|
+
# Never raises — observability failures must not break callers.
|
|
73
|
+
def set_trace_output(value)
|
|
74
|
+
return unless enabled?
|
|
75
|
+
|
|
76
|
+
span = OpenTelemetry::Trace.current_span
|
|
77
|
+
return unless span.context.valid?
|
|
78
|
+
|
|
79
|
+
span.set_attribute('langfuse.trace.output', Recorder.stringify(value))
|
|
80
|
+
rescue StandardError, SystemStackError => e
|
|
81
|
+
OpenTelemetry.handle_error(message: "Langfuse set_trace_output failed: #{e.class}: #{e.message[0, 200]}")
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def reset!
|
|
86
|
+
@configuration&.force_flush_and_shutdown
|
|
87
|
+
@configuration = nil
|
|
88
|
+
SidekiqLifecycle.reset!
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Register thread-local keys that should be cleared at the next
|
|
92
|
+
# Traceable job boundary. Consumers call this when they store
|
|
93
|
+
# per-trace state on `Thread.current` (e.g. the ID of the last
|
|
94
|
+
# recorded message) so the value can't leak into the next job
|
|
95
|
+
# that lands on this Sidekiq thread.
|
|
96
|
+
#
|
|
97
|
+
# Example:
|
|
98
|
+
# PlatformSdk::Observability::Langfuse.track_thread_local(:my_app_last_message_id)
|
|
99
|
+
# Thread.current[:my_app_last_message_id] = msg.id
|
|
100
|
+
#
|
|
101
|
+
# Compared to a name-prefix convention, this requires explicit
|
|
102
|
+
# opt-in (so consumers can't accidentally have unrelated state
|
|
103
|
+
# zeroed) and is O(owned keys) instead of O(all thread locals).
|
|
104
|
+
def track_thread_local(*keys)
|
|
105
|
+
list = (Thread.current[OWNED_THREAD_LOCALS_KEY] ||= [])
|
|
106
|
+
keys.each { |k| list << k unless list.include?(k) }
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Clear every thread-local registered via `track_thread_local` on
|
|
111
|
+
# this thread, then drop the registry itself. Called from the
|
|
112
|
+
# Traceable wrapper's `ensure` block.
|
|
113
|
+
def clear_tracked_thread_locals!
|
|
114
|
+
owned = Thread.current[OWNED_THREAD_LOCALS_KEY]
|
|
115
|
+
return unless owned
|
|
116
|
+
|
|
117
|
+
owned.each { |k| Thread.current[k] = nil }
|
|
118
|
+
Thread.current[OWNED_THREAD_LOCALS_KEY] = nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/platform_sdk/version.rb
CHANGED
data/lib/platform_sdk.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: strongmind-platform-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.28.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Platform Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -184,6 +184,34 @@ dependencies:
|
|
|
184
184
|
- - ">="
|
|
185
185
|
- !ruby/object:Gem::Version
|
|
186
186
|
version: '0'
|
|
187
|
+
- !ruby/object:Gem::Dependency
|
|
188
|
+
name: opentelemetry-sdk
|
|
189
|
+
requirement: !ruby/object:Gem::Requirement
|
|
190
|
+
requirements:
|
|
191
|
+
- - "~>"
|
|
192
|
+
- !ruby/object:Gem::Version
|
|
193
|
+
version: '1.10'
|
|
194
|
+
type: :runtime
|
|
195
|
+
prerelease: false
|
|
196
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
197
|
+
requirements:
|
|
198
|
+
- - "~>"
|
|
199
|
+
- !ruby/object:Gem::Version
|
|
200
|
+
version: '1.10'
|
|
201
|
+
- !ruby/object:Gem::Dependency
|
|
202
|
+
name: opentelemetry-exporter-otlp
|
|
203
|
+
requirement: !ruby/object:Gem::Requirement
|
|
204
|
+
requirements:
|
|
205
|
+
- - "~>"
|
|
206
|
+
- !ruby/object:Gem::Version
|
|
207
|
+
version: '0.27'
|
|
208
|
+
type: :runtime
|
|
209
|
+
prerelease: false
|
|
210
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
211
|
+
requirements:
|
|
212
|
+
- - "~>"
|
|
213
|
+
- !ruby/object:Gem::Version
|
|
214
|
+
version: '0.27'
|
|
187
215
|
- !ruby/object:Gem::Dependency
|
|
188
216
|
name: asset_sync
|
|
189
217
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -286,6 +314,15 @@ files:
|
|
|
286
314
|
- lib/platform_sdk/llm_gateway/client.rb
|
|
287
315
|
- lib/platform_sdk/logging.rb
|
|
288
316
|
- lib/platform_sdk/logging/pii_formatter.rb
|
|
317
|
+
- lib/platform_sdk/observability.rb
|
|
318
|
+
- lib/platform_sdk/observability/langfuse.rb
|
|
319
|
+
- lib/platform_sdk/observability/langfuse/coercions.rb
|
|
320
|
+
- lib/platform_sdk/observability/langfuse/configuration.rb
|
|
321
|
+
- lib/platform_sdk/observability/langfuse/null_span_exporter.rb
|
|
322
|
+
- lib/platform_sdk/observability/langfuse/recorder.rb
|
|
323
|
+
- lib/platform_sdk/observability/langfuse/sidekiq_lifecycle.rb
|
|
324
|
+
- lib/platform_sdk/observability/langfuse/spec_support.rb
|
|
325
|
+
- lib/platform_sdk/observability/langfuse/traceable.rb
|
|
289
326
|
- lib/platform_sdk/one_roster.rb
|
|
290
327
|
- lib/platform_sdk/one_roster/client.rb
|
|
291
328
|
- lib/platform_sdk/ops_genie.rb
|