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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba92036fbe7b63b8355a113e44ff3f62cfcb11817ae3b3c1c444756af55ebf84
|
|
4
|
+
data.tar.gz: 44097d4d441ad6d2d8780a95cb836d0368ed0b772f16036e301ece7e018df0a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6e8880c58683dc7ff719f0c966515841e9a3e3df024f88175297090949a6befaddf23528b24a65d3825fa40e33118348087c0dbe5312543c16bfef65856eaf12
|
|
7
|
+
data.tar.gz: a82f8be469125e99355c5b6a1bd747a1f2e395dd6e0eb098f8319a0976419dfd60087c6252e26c2570db15994ed084d3156069019541c3b32acc7f1827c92792
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-04-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Probabilistic trace sampling with score parity (#60)
|
|
14
|
+
- Dataset run lifecycle methods: `get_dataset_run`, `list_dataset_runs`, `delete_dataset_run` (#62)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Tracing is now isolated-by-default with lazy setup and smart export filtering (#77)
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
- Align docs with implementation (#78)
|
|
21
|
+
|
|
22
|
+
## [0.7.0] - 2026-04-14
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Custom/deterministic trace ID support (#74)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Bump faraday, json, and addressable to patch CVEs (#75)
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
- Align docs with implementation (#70, #76)
|
|
32
|
+
|
|
10
33
|
## [0.6.0] - 2026-03-06
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -69,7 +92,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
69
92
|
- OpenTelemetry-based tracing with OTLP export
|
|
70
93
|
- Distributed caching with Rails.cache backend and stampede protection
|
|
71
94
|
- Prompt management (text and chat) with Mustache templating
|
|
72
|
-
- In-memory caching with TTL and
|
|
95
|
+
- In-memory caching with TTL and bounded expiration-ordered eviction
|
|
73
96
|
- Fallback prompt support
|
|
74
97
|
- Global configuration pattern with `Langfuse.configure`
|
|
75
98
|
|
|
@@ -77,7 +100,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
77
100
|
- Migrated from legacy ingestion API to OTLP endpoint
|
|
78
101
|
- Removed `tracing_enabled` configuration flag (#2)
|
|
79
102
|
|
|
80
|
-
[Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.
|
|
103
|
+
[Unreleased]: https://github.com/simplepractice/langfuse-rb/compare/v0.8.0...HEAD
|
|
104
|
+
[0.8.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.7.0...v0.8.0
|
|
105
|
+
[0.7.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.6.0...v0.7.0
|
|
81
106
|
[0.6.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.5.0...v0.6.0
|
|
82
107
|
[0.5.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.4.0...v0.5.0
|
|
83
108
|
[0.4.0]: https://github.com/simplepractice/langfuse-rb/compare/v0.3.0...v0.4.0
|
data/README.md
CHANGED
|
@@ -8,69 +8,49 @@
|
|
|
8
8
|
|
|
9
9
|
> Ruby SDK for [Langfuse](https://langfuse.com) - Open-source LLM observability and prompt management.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
### Features
|
|
14
|
-
|
|
15
|
-
- 🎯 **Prompt Management** - Centralized prompt versioning with Mustache templating
|
|
16
|
-
- 📊 **LLM Tracing** - Zero-boilerplate observability built on OpenTelemetry
|
|
17
|
-
- ⚡ **Performance** - In-memory or Redis-backed caching with stampede protection, both supporting stale-while-revalidate cache strategy
|
|
18
|
-
- 💬 **Chat & Text Prompts** - First-class support for both formats
|
|
19
|
-
- 🔄 **Automatic Retries** - Built-in exponential backoff for resilient API calls
|
|
20
|
-
- 🛡️ **Fallback Support** - Graceful degradation when API unavailable
|
|
21
|
-
- 🚀 **Rails-Friendly** - Global configuration pattern, works with any Ruby project
|
|
22
|
-
|
|
23
|
-
<br>
|
|
24
|
-
|
|
25
|
-
### Installation
|
|
11
|
+
## Installation
|
|
26
12
|
|
|
27
13
|
```ruby
|
|
28
|
-
|
|
29
|
-
gem 'langfuse-rb'
|
|
14
|
+
gem "langfuse-rb"
|
|
30
15
|
```
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Quick Start
|
|
35
|
-
|
|
36
|
-
> Configure once at startup
|
|
17
|
+
## Quick Start
|
|
37
18
|
|
|
38
19
|
```ruby
|
|
39
|
-
# config/initializers/langfuse.rb (Rails)
|
|
40
|
-
# Or at the top of your script
|
|
41
20
|
Langfuse.configure do |config|
|
|
42
|
-
config.public_key = ENV[
|
|
43
|
-
config.secret_key = ENV[
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
config.cache_backend = :rails # or :memory
|
|
49
|
-
config.cache_stale_while_revalidate = true
|
|
21
|
+
config.public_key = ENV["LANGFUSE_PUBLIC_KEY"]
|
|
22
|
+
config.secret_key = ENV["LANGFUSE_SECRET_KEY"]
|
|
23
|
+
config.base_url = ENV.fetch("LANGFUSE_BASE_URL", "https://cloud.langfuse.com")
|
|
24
|
+
|
|
25
|
+
# Optional: sample traces and trace-linked scores deterministically
|
|
26
|
+
config.sample_rate = 1.0
|
|
50
27
|
end
|
|
28
|
+
|
|
29
|
+
message = Langfuse.client.compile_prompt(
|
|
30
|
+
"greeting",
|
|
31
|
+
variables: { name: "Alice" }
|
|
32
|
+
)
|
|
51
33
|
```
|
|
52
34
|
|
|
53
|
-
|
|
35
|
+
Langfuse tracing is isolated by default. `Langfuse.configure` stores configuration only; it does not replace `OpenTelemetry.tracer_provider`.
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
prompt = Langfuse.client.get_prompt("greeting")
|
|
57
|
-
message = prompt.compile(name: "Alice")
|
|
58
|
-
# => "Hello Alice!"
|
|
59
|
-
```
|
|
37
|
+
`sample_rate` is applied to traces and trace-linked scores. Rebuild the client with `Langfuse.reset!` before expecting runtime sampling changes to take effect.
|
|
60
38
|
|
|
61
|
-
|
|
39
|
+
## Trace an LLM Call
|
|
62
40
|
|
|
63
41
|
```ruby
|
|
64
42
|
Langfuse.observe("chat-completion", as_type: :generation) do |gen|
|
|
43
|
+
gen.model = "gpt-4.1-mini"
|
|
44
|
+
gen.input = [{ role: "user", content: "Hello!" }]
|
|
45
|
+
|
|
65
46
|
response = openai_client.chat(
|
|
66
47
|
parameters: {
|
|
67
|
-
model: "gpt-4",
|
|
48
|
+
model: "gpt-4.1-mini",
|
|
68
49
|
messages: [{ role: "user", content: "Hello!" }]
|
|
69
50
|
}
|
|
70
51
|
)
|
|
71
52
|
|
|
72
53
|
gen.update(
|
|
73
|
-
model: "gpt-4",
|
|
74
54
|
output: response.dig("choices", 0, "message", "content"),
|
|
75
55
|
usage_details: {
|
|
76
56
|
prompt_tokens: response.dig("usage", "prompt_tokens"),
|
|
@@ -80,39 +60,14 @@ Langfuse.observe("chat-completion", as_type: :generation) do |gen|
|
|
|
80
60
|
end
|
|
81
61
|
```
|
|
82
62
|
|
|
83
|
-
|
|
84
|
-
> For complete reference see [docs](./docs/) section.
|
|
85
|
-
|
|
86
|
-
<br>
|
|
87
|
-
|
|
88
|
-
### Requirements
|
|
89
|
-
|
|
90
|
-
- Ruby >= 3.2.0
|
|
91
|
-
- No Rails dependency (works with any Ruby project)
|
|
92
|
-
|
|
93
|
-
<br>
|
|
94
|
-
|
|
95
|
-
### Contributing
|
|
96
|
-
|
|
97
|
-
We welcome contributions! Please:
|
|
98
|
-
|
|
99
|
-
1. Check existing [issues](https://github.com/simplepractice/langfuse-rb/issues)
|
|
100
|
-
2. Open an issue to discuss your idea
|
|
101
|
-
3. Fork the repo and create a feature branch
|
|
102
|
-
4. Write tests (maintain >95% coverage)
|
|
103
|
-
5. Ensure `bundle exec rspec` and `bundle exec rubocop` pass
|
|
104
|
-
6. Submit a pull request
|
|
105
|
-
|
|
106
|
-
> [!TIP]
|
|
107
|
-
> See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.
|
|
108
|
-
|
|
109
|
-
<br>
|
|
110
|
-
|
|
111
|
-
### Support
|
|
63
|
+
## Start Here
|
|
112
64
|
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
65
|
+
- [Documentation Hub](docs/README.md)
|
|
66
|
+
- [Getting Started](docs/GETTING_STARTED.md)
|
|
67
|
+
- [Prompts](docs/PROMPTS.md)
|
|
68
|
+
- [Tracing](docs/TRACING.md)
|
|
69
|
+
- [Scoring](docs/SCORING.md)
|
|
70
|
+
- [Rails Patterns](docs/RAILS.md)
|
|
116
71
|
|
|
117
72
|
## License
|
|
118
73
|
|
data/lib/langfuse/api_client.rb
CHANGED
|
@@ -260,6 +260,61 @@ module Langfuse
|
|
|
260
260
|
end
|
|
261
261
|
end
|
|
262
262
|
|
|
263
|
+
# Fetch a dataset run by dataset and run name
|
|
264
|
+
#
|
|
265
|
+
# @param dataset_name [String] Dataset name (required)
|
|
266
|
+
# @param run_name [String] Run name (required)
|
|
267
|
+
# @return [Hash] The dataset run data
|
|
268
|
+
# @raise [NotFoundError] if the dataset run is not found
|
|
269
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
270
|
+
# @raise [ApiError] for other API errors
|
|
271
|
+
def get_dataset_run(dataset_name:, run_name:)
|
|
272
|
+
with_faraday_error_handling do
|
|
273
|
+
response = connection.get(dataset_run_path(dataset_name: dataset_name, run_name: run_name))
|
|
274
|
+
handle_response(response)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# List dataset runs in a dataset
|
|
279
|
+
#
|
|
280
|
+
# @param dataset_name [String] Dataset name (required)
|
|
281
|
+
# @param page [Integer, nil] Optional page number for pagination
|
|
282
|
+
# @param limit [Integer, nil] Optional limit per page
|
|
283
|
+
# @return [Array<Hash>] Array of dataset run hashes
|
|
284
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
285
|
+
# @raise [ApiError] for other API errors
|
|
286
|
+
def list_dataset_runs(dataset_name:, page: nil, limit: nil)
|
|
287
|
+
result = list_dataset_runs_paginated(dataset_name: dataset_name, page: page, limit: limit)
|
|
288
|
+
result["data"] || []
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Full paginated response including "meta" for internal pagination use
|
|
292
|
+
#
|
|
293
|
+
# @api private
|
|
294
|
+
# @return [Hash] Full response hash with "data" array and "meta" pagination info
|
|
295
|
+
def list_dataset_runs_paginated(dataset_name:, page: nil, limit: nil)
|
|
296
|
+
with_faraday_error_handling do
|
|
297
|
+
response = connection.get(dataset_runs_path(dataset_name), build_dataset_runs_params(page: page, limit: limit))
|
|
298
|
+
handle_response(response)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Delete a dataset run by name
|
|
303
|
+
#
|
|
304
|
+
# @param dataset_name [String] Dataset name (required)
|
|
305
|
+
# @param run_name [String] Run name (required)
|
|
306
|
+
# @return [Hash, nil] Response body, or nil for 204 responses
|
|
307
|
+
# @raise [NotFoundError] if the dataset run is not found
|
|
308
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
309
|
+
# @raise [ApiError] for other API errors
|
|
310
|
+
# @note 404 responses raise NotFoundError to preserve strict delete semantics
|
|
311
|
+
def delete_dataset_run(dataset_name:, run_name:)
|
|
312
|
+
with_faraday_error_handling do
|
|
313
|
+
response = connection.delete(dataset_run_path(dataset_name: dataset_name, run_name: run_name))
|
|
314
|
+
response.status == 204 ? nil : handle_response(response)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
263
318
|
# Fetch projects accessible with the current API keys
|
|
264
319
|
#
|
|
265
320
|
# @return [Hash] The parsed response body containing project data
|
|
@@ -604,6 +659,23 @@ module Langfuse
|
|
|
604
659
|
}.compact
|
|
605
660
|
end
|
|
606
661
|
|
|
662
|
+
# Build params for list_dataset_runs
|
|
663
|
+
def build_dataset_runs_params(page:, limit:)
|
|
664
|
+
{ page: page, limit: limit }.compact
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Build endpoint path for dataset runs
|
|
668
|
+
def dataset_runs_path(dataset_name)
|
|
669
|
+
encoded_name = URI.encode_uri_component(dataset_name)
|
|
670
|
+
"/api/public/datasets/#{encoded_name}/runs"
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Build endpoint path for a specific dataset run
|
|
674
|
+
def dataset_run_path(dataset_name:, run_name:)
|
|
675
|
+
encoded_run_name = URI.encode_uri_component(run_name)
|
|
676
|
+
"#{dataset_runs_path(dataset_name)}/#{encoded_run_name}"
|
|
677
|
+
end
|
|
678
|
+
|
|
607
679
|
# Build query params for list_traces, mapping snake_case to camelCase
|
|
608
680
|
# rubocop:disable Metrics/ParameterLists
|
|
609
681
|
def build_traces_params(page:, limit:, user_id:, name:, session_id:,
|
|
@@ -85,7 +85,7 @@ module Langfuse
|
|
|
85
85
|
# @example Warm with a different default label
|
|
86
86
|
# results = warmer.warm_all(default_label: "staging")
|
|
87
87
|
#
|
|
88
|
-
# @example Warm without any label (
|
|
88
|
+
# @example Warm without any label (API-determined selection)
|
|
89
89
|
# results = warmer.warm_all(default_label: nil)
|
|
90
90
|
#
|
|
91
91
|
# @example With specific versions for some prompts
|
data/lib/langfuse/client.rb
CHANGED
|
@@ -588,6 +588,51 @@ module Langfuse
|
|
|
588
588
|
)
|
|
589
589
|
end
|
|
590
590
|
|
|
591
|
+
# Fetch a dataset run by dataset and run name
|
|
592
|
+
#
|
|
593
|
+
# @param dataset_name [String] Dataset name (required)
|
|
594
|
+
# @param run_name [String] Run name (required)
|
|
595
|
+
# @return [Hash] The dataset run data, including linked run items
|
|
596
|
+
# @raise [NotFoundError] if the dataset run is not found
|
|
597
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
598
|
+
# @raise [ApiError] for other API errors
|
|
599
|
+
def get_dataset_run(dataset_name:, run_name:)
|
|
600
|
+
api_client.get_dataset_run(dataset_name: dataset_name, run_name: run_name)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# List dataset runs for a dataset
|
|
604
|
+
#
|
|
605
|
+
# When page is nil (default), auto-paginates to fetch all runs.
|
|
606
|
+
# When page is provided, returns only that single page.
|
|
607
|
+
#
|
|
608
|
+
# @param dataset_name [String] Dataset name (required)
|
|
609
|
+
# @param page [Integer, nil] Optional page number for pagination
|
|
610
|
+
# @param limit [Integer, nil] Optional limit per page
|
|
611
|
+
# @return [Array<Hash>] Array of dataset run hashes
|
|
612
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
613
|
+
# @raise [ApiError] for other API errors
|
|
614
|
+
def list_dataset_runs(dataset_name:, page: nil, limit: nil)
|
|
615
|
+
if page
|
|
616
|
+
fetch_dataset_runs_page(dataset_name: dataset_name, page: page, limit: limit)
|
|
617
|
+
else
|
|
618
|
+
fetch_all_dataset_runs(dataset_name: dataset_name, limit: limit)
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
# Delete a dataset run by name
|
|
623
|
+
#
|
|
624
|
+
# @param dataset_name [String] Dataset name (required)
|
|
625
|
+
# @param run_name [String] Run name (required)
|
|
626
|
+
# @return [nil]
|
|
627
|
+
# @raise [NotFoundError] if the dataset run is not found
|
|
628
|
+
# @raise [UnauthorizedError] if authentication fails
|
|
629
|
+
# @raise [ApiError] for other API errors
|
|
630
|
+
# @note 404 responses raise NotFoundError to preserve strict delete semantics
|
|
631
|
+
def delete_dataset_run(dataset_name:, run_name:)
|
|
632
|
+
api_client.delete_dataset_run(dataset_name: dataset_name, run_name: run_name)
|
|
633
|
+
nil
|
|
634
|
+
end
|
|
635
|
+
|
|
591
636
|
# Run an experiment against local data or a named dataset
|
|
592
637
|
#
|
|
593
638
|
# @param name [String] Experiment/run name (required)
|
|
@@ -656,17 +701,35 @@ module Langfuse
|
|
|
656
701
|
end
|
|
657
702
|
|
|
658
703
|
def fetch_all_dataset_items(limit:, **filters)
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
704
|
+
fetch_all_paginated_data(limit: limit) do |page:, per_page:|
|
|
705
|
+
api_client.list_dataset_items_paginated(page: page, limit: per_page, **filters)
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def fetch_dataset_runs_page(dataset_name:, page:, limit:)
|
|
710
|
+
api_client.list_dataset_runs(dataset_name: dataset_name, page: page, limit: limit)
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def fetch_all_dataset_runs(dataset_name:, limit:)
|
|
714
|
+
fetch_all_paginated_data(limit: limit) do |page:, per_page:|
|
|
715
|
+
api_client.list_dataset_runs_paginated(dataset_name: dataset_name, page: page, limit: per_page)
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
# Keep dataset collection pagination semantics in one place so edge cases
|
|
720
|
+
# like missing or zero totalPages stay consistent across APIs.
|
|
721
|
+
def fetch_all_paginated_data(limit:)
|
|
722
|
+
page_size = limit || DATASET_ITEMS_PAGE_SIZE
|
|
723
|
+
first_result = yield(page: 1, per_page: page_size)
|
|
724
|
+
records = first_result["data"] || []
|
|
662
725
|
total_pages = first_result.dig("meta", "totalPages") || 1
|
|
663
726
|
|
|
664
|
-
(2..total_pages).each do |
|
|
665
|
-
result =
|
|
666
|
-
|
|
727
|
+
(2..total_pages).each do |page|
|
|
728
|
+
result = yield(page: page, per_page: page_size)
|
|
729
|
+
records.concat(result["data"] || [])
|
|
667
730
|
end
|
|
668
731
|
|
|
669
|
-
|
|
732
|
+
records
|
|
670
733
|
end
|
|
671
734
|
|
|
672
735
|
def resolve_experiment_items(data, dataset_name)
|
data/lib/langfuse/config.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Langfuse
|
|
|
18
18
|
# c.secret_key = "sk_..."
|
|
19
19
|
# end
|
|
20
20
|
#
|
|
21
|
+
# rubocop:disable Metrics/ClassLength
|
|
21
22
|
class Config
|
|
22
23
|
# @return [String, nil] Langfuse public API key
|
|
23
24
|
attr_accessor :public_key
|
|
@@ -74,6 +75,12 @@ module Langfuse
|
|
|
74
75
|
# @return [String, nil] Default release identifier applied to new traces/observations
|
|
75
76
|
attr_accessor :release
|
|
76
77
|
|
|
78
|
+
# @return [Float] Trace sampling rate from 0.0 to 1.0
|
|
79
|
+
attr_reader :sample_rate
|
|
80
|
+
|
|
81
|
+
# @return [#call, nil] Callback that decides whether an ended span should export to Langfuse.
|
|
82
|
+
attr_accessor :should_export_span
|
|
83
|
+
|
|
77
84
|
# @return [#call, nil] Mask callable applied to input, output, and metadata before serialization.
|
|
78
85
|
# Receives `data:` keyword argument. nil disables masking.
|
|
79
86
|
attr_accessor :mask
|
|
@@ -114,6 +121,9 @@ module Langfuse
|
|
|
114
121
|
# @return [Symbol] Default ActiveJob queue name
|
|
115
122
|
DEFAULT_JOB_QUEUE = :default
|
|
116
123
|
|
|
124
|
+
# @return [Float] Default trace sampling rate (sample all traces)
|
|
125
|
+
DEFAULT_SAMPLE_RATE = 1.0
|
|
126
|
+
|
|
117
127
|
# @return [Integer] Number of seconds representing indefinite cache duration (~1000 years)
|
|
118
128
|
INDEFINITE_SECONDS = 1000 * 365 * 24 * 60 * 60
|
|
119
129
|
|
|
@@ -136,7 +146,6 @@ module Langfuse
|
|
|
136
146
|
# @yield [config] Optional block for configuration
|
|
137
147
|
# @yieldparam config [Config] The config instance
|
|
138
148
|
# @return [Config] a new Config instance
|
|
139
|
-
# rubocop:disable Metrics/AbcSize
|
|
140
149
|
def initialize
|
|
141
150
|
@public_key = ENV.fetch("LANGFUSE_PUBLIC_KEY", nil)
|
|
142
151
|
@secret_key = ENV.fetch("LANGFUSE_SECRET_KEY", nil)
|
|
@@ -153,14 +162,11 @@ module Langfuse
|
|
|
153
162
|
@batch_size = DEFAULT_BATCH_SIZE
|
|
154
163
|
@flush_interval = DEFAULT_FLUSH_INTERVAL
|
|
155
164
|
@job_queue = DEFAULT_JOB_QUEUE
|
|
156
|
-
|
|
157
|
-
@release = env_value("LANGFUSE_RELEASE") || detect_release_from_ci_env
|
|
158
|
-
@mask = nil
|
|
165
|
+
initialize_tracing_defaults
|
|
159
166
|
@logger = default_logger
|
|
160
167
|
|
|
161
168
|
yield(self) if block_given?
|
|
162
169
|
end
|
|
163
|
-
# rubocop:enable Metrics/AbcSize
|
|
164
170
|
|
|
165
171
|
# Validate the configuration
|
|
166
172
|
#
|
|
@@ -183,7 +189,8 @@ module Langfuse
|
|
|
183
189
|
validate_swr_config!
|
|
184
190
|
|
|
185
191
|
validate_cache_backend!
|
|
186
|
-
|
|
192
|
+
validate_sample_rate!
|
|
193
|
+
validate_should_export_span!
|
|
187
194
|
validate_mask!
|
|
188
195
|
end
|
|
189
196
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
@@ -205,6 +212,15 @@ module Langfuse
|
|
|
205
212
|
cache_stale_ttl == :indefinite ? INDEFINITE_SECONDS : cache_stale_ttl
|
|
206
213
|
end
|
|
207
214
|
|
|
215
|
+
# Set trace sampling rate.
|
|
216
|
+
#
|
|
217
|
+
# @param value [Numeric, String] Sampling rate from 0.0 to 1.0
|
|
218
|
+
# @raise [ConfigurationError] if value is non-numeric or outside 0.0..1.0
|
|
219
|
+
# @return [Float]
|
|
220
|
+
def sample_rate=(value)
|
|
221
|
+
@sample_rate = coerce_sample_rate(value)
|
|
222
|
+
end
|
|
223
|
+
|
|
208
224
|
private
|
|
209
225
|
|
|
210
226
|
def default_logger
|
|
@@ -215,6 +231,14 @@ module Langfuse
|
|
|
215
231
|
end
|
|
216
232
|
end
|
|
217
233
|
|
|
234
|
+
def initialize_tracing_defaults
|
|
235
|
+
@environment = env_value("LANGFUSE_TRACING_ENVIRONMENT")
|
|
236
|
+
@release = env_value("LANGFUSE_RELEASE") || detect_release_from_ci_env
|
|
237
|
+
self.sample_rate = env_value("LANGFUSE_SAMPLE_RATE") || DEFAULT_SAMPLE_RATE
|
|
238
|
+
@should_export_span = nil
|
|
239
|
+
@mask = nil
|
|
240
|
+
end
|
|
241
|
+
|
|
218
242
|
def validate_cache_backend!
|
|
219
243
|
valid_backends = %i[memory rails]
|
|
220
244
|
return if valid_backends.include?(cache_backend)
|
|
@@ -255,12 +279,24 @@ module Langfuse
|
|
|
255
279
|
raise ConfigurationError, "cache_refresh_threads must be positive"
|
|
256
280
|
end
|
|
257
281
|
|
|
282
|
+
def validate_sample_rate!
|
|
283
|
+
return if sample_rate.is_a?(Numeric) && sample_rate.between?(0.0, 1.0)
|
|
284
|
+
|
|
285
|
+
raise ConfigurationError, "sample_rate must be between 0.0 and 1.0"
|
|
286
|
+
end
|
|
287
|
+
|
|
258
288
|
def validate_mask!
|
|
259
289
|
return if mask.nil? || mask.respond_to?(:call)
|
|
260
290
|
|
|
261
291
|
raise ConfigurationError, "mask must respond to #call"
|
|
262
292
|
end
|
|
263
293
|
|
|
294
|
+
def validate_should_export_span!
|
|
295
|
+
return if should_export_span.nil? || should_export_span.respond_to?(:call)
|
|
296
|
+
|
|
297
|
+
raise ConfigurationError, "should_export_span must respond to #call"
|
|
298
|
+
end
|
|
299
|
+
|
|
264
300
|
def detect_release_from_ci_env
|
|
265
301
|
COMMON_RELEASE_ENV_KEYS.each do |key|
|
|
266
302
|
value = env_value(key)
|
|
@@ -276,5 +312,22 @@ module Langfuse
|
|
|
276
312
|
|
|
277
313
|
value
|
|
278
314
|
end
|
|
315
|
+
|
|
316
|
+
def coerce_sample_rate(value)
|
|
317
|
+
numeric_value = if value.is_a?(Numeric)
|
|
318
|
+
value.to_f
|
|
319
|
+
elsif value.is_a?(String)
|
|
320
|
+
Float(value)
|
|
321
|
+
else
|
|
322
|
+
raise ConfigurationError, "sample_rate must be numeric"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
return numeric_value if numeric_value.between?(0.0, 1.0)
|
|
326
|
+
|
|
327
|
+
raise ConfigurationError, "sample_rate must be between 0.0 and 1.0"
|
|
328
|
+
rescue ArgumentError, TypeError
|
|
329
|
+
raise ConfigurationError, "sample_rate must be numeric"
|
|
330
|
+
end
|
|
279
331
|
end
|
|
332
|
+
# rubocop:enable Metrics/ClassLength
|
|
280
333
|
end
|
|
@@ -133,8 +133,7 @@ module Langfuse
|
|
|
133
133
|
# @yield [observation] Optional block that receives the observation object
|
|
134
134
|
# @return [BaseObservation, Object] The child observation (or block return value if block given)
|
|
135
135
|
def start_observation(name, attrs = {}, as_type: :span, &block)
|
|
136
|
-
#
|
|
137
|
-
# Skip validation to allow unknown types to fall back to Span
|
|
136
|
+
# Skip validation so unknown types fall back to Span in the factory.
|
|
138
137
|
child = Langfuse.start_observation(
|
|
139
138
|
name,
|
|
140
139
|
attrs,
|
|
@@ -142,24 +141,9 @@ module Langfuse
|
|
|
142
141
|
parent_span_context: @otel_span.context,
|
|
143
142
|
skip_validation: true
|
|
144
143
|
)
|
|
144
|
+
return child unless block
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
# Block-based API: auto-ends when block completes
|
|
148
|
-
# Set context and execute block
|
|
149
|
-
current_context = OpenTelemetry::Context.current
|
|
150
|
-
result = OpenTelemetry::Context.with_current(
|
|
151
|
-
OpenTelemetry::Trace.context_with_span(child.otel_span, parent_context: current_context)
|
|
152
|
-
) do
|
|
153
|
-
block.call(child)
|
|
154
|
-
end
|
|
155
|
-
# Only end if not already ended (events auto-end in start_observation)
|
|
156
|
-
child.end unless as_type.to_s == OBSERVATION_TYPES[:event]
|
|
157
|
-
result
|
|
158
|
-
else
|
|
159
|
-
# Stateful API - return observation
|
|
160
|
-
# Events already auto-ended in start_observation
|
|
161
|
-
child
|
|
162
|
-
end
|
|
146
|
+
child.send(:run_in_context, &block)
|
|
163
147
|
end
|
|
164
148
|
|
|
165
149
|
# Sets observation-level input attributes.
|
|
@@ -261,6 +245,27 @@ module Langfuse
|
|
|
261
245
|
prompt
|
|
262
246
|
end
|
|
263
247
|
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
# Runs the block with this observation as the active OTel span,
|
|
252
|
+
# then ends the span in ensure (events excluded — they auto-end).
|
|
253
|
+
# @api private
|
|
254
|
+
def run_in_context
|
|
255
|
+
parent_ctx = OpenTelemetry::Context.current
|
|
256
|
+
span_ctx = OpenTelemetry::Trace.context_with_span(@otel_span, parent_context: parent_ctx)
|
|
257
|
+
OpenTelemetry::Context.with_current(span_ctx) { yield self }
|
|
258
|
+
ensure
|
|
259
|
+
safe_end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Ends the span, swallowing errors so ensure never masks a block exception.
|
|
263
|
+
# @api private
|
|
264
|
+
def safe_end
|
|
265
|
+
self.end unless @type == OBSERVATION_TYPES[:event]
|
|
266
|
+
rescue StandardError
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
264
269
|
end
|
|
265
270
|
|
|
266
271
|
# General-purpose observation for tracking operations, functions, or logical units of work.
|