langfuse-ruby 0.1.7 → 0.2.1
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/.github/workflows/ci.yml +21 -10
- data/.github/workflows/release.yml +2 -2
- data/.mise.toml +8 -0
- data/.rubocop.yml +2 -2
- data/CHANGELOG.md +78 -10
- data/CLAUDE.md +54 -16
- data/Gemfile +1 -0
- data/Gemfile.lock +15 -5
- data/Makefile +6 -5
- data/README.md +338 -89
- data/Rakefile +0 -6
- data/docs/FINAL_SUMMARY.md +12 -185
- data/docs/PUBLISH_GUIDE.md +36 -272
- data/docs/README.md +12 -22
- data/docs/RELEASE_CHECKLIST.md +20 -145
- data/docs/V4.md +159 -0
- data/examples/basic_tracing.rb +11 -10
- data/examples/simplified_usage.rb +7 -6
- data/examples/v4_otel_tracing.rb +70 -0
- data/langfuse-ruby.gemspec +2 -0
- data/lib/langfuse/client.rb +757 -276
- data/lib/langfuse/event.rb +1 -17
- data/lib/langfuse/generation.rb +71 -124
- data/lib/langfuse/null_objects.rb +4 -0
- data/lib/langfuse/otel_exporter.rb +145 -63
- data/lib/langfuse/partial_updates.rb +30 -0
- data/lib/langfuse/prompt.rb +9 -83
- data/lib/langfuse/prompt_cache.rb +65 -0
- data/lib/langfuse/span.rb +29 -153
- data/lib/langfuse/span_wrappers.rb +32 -0
- data/lib/langfuse/template_compiler.rb +56 -0
- data/lib/langfuse/trace.rb +38 -164
- data/lib/langfuse/utils.rb +54 -20
- data/lib/langfuse/version.rb +1 -1
- data/lib/langfuse.rb +61 -56
- data/scripts/release.sh +12 -12
- metadata +37 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45de0f0dc3915b41c725f9099843bd6fd1e29d9a532a11a13bb65ca744a753fa
|
|
4
|
+
data.tar.gz: d04b31ebc438237d2462fec8e3c5e407f27548eb40c25164a7dc939d39942183
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7677e27533af07f139e14dc6d189d16c3d00a75a625d5058e332221b89d107e76206fadc9d1fd4175346eaab43cd11322f724aca87d61acffc1f267ec651d301
|
|
7
|
+
data.tar.gz: 2bb6e47d16ff17e62200f2da93e539549edaf329419bb352f6081d7983e813c30d03cf61b775f98f643fc7b199921def0e0944b8aaf717daa80a18de8bb8067e
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -7,15 +7,30 @@ on:
|
|
|
7
7
|
branches: [ master ]
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: '4.0'
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Run RuboCop
|
|
23
|
+
run: bundle exec rubocop
|
|
24
|
+
|
|
10
25
|
test:
|
|
11
26
|
runs-on: ubuntu-latest
|
|
12
27
|
|
|
13
28
|
strategy:
|
|
14
29
|
matrix:
|
|
15
|
-
ruby-version: ['3.1', '3.2', '3.3', '3.4']
|
|
30
|
+
ruby-version: ['3.1', '3.2', '3.3', '3.4', '4.0']
|
|
16
31
|
|
|
17
32
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
33
|
+
- uses: actions/checkout@v6
|
|
19
34
|
|
|
20
35
|
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
21
36
|
uses: ruby/setup-ruby@v1
|
|
@@ -29,25 +44,21 @@ jobs:
|
|
|
29
44
|
- name: Run offline tests
|
|
30
45
|
run: bundle exec ruby scripts/test_offline.rb
|
|
31
46
|
|
|
32
|
-
- name: Run RuboCop
|
|
33
|
-
run: bundle exec rubocop
|
|
34
|
-
continue-on-error: true
|
|
35
|
-
|
|
36
47
|
build:
|
|
37
48
|
runs-on: ubuntu-latest
|
|
38
|
-
needs: test
|
|
49
|
+
needs: [lint, test]
|
|
39
50
|
|
|
40
51
|
steps:
|
|
41
|
-
- uses: actions/checkout@
|
|
52
|
+
- uses: actions/checkout@v6
|
|
42
53
|
|
|
43
54
|
- name: Set up Ruby
|
|
44
55
|
uses: ruby/setup-ruby@v1
|
|
45
56
|
with:
|
|
46
|
-
ruby-version: '
|
|
57
|
+
ruby-version: '4.0'
|
|
47
58
|
bundler-cache: true
|
|
48
59
|
|
|
49
60
|
- name: Build gem
|
|
50
61
|
run: gem build langfuse-ruby.gemspec
|
|
51
62
|
|
|
52
63
|
- name: Verify gem can be installed
|
|
53
|
-
run: gem install langfuse-ruby-*.gem
|
|
64
|
+
run: gem install langfuse-ruby-*.gem
|
|
@@ -13,12 +13,12 @@ jobs:
|
|
|
13
13
|
contents: write
|
|
14
14
|
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
17
|
|
|
18
18
|
- name: Set up Ruby
|
|
19
19
|
uses: ruby/setup-ruby@v1
|
|
20
20
|
with:
|
|
21
|
-
ruby-version: '
|
|
21
|
+
ruby-version: '4.0'
|
|
22
22
|
bundler-cache: true
|
|
23
23
|
|
|
24
24
|
- name: Run tests
|
data/.mise.toml
ADDED
data/.rubocop.yml
CHANGED
|
@@ -47,7 +47,7 @@ Metrics/CyclomaticComplexity:
|
|
|
47
47
|
Max: 20
|
|
48
48
|
|
|
49
49
|
Metrics/PerceivedComplexity:
|
|
50
|
-
Max:
|
|
50
|
+
Max: 25
|
|
51
51
|
|
|
52
52
|
Metrics/ParameterLists:
|
|
53
53
|
Enabled: false
|
|
@@ -63,4 +63,4 @@ Lint/UnusedMethodArgument:
|
|
|
63
63
|
- 'lib/langfuse/evaluation.rb'
|
|
64
64
|
|
|
65
65
|
Metrics/ClassLength:
|
|
66
|
-
Max:
|
|
66
|
+
Max: 850
|
data/CHANGELOG.md
CHANGED
|
@@ -7,17 +7,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.1] - 2026-09-17
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
- README and examples now lead with **Langfuse v4** (`ingestion_mode: :otel`): OTLP ingestion, observations-first tracing, `usage_details` / `cost_details`, typed observations, and the Cloud cutoff of non-score `/api/public/ingestion` traffic on 16 November 2026
|
|
14
|
+
- Added [docs/V4.md](docs/V4.md) (attribute mapping, scores, cutover checklist) and `examples/v4_otel_tracing.rb`
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **`Langfuse.trace` executed the block twice**: an exception raised inside the block was caught by the method-level `rescue` and the block was re-run with a `NullTrace` — duplicating LLM calls and their cost. Only trace creation degrades to `NullTrace` now; exceptions from the block propagate untouched
|
|
18
|
+
- **Typed API errors collapsed into `APIError`**: `AuthenticationError` / `RateLimitError` / `ValidationError` raised by `handle_response` were re-wrapped by the generic `rescue` in `#request`, so callers could not rescue them selectively
|
|
19
|
+
- **`Langfuse.configure` values for `timeout` / `retries` were ignored**: the `Client#initialize` defaults (30 / 3) shadowed the configured values
|
|
20
|
+
- **OTel mode dropped token usage**: only `promptTokens` / `completionTokens` were mapped. All legacy shapes (`promptTokens`, `inputTokens`, `input`) now map to `gen_ai.usage.*` and are normalized into `langfuse.observation.usage_details`, which is what Langfuse v4 uses for cost. Usage carrying a non-token `unit` is skipped instead of being reported as tokens
|
|
21
|
+
- **OTel mode produced duplicate observations**: the `*-create` and `*-update` events of one observation were exported as two spans sharing a span id, which the append-only v4 data model stores twice. They are collapsed into one span carrying the final state
|
|
22
|
+
- **OTel mode stringified structured attributes**: hashes and arrays (for example `model_parameters[:tools]`) were sent through Ruby's `inspect`; they are JSON-encoded now
|
|
23
|
+
- **OTel export leaked raw Faraday errors**: failures on the OTLP connection now surface as `TimeoutError` / `NetworkError` / `APIError`, matching the ingestion API path
|
|
24
|
+
- **`shutdown` killed the flush thread mid-send**: events already drained from the queue were lost. The thread is now signalled to stop and joined (5 s grace period, kill only as a fallback)
|
|
25
|
+
- **Flush thread was not fork-safe**: after `fork` (e.g. Puma workers) the child inherited a dead thread and the parent's queued events. Each process now recreates its own flush thread and drops inherited events instead of sending them twice
|
|
26
|
+
- **Permanently failing batches were re-queued forever**: a 4xx (validation/auth) failure re-queued the same events on every flush, blocking the queue. They are now dropped with a warning; transient failures (5xx, network) are still re-queued
|
|
27
|
+
- **Event queue races**: enqueue, the `trace-update` → `trace-create` merge, and the flush drain ran without a shared lock, so a concurrent flush could interleave with a merge. They are serialized by a queue mutex now
|
|
28
|
+
- **Prompt compilation re-expanded placeholders coming from variable values**: variables were substituted one after another, so a value containing `{{other_var}}` was expanded by a later round (user input could inject template syntax, and the result depended on hash order). All variables are now substituted in a single pass, and a value containing a placeholder stays literal
|
|
29
|
+
- **`Span#generation` and `Generation#generation` dropped `usage_details`, `cost_details` and `prompt`**: the parameters were missing from both signatures, so a generation created under a span or another generation silently lost its v4 cost data and prompt link (only `Trace#generation` forwarded them)
|
|
30
|
+
- **Retries hit non-retryable failures**: `#request` retried every error, so a 401 or a 422 was sent four times before failing. Only timeouts, network errors, 429 and 5xx are retried now
|
|
31
|
+
- **Retries ignored `Retry-After`**: a rate-limited request was retried on a fixed 1 s / 2 s / 3 s schedule regardless of the server's instruction, and all clients retried in lockstep because the delay carried no jitter
|
|
32
|
+
- **`Langfuse.get_prompt` retried twice over**: its own retry loop wrapped the HTTP layer's retries, so one call could issue up to nine requests (and a missing prompt was fetched three times before returning `nil`). Retries now happen in one place
|
|
33
|
+
- **An unknown `ingestion_mode` silently behaved like `:legacy`**: a typo such as `LANGFUSE_INGESTION_MODE=otlp` looked like a working v4 setup. The value is now normalized (`"OTEL"` → `:otel`) and validated, with a warning when it is not recognized
|
|
34
|
+
- **A flushed `trace-update` sent unprocessed `to_dict` output**: when the matching `trace-create` was already gone from the queue, the reconstructed body used symbol/snake_case keys and skipped environment injection and the `mask` callable, so the API could reject the event and PII could leave unredacted. The reconstruction now re-runs the same prepare/env/mask path as every other enqueue
|
|
35
|
+
|
|
10
36
|
### Added
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
37
|
+
- OTLP payloads are chunked to the 3.5 MB batch limit (previously only the ingestion API path was chunked)
|
|
38
|
+
- OTLP `partialSuccess` responses (HTTP 200 with rejected spans) are logged as warnings instead of silently losing data
|
|
39
|
+
- `max_queue_size` config (constructor / `Langfuse.configure` / `LANGFUSE_MAX_QUEUE_SIZE`, default 10,000): the event queue is bounded; when full, the oldest events are dropped with a rate-limited warning instead of growing memory without bound
|
|
40
|
+
- `get_prompt` resilience: when a refetch fails, the expired cache entry is served with a warning instead of raising (a Langfuse outage no longer breaks prompt resolution for previously fetched prompts). The prompt cache is now bounded (200 entries) and measures TTLs on the monotonic clock, so wall-clock jumps cannot extend or shorten entry lifetimes
|
|
41
|
+
- `Client#inspect` redacts the secret key, keeping it out of logs and console output
|
|
42
|
+
- `http_adapter` config (constructor / `Langfuse.configure`): selects the Faraday adapter, so a connection-pooling adapter such as `:net_http_persistent` can be used to keep the TLS connection alive between flushes. Defaults to Faraday's default adapter; an adapter whose gem is missing logs a warning and falls back instead of raising
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Retries are now applied only to transient failures (`TimeoutError`, `NetworkError`, `RateLimitError`, 5xx `APIError`), honor `Retry-After` (seconds or HTTP date, capped at 10 s), and otherwise back off exponentially from 0.5 s with ±50% jitter, capped at 10 s. `retries:` can be overridden per request
|
|
46
|
+
- Ingestion batches are serialized to JSON once and posted as a pre-encoded body; the per-event chunking path only runs when the batch exceeds the 3.5 MB limit (previously every batch was measured event by event and then re-encoded by Faraday)
|
|
47
|
+
- `*-update` events send only the fields that actually changed, plus the identifying ones, instead of the observation's full body. Ending a long generation no longer re-uploads its input and model parameters
|
|
48
|
+
- Enhanced observation wrappers (`agent`, `tool`, `chain`, `retriever`, `evaluator`, `guardrail`) are generated once in the new `Langfuse::SpanWrappers` module and shared by `Client`, `Trace`, `Span` and `Generation`, replacing 24 hand-written copies (~400 lines); an explicit `as_type:` passed by the caller no longer overrides the wrapper's type
|
|
49
|
+
- `Client#evaluator` is available as an alias of `Client#evaluator_obs`, so the helper has the same name as `Trace#evaluator` / `Span#evaluator` / `Generation#evaluator` (both names keep working)
|
|
50
|
+
- Placeholder compilation and variable extraction moved into the new `Langfuse::TemplateCompiler`, shared by `Prompt`, `PromptTemplate` and `ChatPromptTemplate` (previously four copies of the substitution loop and three of the variable scanner)
|
|
51
|
+
- `Trace`, `Span` and `Event` now serialize through `to_dict` when enqueuing `*-create` / `*-update` events instead of rebuilding the same hash in each method, so a field can no longer be added to one path and forgotten in the others
|
|
52
|
+
- `Utils.deep_stringify_keys` is now an alias of `Utils.deep_camelize_keys`: the two implementations were identical, and both camelize keys rather than only stringifying them
|
|
53
|
+
|
|
54
|
+
## [0.2.0] - 2026-07-18
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
- **Environment support**: `environment` config / `LANGFUSE_TRACING_ENVIRONMENT` env var, injected into trace, observation and score bodies
|
|
58
|
+
- **Sampling**: `sample_rate` config / `LANGFUSE_SAMPLE_RATE` env var, deterministic trace-based sampling (all events of a trace share the same decision)
|
|
59
|
+
- **Masking**: `mask` callable applied to `input`/`output`/`metadata` before sending, for PII redaction
|
|
60
|
+
- **flush_at threshold**: flush as soon as the queue reaches `flush_at` events (default 15, env `LANGFUSE_FLUSH_AT`), via a condition-variable wake-up on the flush thread
|
|
61
|
+
- **Batch chunking**: ingestion batches are split to respect the 3.5 MB API limit; oversized single events are dropped with a warning
|
|
62
|
+
- **207 partial-success handling**: per-event errors from the ingestion API are logged via the structured logger
|
|
63
|
+
- **Score full fields**: `session_id`, `dataset_run_id`, `metadata`, `config_id`, `queue_id`, `id`, `environment`, and string values for CATEGORICAL/CORRECTION scores; `create_score` alias
|
|
64
|
+
- **Generation usage_details / cost_details**: new v4 usage model (arbitrary keys such as cache tokens) alongside legacy `usage`
|
|
65
|
+
- **Generation prompt linking**: `prompt:` accepts a `Langfuse::Prompt` or `{ name:, version: }` hash, emitted as `promptName`/`promptVersion`
|
|
66
|
+
- **Trace public field**: `public:` flag for shareable traces
|
|
67
|
+
- **LANGFUSE_BASE_URL** env var alias (new SDK standard) alongside `LANGFUSE_HOST`
|
|
68
|
+
- **Structured Logger**: replaces `puts` with `Logger`; level controlled by `debug` / `LANGFUSE_DEBUG`
|
|
69
|
+
- **at_exit shutdown hook**: pending events are flushed on process exit (configurable via `shutdown_on_exit`)
|
|
70
|
+
- **W3C hex IDs in OTel mode**: native 32-char trace IDs and 16-char span IDs for OTel ingestion
|
|
71
|
+
- **OTel exporter attributes**: `langfuse.environment`, `langfuse.trace.public`, `langfuse.internal.as_root`, `langfuse.observation.usage_details`, `langfuse.observation.cost_details`, `langfuse.observation.prompt.name`, `langfuse.observation.prompt.version`
|
|
72
|
+
- **Simplified API**: Class-level convenience methods (`Langfuse.trace`, `get_prompt`, `client`, `flush`, `shutdown`, `reset!`) with graceful degradation via null objects
|
|
73
|
+
- **Retry Support**: `get_prompt` supports configurable retries with exponential backoff (default: 2 retries)
|
|
74
|
+
- **Ruby 4.0 support**: CI matrix covers Ruby 3.1–4.0; explicit `base64` / `tsort` dependencies for Ruby 4.0 gem packaging
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
- **OTel mode scores lost**: scores were exported as OTLP spans with `langfuse.score.*` attributes, which the server does not map to Langfuse scores. Scores now always route through the ingestion API (`score-create` batch), with trace/observation IDs normalized to OTel hex IDs so they attach to the correct entities
|
|
78
|
+
- **OTel flush failure drops scores**: when OTEL export of non-score events fails, score events in the same batch are now re-queued together with OTEL events (they were previously drained by `flush` and permanently lost on `raise`)
|
|
79
|
+
- **OTel ID mismatch**: observation-level scores referenced full UUIDs while spans used truncated hex, breaking attachment. IDs are now normalized on both sides
|
|
80
|
+
- **Span/Generation score missing trace_id**: `Span#score` and `Generation#score` now pass `trace_id` so the server can attach observation-level scores correctly
|
|
81
|
+
- **Process-level singleton**: `Langfuse.client` was thread-local (`Thread.current`), creating one client + flush thread per thread under Puma/Sidekiq. Now a single process-wide client guarded by a `Mutex`
|
|
82
|
+
- **Idempotent shutdown**: `shutdown` can be called multiple times safely
|
|
83
|
+
- **Body serialization**: event bodies now only camelCase top-level keys; user data under `input`/`output`/`metadata`/`usageDetails`/`costDetails`/`modelParameters` is passed through verbatim so user-provided keys are not mangled
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
- `Langfuse.client` is now process-wide instead of thread-local. Use `Langfuse.new` for isolated clients in tests
|
|
87
|
+
- Default `flush_interval` behavior unchanged, but the flush thread now also wakes on the `flush_at` threshold
|
|
88
|
+
- `Configuration` gains `environment`, `sample_rate`, `mask`, `flush_at`, `logger`, `shutdown_on_exit` attributes
|
|
21
89
|
|
|
22
90
|
## [0.1.5] - 2025-12-26
|
|
23
91
|
|
data/CLAUDE.md
CHANGED
|
@@ -8,8 +8,14 @@ This is the official Ruby SDK for [Langfuse](https://langfuse.com) - an open-sou
|
|
|
8
8
|
|
|
9
9
|
## Common Commands
|
|
10
10
|
|
|
11
|
+
Ruby version is managed with [mise](https://mise.jdx.dev) via `.mise.toml`
|
|
12
|
+
(defaults to the latest stable Ruby). Run `mise install` after cloning, then
|
|
13
|
+
`bundle install`. With mise's shell integration active, the commands below work
|
|
14
|
+
directly; in a non-mise shell prefix them with `mise exec --`.
|
|
15
|
+
|
|
11
16
|
```bash
|
|
12
17
|
# Install dependencies
|
|
18
|
+
mise install # install the pinned Ruby version
|
|
13
19
|
bundle install
|
|
14
20
|
|
|
15
21
|
# Run all RSpec tests
|
|
@@ -30,8 +36,8 @@ bundle exec rubocop
|
|
|
30
36
|
# Build the gem
|
|
31
37
|
bundle exec rake build
|
|
32
38
|
|
|
33
|
-
# Release
|
|
34
|
-
|
|
39
|
+
# Release: bump version + CHANGELOG, then `make tag VERSION=x.y.z`
|
|
40
|
+
# (pushes v* tag; GitHub Actions publishes to RubyGems)
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
## Architecture
|
|
@@ -39,22 +45,33 @@ bundle exec rake release_gem
|
|
|
39
45
|
### Core Classes
|
|
40
46
|
|
|
41
47
|
- **`Langfuse`** ([lib/langfuse.rb](lib/langfuse.rb)) - Module with class-level convenience methods (`trace`, `get_prompt`, `client`, `flush`, `shutdown`, `reset!`)
|
|
42
|
-
- **`Langfuse::Client`** ([lib/langfuse/client.rb](lib/langfuse/client.rb)) - Main entry point. Handles API authentication, HTTP connections (via Faraday), event queuing, and background flush thread for auto-batching events.
|
|
48
|
+
- **`Langfuse::Client`** ([lib/langfuse/client.rb](lib/langfuse/client.rb)) - Main entry point. Handles API authentication, HTTP connections (via Faraday), event queuing, sampling/masking, and background flush thread for auto-batching events.
|
|
43
49
|
- **`Langfuse::Trace`** ([lib/langfuse/trace.rb](lib/langfuse/trace.rb)) - Top-level container for a request/session.
|
|
44
50
|
- **`Langfuse::Span`** ([lib/langfuse/span.rb](lib/langfuse/span.rb)) - Timed operation with enhanced type support.
|
|
45
|
-
- **`Langfuse::Generation`** ([lib/langfuse/generation.rb](lib/langfuse/generation.rb)) - LLM call tracking.
|
|
51
|
+
- **`Langfuse::Generation`** ([lib/langfuse/generation.rb](lib/langfuse/generation.rb)) - LLM call tracking (supports `usage_details`/`cost_details` and prompt linking).
|
|
46
52
|
- **`Langfuse::Event`** ([lib/langfuse/event.rb](lib/langfuse/event.rb)) - Point-in-time events.
|
|
47
|
-
- **`Langfuse::
|
|
53
|
+
- **`Langfuse::SpanWrappers`** ([lib/langfuse/span_wrappers.rb](lib/langfuse/span_wrappers.rb)) - Generates the enhanced observation helpers (`agent`, `tool`, `chain`, `retriever`, `evaluator`, `guardrail`) for Client/Trace/Span/Generation as `#span` calls with a fixed `as_type`.
|
|
54
|
+
- **`Langfuse::Prompt`** ([lib/langfuse/prompt.rb](lib/langfuse/prompt.rb)) - Prompt templates.
|
|
55
|
+
- **`Langfuse::TemplateCompiler`** ([lib/langfuse/template_compiler.rb](lib/langfuse/template_compiler.rb)) - `{{var}}` / `{var}` substitution and variable extraction shared by `Prompt`, `PromptTemplate` and `ChatPromptTemplate`.
|
|
56
|
+
- **`Langfuse::PromptCache`** ([lib/langfuse/prompt_cache.rb](lib/langfuse/prompt_cache.rb)) - Bounded, thread-safe prompt cache used by `Client#get_prompt`: monotonic-clock TTLs, oldest-first eviction, stale reads for outage fallback.
|
|
57
|
+
- **`Langfuse::PartialUpdates`** ([lib/langfuse/partial_updates.rb](lib/langfuse/partial_updates.rb)) - Dirty tracking for `Trace`/`Span`/`Generation`: `update`/`end` record which fields the caller passed, and `update_body` slices `to_dict` down to those plus the identifying fields (`id`, `trace_id`, `type`).
|
|
58
|
+
- **`Langfuse::OtelExporter`** ([lib/langfuse/otel_exporter.rb](lib/langfuse/otel_exporter.rb)) - Maps Langfuse events to OTLP/HTTP JSON when `ingestion_mode: :otel`.
|
|
48
59
|
- **`Langfuse::NullTrace/NullGeneration/NullSpan`** ([lib/langfuse/null_objects.rb](lib/langfuse/null_objects.rb)) - Null objects for graceful degradation.
|
|
49
60
|
|
|
61
|
+
New projects should set `ingestion_mode: :otel` (Langfuse v4 / OTLP). The
|
|
62
|
+
default remains `:legacy` for compatibility; Cloud stops accepting non-score
|
|
63
|
+
`/api/public/ingestion` traffic on 16 November 2026. See [docs/V4.md](docs/V4.md).
|
|
64
|
+
|
|
50
65
|
### Simplified API (Recommended)
|
|
51
66
|
|
|
52
67
|
```ruby
|
|
68
|
+
Langfuse.configure { |c| c.ingestion_mode = :otel } # Langfuse v4
|
|
69
|
+
|
|
53
70
|
# Block-based tracing with automatic flush
|
|
54
71
|
Langfuse.trace("my-trace", user_id: "user-1") do |trace|
|
|
55
72
|
gen = trace.generation(name: "openai", model: "gpt-4", input: messages)
|
|
56
73
|
response = call_llm(...)
|
|
57
|
-
gen.end(output: response,
|
|
74
|
+
gen.end(output: response, usage_details: { input: 10, output: 20, total: 30 })
|
|
58
75
|
end # Auto flush!
|
|
59
76
|
|
|
60
77
|
# Get prompt with variables and retry
|
|
@@ -63,10 +80,17 @@ Langfuse.get_prompt("my-prompt", variables: { name: "Alice" }, retries: 3)
|
|
|
63
80
|
|
|
64
81
|
### Event Flow
|
|
65
82
|
|
|
66
|
-
1. Observations (traces, spans, generations, events) are created via Client methods
|
|
67
|
-
2.
|
|
68
|
-
3.
|
|
69
|
-
4.
|
|
83
|
+
1. Observations (traces, spans, generations, events, scores) are created via Client methods
|
|
84
|
+
2. Bodies are prepared via `Utils.prepare_event_body` (top-level camelCase; user data under `input`/`output`/`metadata`/etc. is left verbatim), then environment injection, masking, and sampling are applied
|
|
85
|
+
3. Events are queued in `@event_queue` (thread-safe `Concurrent::Array`, guarded by `@queue_mutex`). The queue is bounded by `max_queue_size` (default 10,000); when full, the oldest events are dropped with a rate-limited warning. `trace-update` events merge into the queued `trace-create` under the same lock; if the create has already flushed, the update is converted to a create from `trace_ref.to_dict` and re-run through the same camelCase / environment / mask path
|
|
86
|
+
4. Background flush thread wakes on `flush_interval` **or** when the queue reaches `flush_at`. It is recreated after `fork` (events inherited from the parent are dropped; the parent still flushes them) and stopped gracefully on `shutdown` (signalled, joined with a 5 s grace period, killed only as a fallback)
|
|
87
|
+
5. `*-update` events carry only the changed fields (see `PartialUpdates`), so a long generation's input is not re-sent when it ends
|
|
88
|
+
6. Flush path depends on `ingestion_mode` (both paths chunk to 3.5 MB):
|
|
89
|
+
- `:legacy` → batched POST to `/api/public/ingestion`
|
|
90
|
+
- `:otel` → non-score events via OTLP `/api/public/otel/v1/traces`, after collapsing each observation's `*-create`/`*-update` events into one span (v4 is append-only, so exporting both would duplicate the observation); **scores always go through the ingestion API**, with IDs normalized to OTel hex so they attach correctly. On OTEL transport failure, the not-yet-sent chunks and the batch's score events are re-queued; `partialSuccess` responses are logged as warnings
|
|
91
|
+
- On failure both paths re-queue only for transient errors; permanent failures (4xx validation/auth) drop the batch with a warning so it cannot block the queue forever
|
|
92
|
+
- The legacy batch is serialized to JSON once and posted as a String; the per-event chunking path only runs when that payload exceeds 3.5 MB
|
|
93
|
+
7. Manual flush via `client.flush`; idempotent shutdown via `client.shutdown` (plus optional `at_exit` hook)
|
|
70
94
|
|
|
71
95
|
### Observation Types
|
|
72
96
|
|
|
@@ -81,20 +105,34 @@ Enhanced types are implemented as spans with `as_type` metadata sent to the API.
|
|
|
81
105
|
Client accepts config via:
|
|
82
106
|
1. Constructor parameters
|
|
83
107
|
2. `Langfuse.configure` block
|
|
84
|
-
3. Environment variables: `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST`, `LANGFUSE_FLUSH_INTERVAL`, `LANGFUSE_AUTO_FLUSH`
|
|
108
|
+
3. Environment variables: `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST` / `LANGFUSE_BASE_URL`, `LANGFUSE_FLUSH_INTERVAL`, `LANGFUSE_FLUSH_AT`, `LANGFUSE_MAX_QUEUE_SIZE`, `LANGFUSE_AUTO_FLUSH`, `LANGFUSE_TRACING_ENVIRONMENT`, `LANGFUSE_SAMPLE_RATE`, `LANGFUSE_DEBUG`, `LANGFUSE_INGESTION_MODE` (`otel` for v4, `legacy` for pre-v4)
|
|
109
|
+
|
|
110
|
+
Default host is `https://us.cloud.langfuse.com`. Default `ingestion_mode` is `:legacy`; use `:otel` for Langfuse v4.
|
|
111
|
+
|
|
112
|
+
`http_adapter` is constructor/`configure`-only (no env var), since the adapter has to be in the app's `Gemfile` anyway.
|
|
85
113
|
|
|
86
114
|
### Error Handling
|
|
87
115
|
|
|
88
116
|
Custom exceptions in [lib/langfuse/errors.rb](lib/langfuse/errors.rb):
|
|
89
117
|
- `AuthenticationError`, `APIError`, `NetworkError`, `ValidationError`, `RateLimitError`, `TimeoutError`
|
|
90
118
|
|
|
91
|
-
Graceful degradation: When
|
|
119
|
+
Graceful degradation: When trace creation fails, `Langfuse.trace` yields a `NullTrace` that silently no-ops all operations. Only creation is rescued — an exception raised by the block propagates and the block is never re-run (re-running it would duplicate LLM calls).
|
|
120
|
+
|
|
121
|
+
Retries live in `Client#request` only: `TimeoutError`, `NetworkError`, `RateLimitError` and 5xx `APIError` are retried up to `retries` times (per-call override via `retries:`), honoring `Retry-After` (seconds or HTTP date, capped at `MAX_RETRY_DELAY_SECONDS`) and otherwise backing off exponentially from `RETRY_BASE_DELAY_SECONDS` with ±50% jitter. `Langfuse.get_prompt` passes `retries:` down instead of running its own loop (the two used to multiply).
|
|
92
122
|
|
|
93
123
|
## Key Implementation Details
|
|
94
124
|
|
|
95
|
-
- Uses Faraday for HTTP with Basic Auth (public_key:secret_key)
|
|
125
|
+
- Uses Faraday for HTTP with Basic Auth (public_key:secret_key); `http_adapter` selects the adapter (e.g. `:net_http_persistent` for keep-alive) and falls back to `Faraday.default_adapter` with a warning when the adapter's gem is missing
|
|
96
126
|
- Prompt names with special characters are auto-URL-encoded via `Utils.url_encode`
|
|
97
|
-
- `trace-update` events merge into
|
|
98
|
-
-
|
|
99
|
-
-
|
|
127
|
+
- `trace-update` events merge into the queued `trace-create` under `@queue_mutex` (deduplication without flush races). A reconstruction from `trace_ref` (create already flushed) goes through `prepare_queued_body` so keys stay camelCase, the default environment is injected, and the mask still applies
|
|
128
|
+
- `get_prompt` caches via `PromptCache`: on refetch failure the stale entry is served with a warning rather than raising
|
|
129
|
+
- `Client#inspect` redacts the secret key
|
|
130
|
+
- The enhanced observation wrappers are generated by `SpanWrappers#define_span_wrappers` (Client names the evaluator helper `evaluator_obs` and aliases `evaluator` to it); `embedding` stays hand-written in each class because it folds model/usage into metadata
|
|
131
|
+
- `Trace`/`Span`/`Event` enqueue `to_dict`, so `to_dict` is the single place a body field is defined (`Generation` already worked this way)
|
|
132
|
+
- `TemplateCompiler.compile` substitutes all variables in one pass over the template, so a variable value containing `{{...}}` is never expanded again; unknown placeholders are left untouched
|
|
133
|
+
- Event bodies use `Utils.prepare_event_body` (top-level camelCase only; nested user data is not mangled)
|
|
134
|
+
- Process-wide singleton client via `Langfuse::CLIENT_MUTEX` (not thread-local); use `Langfuse.new` for isolated clients in tests
|
|
135
|
+
- In OTel mode, IDs are W3C hex (`generate_trace_id` / `generate_observation_id`); scores normalize refs via `OtelExporter.to_otel_trace_id` / `to_otel_span_id`
|
|
136
|
+
- In OTel mode, a legacy `usage` object is normalized into `langfuse.observation.usage_details` (v4's cost model) unless `usage_details` was set explicitly; non-token `unit`s are skipped
|
|
137
|
+
- Typed errors from `handle_response` keep their class through `#request`; only unexpected errors are wrapped in `APIError`
|
|
100
138
|
- `get_prompt` supports configurable retries with exponential backoff
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
langfuse-ruby (0.1
|
|
4
|
+
langfuse-ruby (0.2.1)
|
|
5
|
+
base64 (>= 0.1.0)
|
|
5
6
|
concurrent-ruby (~> 1.0)
|
|
6
7
|
faraday (>= 1.8, < 3.0)
|
|
7
8
|
faraday-multipart (~> 1.0)
|
|
@@ -21,11 +22,12 @@ GEM
|
|
|
21
22
|
bigdecimal
|
|
22
23
|
rexml
|
|
23
24
|
diff-lcs (1.6.2)
|
|
25
|
+
docile (1.4.1)
|
|
24
26
|
faraday (2.13.2)
|
|
25
27
|
faraday-net_http (>= 2.0, < 3.5)
|
|
26
28
|
json
|
|
27
29
|
logger
|
|
28
|
-
faraday-multipart (1.
|
|
30
|
+
faraday-multipart (1.2.0)
|
|
29
31
|
multipart-post (~> 2.0)
|
|
30
32
|
faraday-net_http (3.4.1)
|
|
31
33
|
net-http (>= 0.5.0)
|
|
@@ -76,12 +78,18 @@ GEM
|
|
|
76
78
|
parser (>= 3.3.7.2)
|
|
77
79
|
prism (~> 1.4)
|
|
78
80
|
ruby-progressbar (1.13.0)
|
|
81
|
+
simplecov (0.22.0)
|
|
82
|
+
docile (~> 1.1)
|
|
83
|
+
simplecov-html (~> 0.11)
|
|
84
|
+
simplecov_json_formatter (~> 0.1)
|
|
85
|
+
simplecov-html (0.13.2)
|
|
86
|
+
simplecov_json_formatter (0.1.4)
|
|
87
|
+
tsort (0.2.0)
|
|
79
88
|
unicode-display_width (3.1.4)
|
|
80
89
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
|
81
|
-
unicode-emoji (4.0
|
|
90
|
+
unicode-emoji (4.2.0)
|
|
82
91
|
uri (1.0.3)
|
|
83
|
-
vcr (6.
|
|
84
|
-
base64
|
|
92
|
+
vcr (6.4.0)
|
|
85
93
|
webmock (3.25.1)
|
|
86
94
|
addressable (>= 2.8.0)
|
|
87
95
|
crack (>= 0.3.2)
|
|
@@ -98,6 +106,8 @@ DEPENDENCIES
|
|
|
98
106
|
rake (~> 13.0)
|
|
99
107
|
rspec (~> 3.0)
|
|
100
108
|
rubocop (~> 1.0)
|
|
109
|
+
simplecov (~> 0.22)
|
|
110
|
+
tsort (>= 0.1.0)
|
|
101
111
|
vcr (~> 6.0)
|
|
102
112
|
webmock (~> 3.0)
|
|
103
113
|
yard (~> 0.9)
|
data/Makefile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.PHONY: install test spec test-offline test-all lint lint-fix build release clean console help tag
|
|
1
|
+
.PHONY: install test spec test-offline test-all lint lint-fix build release clean console help tag coverage
|
|
2
2
|
|
|
3
3
|
help: ## Show this help
|
|
4
4
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
|
@@ -17,6 +17,10 @@ test-all: ## Run all tests (spec + offline)
|
|
|
17
17
|
|
|
18
18
|
test: spec ## Alias for spec
|
|
19
19
|
|
|
20
|
+
coverage: ## Run tests and open coverage report
|
|
21
|
+
bundle exec rake spec
|
|
22
|
+
open coverage/index.html
|
|
23
|
+
|
|
20
24
|
lint: ## Run RuboCop linter
|
|
21
25
|
bundle exec rubocop
|
|
22
26
|
|
|
@@ -26,9 +30,6 @@ lint-fix: ## Run RuboCop with auto-correct
|
|
|
26
30
|
build: ## Build the gem
|
|
27
31
|
bundle exec rake build
|
|
28
32
|
|
|
29
|
-
release: tag ## Release: tag + build + push to RubyGems
|
|
30
|
-
bundle exec rake release_gem
|
|
31
|
-
|
|
32
33
|
clean: ## Remove built gem files
|
|
33
34
|
rm -f langfuse-ruby-*.gem
|
|
34
35
|
rm -rf pkg/
|
|
@@ -36,7 +37,7 @@ clean: ## Remove built gem files
|
|
|
36
37
|
console: ## Start an IRB console with the gem loaded
|
|
37
38
|
bundle exec irb -r langfuse
|
|
38
39
|
|
|
39
|
-
tag: ## Create and push a version tag. Usage: make tag
|
|
40
|
+
tag: ## Create and push a version tag. Usage: make tag VERSION=x.y.z
|
|
40
41
|
@git fetch --tags; \
|
|
41
42
|
if [ -z "$(VERSION)" ]; then \
|
|
42
43
|
LATEST=$$(git tag -l 'v[0-9]*' --sort=-v:refname | head -n1); \
|