Module: Verica

Defined in:
lib/verica.rb,
lib/verica/config.rb,
lib/verica/version.rb,
lib/verica/providers.rb,
lib/verica/openai_wrapper.rb,
lib/verica/ruby_openai_wrapper.rb

Overview

Fail-open by contract: nothing in this module ever raises into the host app.

Defined Under Namespace

Modules: Providers Classes: Config, OpenAIWrapper, RubyOpenAIWrapper

Constant Summary collapse

CONVERSATION_KEY =

Fiber-local key holding the per-request conversation override. A value of nil means "no override active" (fall back to the global config); any String (including '') means an override IS active for this fiber/thread.

:verica_conversation_override
TAGS_KEY =

Fiber-local key holding the per-request tag scope (Array); nil means no scope is active. Scopes UNION with the global config tags.

:verica_tags_scope
VERSION =
'0.1.8'

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.configObject (readonly)

Returns the value of attribute config.



21
22
23
# File 'lib/verica.rb', line 21

def config
  @config
end

Class Method Details

.conversation_idObject

Resolves the conversation id both wrappers stamp on spans: the thread-local override (when a with_conversation block is active) wins over the global config; nil/empty resolves to nil so the attribute is omitted.



43
44
45
46
47
48
# File 'lib/verica.rb', line 43

def conversation_id
  override = Thread.current[CONVERSATION_KEY]
  raw = override.nil? ? @config&.conversation_id : override
  value = raw.to_s
  value.empty? ? nil : value
end

.current_tagsObject

Global config tags ∪ the active thread-local scope, deduped in order.



67
68
69
# File 'lib/verica.rb', line 67

def current_tags
  merge_tags(@config&.tags || [], Thread.current[TAGS_KEY] || [])
end

.flushObject



127
128
129
130
131
# File 'lib/verica.rb', line 127

def flush
  OpenTelemetry.tracer_provider.force_flush if @initialized
rescue StandardError
  nil
end

.init(**options) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/verica.rb', line 71

def init(**options)
  if @initialized
    warn '[verica] init called twice; ignoring the second call.'
    return true
  end

  cfg, missing = Config.resolve(options, ENV)
  if cfg.nil?
    warn "[verica] missing #{missing.join(' and ')}; tracing is disabled."
    return false
  end

  begin
    require 'logger'
    require 'opentelemetry/sdk'
    require 'opentelemetry-exporter-otlp'

    # Spec §5: export errors (401, network) must not spam the host app's
    # logs; OTel Ruby reports them through OpenTelemetry.logger.
    OpenTelemetry.logger = Logger.new(IO::NULL) unless cfg.debug

    OpenTelemetry::SDK.configure do |c|
      c.service_name = cfg.service_name
      c.add_span_processor(
        OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
          OpenTelemetry::Exporter::OTLP::Exporter.new(
            endpoint: "#{cfg.endpoint}/v1/traces",
            headers: { 'Authorization' => "Bearer #{cfg.token}" }
          )
        )
      )
    end
    @config = cfg
    @initialized = true
    true
  rescue StandardError => e
    warn "[verica] init failed; tracing is disabled.#{cfg.debug ? " #{e.message}" : ''}"
    false
  end
end

.shutdownObject



133
134
135
136
137
138
139
140
# File 'lib/verica.rb', line 133

def shutdown
  OpenTelemetry.tracer_provider.shutdown if @initialized
rescue StandardError
  nil
ensure
  @initialized = false
  @config = nil
end

.with_conversation(id) ⇒ Object

Scopes gen_ai.conversation.id to a single request/thread, overriding the global conversation_id: from init. Thread-safe (fiber-local storage), nestable, and restored on the way out even if the block raises. id is coerced with to_s; nil/empty means "no conversation attribute" for spans emitted inside the block. Returns the block's value.

Verica.with_conversation("chat-#{conversation.id}") do
client.chat(parameters: { ... })
end


32
33
34
35
36
37
38
# File 'lib/verica.rb', line 32

def with_conversation(id)
  previous = Thread.current[CONVERSATION_KEY]
  Thread.current[CONVERSATION_KEY] = id.to_s
  yield
ensure
  Thread.current[CONVERSATION_KEY] = previous
end

.with_tags(tags) ⇒ Object

Adds tags to every span emitted inside the block, UNIONED with the global tags: from init (global first, dedup, to_s coercion). Thread-safe (fiber-local), nestable (nested scopes accumulate), restored on the way out even if the block raises. Returns the block's value.

Verica.with_tags(["mentor-chat"]) do
client.chat(parameters: { ... })
end


58
59
60
61
62
63
64
# File 'lib/verica.rb', line 58

def with_tags(tags)
  previous = Thread.current[TAGS_KEY]
  Thread.current[TAGS_KEY] = merge_tags(previous || [], tags)
  yield
ensure
  Thread.current[TAGS_KEY] = previous
end

.wrap_openai(client) ⇒ Object

Wraps the official openai gem client so chat completions emit traces.



113
114
115
# File 'lib/verica.rb', line 113

def wrap_openai(client)
  OpenAIWrapper.new(client)
end

.wrap_openai_compatible(client) ⇒ Object Also known as: wrap_ruby_openai

Wraps a client that speaks the community ruby-openai gem's chat(parameters:) API, so its calls emit traces. This is the path for any OpenAI-compatible endpoint: OpenAI itself, plus Gemini and Anthropic via their OpenAI-compatible endpoints (the provider is inferred from the model). Preferred name; wrap_ruby_openai is a back-compat alias.



122
123
124
# File 'lib/verica.rb', line 122

def wrap_openai_compatible(client)
  RubyOpenAIWrapper.new(client)
end