Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `Config#valid?` and `Langfuse.configured?` — non-raising client-config validity predicates. Note: these reflect client configuration validity, not tracing readiness.

### Changed
- Tracing readiness now also validates `batch_size` (positive Integer) and `flush_interval` (positive number); invalid values disable tracing with a one-time specific warning instead of crashing span-processor setup. `Langfuse.tracer_provider` errors and the tracing-disabled warning now name the specific invalid setting.

### Fixed
- `Config#validate!` raises `ConfigurationError` instead of leaking `ArgumentError`/`NoMethodError` for non-numeric numeric fields and non-string credentials; string/bogus `cache_stale_ttl` values no longer silently pass validation.

## [0.10.1] - 2026-05-05

### Changed
Expand Down
29 changes: 28 additions & 1 deletion docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,33 @@ config = Langfuse.configuration
puts config.cache_ttl # => 60
```

### `Langfuse.configured?`

Returns whether the global configuration is valid without raising.

**Signature:**

```ruby
Langfuse.configured? # => Boolean
```

**Returns:** `true` if configuration passes full validation, `false` otherwise

**Example:**

```ruby
Langfuse.configured? # => false

Langfuse.configure do |config|
config.public_key = ENV["LANGFUSE_PUBLIC_KEY"]
config.secret_key = ENV["LANGFUSE_SECRET_KEY"]
end

Langfuse.configured? # => true
```

`configured?` reflects client configuration validity (credentials, cache, and client settings), not tracing readiness. Tracing settings (`batch_size`, `flush_interval`, `should_export_span`) are validated separately and lazily — if they are invalid, tracing degrades to no-op spans with a one-time warning instead of raising, and `configured?` can still return `true`.

### `Langfuse.reset!`

Reset configuration, caches, and client instance. Primarily for testing.
Expand Down Expand Up @@ -121,7 +148,7 @@ Return Langfuse's internal tracer provider so you can explicitly install it as t
Langfuse.tracer_provider # => OpenTelemetry::SDK::Trace::TracerProvider
```

**Raises:** `ConfigurationError` if `public_key`, `secret_key`, or `base_url` are not configured
**Raises:** `ConfigurationError` for missing credentials, a non-callable `should_export_span`, or an invalid `batch_size`/`flush_interval`; the message names the offending setting

`Langfuse.configure` does not call this for you. This is the explicit global-install seam. If you also want another OpenTelemetry backend or custom propagation, that remains application-owned setup.

Expand Down
34 changes: 32 additions & 2 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,16 +614,46 @@ Langfuse.client
# => Raises Langfuse::ConfigurationError: "public_key is required"
```

To check validity without raising, use `Config#valid?` or the module-level `Langfuse.configured?`:

```ruby
Langfuse.configured? # => false (before configure)

Langfuse.configure do |config|
config.public_key = ENV["LANGFUSE_PUBLIC_KEY"]
config.secret_key = ENV["LANGFUSE_SECRET_KEY"]
end

Langfuse.configured? # => true

# Or directly on a config object:
config = Langfuse::Config.new
config.valid? # => false (no keys set)
```

Note: `configured?` and `valid?` reflect client configuration validity, not tracing readiness. Tracing settings (`batch_size`, `flush_interval`, `should_export_span`) are validated separately when tracing is set up — invalid values disable tracing with a one-time warning, while `configured?` can still return `true`.

Validation rules:

- `public_key` must be present
- `secret_key` must be present
- `public_key` must be a non-empty String
- `secret_key` must be a non-empty String
- `base_url` must be a non-empty String
- `timeout`, `cache_max_size`, `cache_lock_timeout`, and `cache_refresh_threads` must be positive numbers
- `cache_ttl` must be a non-negative number
- `cache_stale_ttl` must be a non-negative number or `:indefinite`
- `cache_backend` must be `:memory`, `:rails`, or `:auto`
- If `:rails` is selected, or `:auto` resolves to `:rails`, Rails and `Rails.cache` must be available
- `prompt_cache_observer` must respond to `#call` (if set)
- `should_export_span` must respond to `#call` (if set)
- `mask` must respond to `#call` (if set)

Tracing readiness (checked lazily when tracing starts, and by `Langfuse.tracer_provider`):

- `public_key`, `secret_key`, and `base_url` must be non-empty Strings
- `should_export_span` must respond to `#call` (if set)
- `batch_size` must be a positive Integer
- `flush_interval` must be a positive number

## Accessing Current Configuration

```ruby
Expand Down
8 changes: 8 additions & 0 deletions docs/ERROR_HANDLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ Langfuse.configure do |config|
# Oops, forgot to set keys
end

Langfuse.configured?
# => false

Langfuse.client
# => Langfuse::ConfigurationError: public_key is required
```
Expand All @@ -45,8 +48,13 @@ Langfuse.configure do |config|
config.public_key = ENV['LANGFUSE_PUBLIC_KEY']
config.secret_key = ENV['LANGFUSE_SECRET_KEY']
end

Langfuse.configured?
# => true
```

Tracing misconfiguration never raises from `Langfuse.observe` or `Langfuse.start_observation` — invalid tracing settings (missing credentials, bad `batch_size`/`flush_interval`, non-callable `should_export_span`) degrade to no-op spans with a one-time warning naming the offending setting. Only `Langfuse.tracer_provider` and `Client.new` raise `ConfigurationError`.

**Validation checklist:**
- `public_key` present and starts with `pk-lf-`
- `secret_key` present and starts with `sk-lf-`
Expand Down
49 changes: 25 additions & 24 deletions lib/langfuse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ def configure
configuration
end

# Returns whether the global configuration is valid
#
# @note Reflects client configuration validity, not tracing readiness —
# tracing settings are validated separately and lazily by {.tracer_provider}.
# @return [Boolean] +true+ if the global configuration is valid, +false+ otherwise
def configured?
configuration.valid?
end

# Returns the global singleton client
#
# @return [Client] the global client instance
Expand All @@ -111,7 +120,8 @@ def client
# Return Langfuse's internal tracer provider for explicit global OpenTelemetry installation.
#
# @return [OpenTelemetry::SDK::Trace::TracerProvider]
# @raise [ConfigurationError] if tracing is not fully configured
# @raise [ConfigurationError] if tracing configuration is invalid; the message
# names the offending tracing setting
#
# @example
# Langfuse.configure do |config|
Expand All @@ -121,13 +131,11 @@ def client
#
# OpenTelemetry.tracer_provider = Langfuse.tracer_provider
def tracer_provider
unless tracing_config_ready?
raise ConfigurationError,
"Langfuse tracing is disabled until public_key, secret_key, and base_url are configured."
end

configuration.validate_tracing!
OtelSetup.setup(configuration) unless OtelSetup.initialized?
OtelSetup.tracer_provider
rescue ConfigurationError => e
raise ConfigurationError, tracing_disabled_message(e.message)
end

# Shutdown Langfuse and flush any pending traces and scores
Expand Down Expand Up @@ -509,7 +517,6 @@ def valid_observation_type?(type)
def otel_tracer
return tracer_provider.tracer(LANGFUSE_TRACER_NAME, Langfuse::VERSION) if setup_tracing_if_ready

warn_tracing_disabled_once
noop_tracer
end

Expand Down Expand Up @@ -547,39 +554,33 @@ def wrap_otel_span(otel_span, type_str, otel_tracer, attributes: nil)
observation_class.new(otel_span, otel_tracer, attributes: attributes)
end

# rubocop:disable Naming/PredicateMethod
def setup_tracing_if_ready
return true if OtelSetup.initialized?
return false unless tracing_config_ready?

# OtelSetup.setup validates tracing config before touching provider state
OtelSetup.setup(configuration)
true
end
# rubocop:enable Naming/PredicateMethod

def tracing_config_ready?
configured?(configuration.public_key) &&
configured?(configuration.secret_key) &&
configured?(configuration.base_url)
end

def configured?(value)
!value.nil? && !value.empty?
rescue ConfigurationError => e
warn_tracing_disabled_once(e.message)
false
end

Comment thread
cursor[bot] marked this conversation as resolved.
def warn_tracing_disabled_once
def warn_tracing_disabled_once(detail)
return if @tracing_disabled_warning_emitted

tracing_warning_mutex.synchronize do
return if @tracing_disabled_warning_emitted

configuration.logger.warn(
"Langfuse tracing is disabled until public_key, secret_key, and base_url are configured."
)
# logger is unvalidated and this path fires precisely when config is known-bad
configuration.logger&.warn(tracing_disabled_message(detail))
@tracing_disabled_warning_emitted = true
end
end

def tracing_disabled_message(detail)
"Langfuse tracing is disabled: #{detail}"
end

def tracing_warning_mutex
@tracing_warning_mutex ||= Mutex.new
end
Expand Down
98 changes: 78 additions & 20 deletions lib/langfuse/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,29 +176,46 @@ def initialize
#
# @raise [ConfigurationError] if configuration is invalid
# @return [void]
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def validate!
raise ConfigurationError, "public_key is required" if public_key.nil? || public_key.empty?
raise ConfigurationError, "secret_key is required" if secret_key.nil? || secret_key.empty?
raise ConfigurationError, "base_url cannot be empty" if base_url.nil? || base_url.empty?
raise ConfigurationError, "timeout must be positive" if timeout.nil? || timeout <= 0
raise ConfigurationError, "cache_ttl must be non-negative" if cache_ttl.nil? || cache_ttl.negative?
raise ConfigurationError, "cache_max_size must be positive" if cache_max_size.nil? || cache_max_size <= 0

if cache_lock_timeout.nil? || cache_lock_timeout <= 0
raise ConfigurationError,
"cache_lock_timeout must be positive"
end

validate_connection_settings!
validate_positive_number!("timeout", timeout)
validate_non_negative_number!("cache_ttl", cache_ttl)
validate_positive_number!("cache_max_size", cache_max_size)
validate_positive_number!("cache_lock_timeout", cache_lock_timeout)
validate_swr_config!

validate_cache_backend!
validate_prompt_cache_observer!
validate_sample_rate!
validate_should_export_span!
validate_mask!
end
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

# Check whether the configuration is valid without raising.
#
# @return [Boolean] +true+ if valid, +false+ otherwise
def valid?
validate!

true
rescue Langfuse::ConfigurationError
false
end

# Validate only the settings tracing setup and export consume.
#
# Deliberately narrower than {#validate!}: invalid cache/client settings
# must never disable tracing (parity with the Python and JS SDKs), and
# tracing-only settings like batch_size never fail client validation.
#
# @api private
# @raise [ConfigurationError] with a message naming the offending tracing setting
# @return [void]
def validate_tracing!
validate_connection_settings!
validate_should_export_span!
validate_batch_size!
validate_flush_interval!
end

# Normalize stale_ttl value
#
Expand Down Expand Up @@ -244,6 +261,48 @@ def initialize_tracing_defaults
@mask = nil
end

# The keys and URL identifying the Langfuse backend — required by both
# client validation and tracing readiness.
def validate_connection_settings!
validate_required_string!("public_key", public_key)
validate_required_string!("secret_key", secret_key)
validate_required_string!("base_url", base_url, message: "base_url cannot be empty")
end

# Type checks precede comparisons so bad types raise ConfigurationError
# instead of leaking ArgumentError/NoMethodError through #valid?.
def validate_required_string!(name, value, message: "#{name} is required")
return if value.is_a?(String) && !value.empty?

raise ConfigurationError, message
end

def validate_positive_number!(name, value)
return if value.is_a?(Numeric) && value.positive?

raise ConfigurationError, "#{name} must be positive"
end

def validate_non_negative_number!(name, value)
return if value.is_a?(Numeric) && !value.negative?

raise ConfigurationError, "#{name} must be non-negative"
end

# SpanProcessor computes batch_size * 2 and flush_interval * 1000, so both
# must be type-checked before tracing setup to avoid crashes mid-export.
def validate_batch_size!
return if batch_size.is_a?(Integer) && batch_size.positive?

raise ConfigurationError, "batch_size must be a positive Integer"
end

def validate_flush_interval!
return if flush_interval.is_a?(Numeric) && flush_interval.positive?

raise ConfigurationError, "flush_interval must be a positive number"
end

def validate_cache_backend!
valid_backends = %i[memory rails auto]
return if valid_backends.include?(cache_backend)
Expand Down Expand Up @@ -277,17 +336,16 @@ def validate_swr_stale_ttl!
"cache_stale_ttl must be non-negative or :indefinite"
end

# Validate numeric values are non-negative
return unless cache_stale_ttl.is_a?(Integer) && cache_stale_ttl.negative?
# Accept :indefinite or any non-negative numeric value; reject everything else
return if cache_stale_ttl == :indefinite
return if cache_stale_ttl.is_a?(Numeric) && !cache_stale_ttl.negative?

raise ConfigurationError,
"cache_stale_ttl must be non-negative or :indefinite"
end

def validate_refresh_threads!
return unless cache_refresh_threads.nil? || cache_refresh_threads <= 0

raise ConfigurationError, "cache_refresh_threads must be positive"
validate_positive_number!("cache_refresh_threads", cache_refresh_threads)
end

def validate_sample_rate!
Expand Down
Loading
Loading