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
nilmeans "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.9'
Class Attribute Summary collapse
-
.config ⇒ Object
readonly
Returns the value of attribute config.
Class Method Summary collapse
-
.conversation_id ⇒ Object
Resolves the conversation id both wrappers stamp on spans: the thread-local override (when a
with_conversationblock is active) wins over the global config; nil/empty resolves to nil so the attribute is omitted. -
.current_tags ⇒ Object
Global config tags ∪ the active thread-local scope, deduped in order.
- .flush ⇒ Object
- .init(**options) ⇒ Object
- .shutdown ⇒ Object
-
.with_conversation(id) ⇒ Object
Scopes
gen_ai.conversation.idto a single request/thread, overriding the globalconversation_id:frominit. -
.with_span(name, &block) ⇒ Object
Opens
nameas the active span for the block: wrapped-client calls and nested with_span/with_tool inside become children, so a whole agent run lands as ONE trace with a span tree. -
.with_tags(tags) ⇒ Object
Adds tags to every span emitted inside the block, UNIONED with the global
tags:frominit(global first, dedup,to_scoercion). -
.with_tool(name, &block) ⇒ Object
with_span + the OTel GenAI semconv tool attributes (gen_ai.operation.name = execute_tool, gen_ai.tool.name).
-
.wrap_openai(client) ⇒ Object
Wraps the official
openaigem client so chat completions emit traces. -
.wrap_openai_compatible(client) ⇒ Object
(also: wrap_ruby_openai)
Wraps a client that speaks the community
ruby-openaigem'schat(parameters:)API, so its calls emit traces.
Class Attribute Details
.config ⇒ Object (readonly)
Returns the value of attribute config.
22 23 24 |
# File 'lib/verica.rb', line 22 def config @config end |
Class Method Details
.conversation_id ⇒ Object
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.
44 45 46 47 48 49 |
# File 'lib/verica.rb', line 44 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_tags ⇒ Object
Global config tags ∪ the active thread-local scope, deduped in order.
68 69 70 |
# File 'lib/verica.rb', line 68 def (@config&. || [], Thread.current[TAGS_KEY] || []) end |
.flush ⇒ Object
149 150 151 152 153 |
# File 'lib/verica.rb', line 149 def flush OpenTelemetry.tracer_provider.force_flush if @initialized rescue StandardError nil end |
.init(**options) ⇒ Object
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/verica.rb', line 93 def init(**) if @initialized warn '[verica] init called twice; ignoring the second call.' return true end cfg, missing = Config.resolve(, 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.}" : ''}" false end end |
.shutdown ⇒ Object
155 156 157 158 159 160 161 162 |
# File 'lib/verica.rb', line 155 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
33 34 35 36 37 38 39 |
# File 'lib/verica.rb', line 33 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_span(name, &block) ⇒ Object
Opens name as the active span for the block: wrapped-client calls and
nested with_span/with_tool inside become children, so a whole agent run
lands as ONE trace with a span tree. The span name is name verbatim
(what span_check patterns match). Exceptions mark the span as error and
propagate untouched. No-op before init (fail-open). Returns the block's
value.
Verica.with_span("agent.run") { ...loop... }
80 81 82 |
# File 'lib/verica.rb', line 80 def with_span(name, &block) manual_span(name.to_s, {}, &block) 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
59 60 61 62 63 64 65 |
# File 'lib/verica.rb', line 59 def () previous = Thread.current[TAGS_KEY] Thread.current[TAGS_KEY] = (previous || [], ) yield ensure Thread.current[TAGS_KEY] = previous end |
.with_tool(name, &block) ⇒ Object
with_span + the OTel GenAI semconv tool attributes (gen_ai.operation.name = execute_tool, gen_ai.tool.name).
Verica.with_tool("search_exercises") { execute(...) }
88 89 90 91 |
# File 'lib/verica.rb', line 88 def with_tool(name, &block) n = name.to_s manual_span(n, { 'gen_ai.operation.name' => 'execute_tool', 'gen_ai.tool.name' => n }, &block) end |
.wrap_openai(client) ⇒ Object
Wraps the official openai gem client so chat completions emit traces.
135 136 137 |
# File 'lib/verica.rb', line 135 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.
144 145 146 |
# File 'lib/verica.rb', line 144 def wrap_openai_compatible(client) RubyOpenAIWrapper.new(client) end |