ask-instrumentation 0.1.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 +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/ask/instrumentation/chat.rb +13 -0
- data/lib/ask/instrumentation/embedding.rb +10 -0
- data/lib/ask/instrumentation/tool.rb +10 -0
- data/lib/ask/instrumentation/version.rb +5 -0
- data/lib/ask/instrumentation.rb +163 -0
- data/lib/ask-instrumentation.rb +1 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dcb0532585201d8c5d321aa173794fc0774b34e634b0b65ca0dcee15f2819649
|
|
4
|
+
data.tar.gz: a56ac64e426a4a01c27983cb4e705eba13ccb4a71b075ace4a2595cf4ca63229
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5aa9ae01411b237e901041d55f604283dea028e2262a5f2c818317d2e57e490a0f065f9462a5efee089b6b1e900960ac9569adc553022356d5821eea8d9d0d72
|
|
7
|
+
data.tar.gz: 69d831ccbbd59ee50374ede18ef77a073fb856b1089e8676ba18cf284bb853718fcf34d92b4c5e0f672d93f3ef20d536e1f4b3d1678834f4de354ca90251a214
|
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kaka Ruto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# ask-instrumentation
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/ask-instrumentation)
|
|
4
|
+
[](https://github.com/ask-rb/ask-instrumentation/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
LLM observability for the ask-rb ecosystem. Emits `ActiveSupport::Notifications`
|
|
7
|
+
events for chat completions, embeddings, tool calls, and image generation.
|
|
8
|
+
|
|
9
|
+
Works with **any** LLM provider — not tied to a specific one. Subscribe to events
|
|
10
|
+
for cost tracking, logging, analytics, or alerting.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "ask-instrumentation"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or install it yourself:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
gem install ask-instrumentation
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require "ask/instrumentation"
|
|
28
|
+
|
|
29
|
+
# Subscribe to all ask events
|
|
30
|
+
Ask::Instrumentation.subscribe do |event|
|
|
31
|
+
puts "#{event.name}: #{event.duration}ms"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Instrument a chat completion
|
|
35
|
+
Ask::Instrumentation.instrument("chat.ask", provider: "openai", model: "gpt-4") do
|
|
36
|
+
# your LLM call here
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Wrap with metadata context
|
|
40
|
+
Ask::Instrumentation.with_metadata(user_id: 42, session_id: "abc") do
|
|
41
|
+
Ask::Instrumentation.instrument("chat.ask", { provider: "openai", model: "gpt-4" }) do
|
|
42
|
+
# ...
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Events
|
|
48
|
+
|
|
49
|
+
| Event | Payload | Description |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `chat.ask` | provider, model, input_tokens, output_tokens, duration | Chat completion |
|
|
52
|
+
| `chat.stream.ask` | provider, model, input_tokens, output_tokens, duration | Streaming chat |
|
|
53
|
+
| `tool.ask` | provider, tool_name, tool_args, duration | Tool call |
|
|
54
|
+
| `embedding.ask` | provider, model, input_tokens, duration | Embedding |
|
|
55
|
+
| `image.ask` | provider, model, size, duration | Image generation |
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### Cost Tracking
|
|
60
|
+
|
|
61
|
+
Subscribe to chat events and sum model costs:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
require "ask/instrumentation"
|
|
65
|
+
|
|
66
|
+
COST_PER_TOKEN = {
|
|
67
|
+
"gpt-4" => { input: 0.03 / 1000, output: 0.06 / 1000 },
|
|
68
|
+
"gpt-3.5-turbo" => { input: 0.001 / 1000, output: 0.002 / 1000 },
|
|
69
|
+
"claude-3-opus" => { input: 0.015 / 1000, output: 0.075 / 1000 }
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
total_cost = 0.0
|
|
73
|
+
|
|
74
|
+
Ask::Instrumentation.subscribe(/chat\.ask/) do |event|
|
|
75
|
+
payload = event.payload
|
|
76
|
+
model = payload[:model]
|
|
77
|
+
pricing = COST_PER_TOKEN[model]
|
|
78
|
+
next unless pricing
|
|
79
|
+
|
|
80
|
+
cost = (payload[:input_tokens].to_i * pricing[:input]) +
|
|
81
|
+
(payload[:output_tokens].to_i * pricing[:output])
|
|
82
|
+
total_cost += cost
|
|
83
|
+
|
|
84
|
+
puts "[COST] #{model}: $%.6f (total: $%.4f)" % [cost, total_cost]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Later, in your application code:
|
|
88
|
+
Ask::Instrumentation.with_metadata(session_id: "sess_123") do
|
|
89
|
+
Ask::Instrumentation.instrument("chat.ask",
|
|
90
|
+
provider: "openai",
|
|
91
|
+
model: "gpt-4",
|
|
92
|
+
input_tokens: 150,
|
|
93
|
+
output_tokens: 50
|
|
94
|
+
) do
|
|
95
|
+
# actual LLM API call
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Request Logging
|
|
101
|
+
|
|
102
|
+
Log all LLM events to a file with structured data:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
require "ask/instrumentation"
|
|
106
|
+
require "logger"
|
|
107
|
+
|
|
108
|
+
logger = Logger.new("log/llm.log")
|
|
109
|
+
|
|
110
|
+
Ask::Instrumentation.subscribe do |event|
|
|
111
|
+
logger.info({
|
|
112
|
+
event: event.name,
|
|
113
|
+
duration: event.duration.round(2),
|
|
114
|
+
**event.payload
|
|
115
|
+
}.to_json)
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Usage Analytics with Metadata
|
|
120
|
+
|
|
121
|
+
Track per-user and per-session usage:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
require "ask/instrumentation"
|
|
125
|
+
|
|
126
|
+
# In your application (e.g., a Rails controller):
|
|
127
|
+
class ChatController < ApplicationController
|
|
128
|
+
def create
|
|
129
|
+
Ask::Instrumentation.with_metadata(
|
|
130
|
+
user_id: current_user.id,
|
|
131
|
+
session_id: request.session.id,
|
|
132
|
+
request_id: request.request_id
|
|
133
|
+
) do
|
|
134
|
+
# All events emitted here automatically include user/session context
|
|
135
|
+
response = llm_client.chat(params[:message])
|
|
136
|
+
render json: response
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# In a monitoring background worker:
|
|
142
|
+
Ask::Instrumentation.subscribe(/chat\.ask/) do |event|
|
|
143
|
+
payload = event.payload
|
|
144
|
+
UsageReport.increment(
|
|
145
|
+
user_id: payload[:user_id],
|
|
146
|
+
model: payload[:model],
|
|
147
|
+
tokens_in: payload[:input_tokens],
|
|
148
|
+
tokens_out: payload[:output_tokens]
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Integration with ask-llm-providers
|
|
154
|
+
|
|
155
|
+
When using the ask-rb ecosystem, providers emit events automatically:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
require "ask/provider"
|
|
159
|
+
|
|
160
|
+
# Events are emitted automatically — no manual instrumentation needed.
|
|
161
|
+
provider = Ask::Provider.for(:openai)
|
|
162
|
+
provider.complete(prompt: "Hello!") # emits chat.ask
|
|
163
|
+
|
|
164
|
+
# Subscribe to track everything:
|
|
165
|
+
Ask::Instrumentation.subscribe do |event|
|
|
166
|
+
puts "[#{event.name}] #{event.payload[:model]} (#{event.duration}ms)"
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### With Your Own Provider
|
|
171
|
+
|
|
172
|
+
You can emit events from any custom provider:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
class MyCustomProvider
|
|
176
|
+
def complete(prompt)
|
|
177
|
+
Ask::Instrumentation.instrument("chat.ask",
|
|
178
|
+
provider: "my_custom",
|
|
179
|
+
model: "my-model-v1",
|
|
180
|
+
input_tokens: prompt.length / 4
|
|
181
|
+
) do
|
|
182
|
+
response = call_api(prompt)
|
|
183
|
+
# enrich payload after the call by returning a hash from the block?
|
|
184
|
+
# No — use with_metadata or include everything upfront.
|
|
185
|
+
response
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## API
|
|
192
|
+
|
|
193
|
+
### `.subscribe(pattern = /\.ask$/, &block)`
|
|
194
|
+
|
|
195
|
+
Subscribe to ask events. Accepts an optional pattern (defaults to all `.ask` events).
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
# All ask events
|
|
199
|
+
Ask::Instrumentation.subscribe { |event| ... }
|
|
200
|
+
|
|
201
|
+
# Only chat events
|
|
202
|
+
Ask::Instrumentation.subscribe(/chat\.ask/) { |event| ... }
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### `.unsubscribe(subscriber_or_name)`
|
|
206
|
+
|
|
207
|
+
Remove a subscriber by passing the object returned from `subscribe` or a string/regexp.
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
subscriber = Ask::Instrumentation.subscribe { |e| ... }
|
|
211
|
+
Ask::Instrumentation.unsubscribe(subscriber)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### `.instrument(name, payload = {}, &block)`
|
|
215
|
+
|
|
216
|
+
Emit an event. The block is instrumented and its return value is passed through.
|
|
217
|
+
Metadata from `with_metadata` is automatically merged into the payload.
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
result = Ask::Instrumentation.instrument("chat.ask",
|
|
221
|
+
provider: "openai",
|
|
222
|
+
model: "gpt-4"
|
|
223
|
+
) do
|
|
224
|
+
llm_call
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### `.with_metadata(hash, &block)`
|
|
229
|
+
|
|
230
|
+
Set thread-local metadata that is merged into all events emitted inside the block.
|
|
231
|
+
Nested blocks merge inner metadata into outer, with inner values taking precedence.
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
Ask::Instrumentation.with_metadata(user_id: 42) do
|
|
235
|
+
Ask::Instrumentation.instrument("chat.ask", provider: "openai", model: "gpt-4") do
|
|
236
|
+
# event payload includes user_id: 42
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `.current_metadata`
|
|
242
|
+
|
|
243
|
+
Return the current thread's metadata hash (empty hash if no metadata is set).
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
Ask::Instrumentation.current_metadata # => {}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Thread Safety
|
|
250
|
+
|
|
251
|
+
All metadata is stored in `Thread.current`, making it safe to use in concurrent
|
|
252
|
+
environments. Each thread has its own metadata context:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
Ask::Instrumentation.with_metadata(thread: "main") do
|
|
256
|
+
Thread.new do
|
|
257
|
+
# This thread has its own metadata context
|
|
258
|
+
Ask::Instrumentation.current_metadata # => {}
|
|
259
|
+
Ask::Instrumentation.with_metadata(thread: "worker") do
|
|
260
|
+
# ...
|
|
261
|
+
end
|
|
262
|
+
end.join
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Development
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
git clone https://github.com/ask-rb/ask-instrumentation.git
|
|
270
|
+
cd ask-instrumentation
|
|
271
|
+
bundle install
|
|
272
|
+
bundle exec rake test
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Ask
|
|
2
|
+
module Instrumentation
|
|
3
|
+
# Placeholder for Chat-specific instrumentation helpers.
|
|
4
|
+
#
|
|
5
|
+
# This module will be loaded automatically when
|
|
6
|
+
# +Ask::Instrumentation::Chat+ is referenced (via +autoload+).
|
|
7
|
+
#
|
|
8
|
+
# Future versions may include helper methods for instrumenting
|
|
9
|
+
# streaming chat completions.
|
|
10
|
+
module Chat
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module Ask
|
|
2
|
+
module Instrumentation
|
|
3
|
+
# Placeholder for Embedding-specific instrumentation helpers.
|
|
4
|
+
#
|
|
5
|
+
# This module will be loaded automatically when
|
|
6
|
+
# +Ask::Instrumentation::Embedding+ is referenced (via +autoload+).
|
|
7
|
+
module Embedding
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
require "active_support"
|
|
2
|
+
require "active_support/notifications"
|
|
3
|
+
require_relative "instrumentation/version"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
# Instrumentation for LLM observability in the ask-rb ecosystem.
|
|
7
|
+
#
|
|
8
|
+
# Provides a thin wrapper around +ActiveSupport::Notifications+ for emitting
|
|
9
|
+
# and subscribing to LLM events such as chat completions, embeddings, tool
|
|
10
|
+
# calls, and image generation.
|
|
11
|
+
#
|
|
12
|
+
# Events follow the +{operation}.ask+ naming convention:
|
|
13
|
+
#
|
|
14
|
+
# chat.ask # Chat completion
|
|
15
|
+
# chat.stream.ask # Streaming chat
|
|
16
|
+
# tool.ask # Tool execution
|
|
17
|
+
# embedding.ask # Embedding generation
|
|
18
|
+
# image.ask # Image generation
|
|
19
|
+
#
|
|
20
|
+
# Each event payload includes provider-agnostic keys such as +provider+,
|
|
21
|
+
# +model+, +duration+, and provider-specific metrics (+input_tokens+,
|
|
22
|
+
# +output_tokens+, etc.).
|
|
23
|
+
#
|
|
24
|
+
# == Usage
|
|
25
|
+
#
|
|
26
|
+
# require "ask/instrumentation"
|
|
27
|
+
#
|
|
28
|
+
# # Subscribe to all ask events
|
|
29
|
+
# Ask::Instrumentation.subscribe do |event|
|
|
30
|
+
# puts "#{event.name}: #{event.duration}ms"
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# # Subscribe with a callable subscriber
|
|
34
|
+
# Ask::Instrumentation.subscribe(MySubscriber.new)
|
|
35
|
+
#
|
|
36
|
+
# # Instrument a chat completion
|
|
37
|
+
# Ask::Instrumentation.instrument("chat.ask", provider: "openai", model: "gpt-4") do
|
|
38
|
+
# # your LLM call here
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # Wrap with metadata context
|
|
42
|
+
# Ask::Instrumentation.with_metadata(user_id: 42, session_id: "abc") do
|
|
43
|
+
# Ask::Instrumentation.instrument("chat.ask", provider: "openai", model: "gpt-4") do
|
|
44
|
+
# # ...
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
module Instrumentation
|
|
48
|
+
autoload :Chat, "ask/instrumentation/chat"
|
|
49
|
+
autoload :Embedding, "ask/instrumentation/embedding"
|
|
50
|
+
autoload :Tool, "ask/instrumentation/tool"
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
# Subscribe to ask instrumentation events.
|
|
54
|
+
#
|
|
55
|
+
# Accepts a callable subscriber (an object that responds to +#call+)
|
|
56
|
+
# and/or a block. When a pattern is given, only events matching the
|
|
57
|
+
# pattern are delivered to the subscriber.
|
|
58
|
+
#
|
|
59
|
+
# @overload subscribe(pattern = /\.ask$/, subscriber)
|
|
60
|
+
# @param pattern [Regexp, String] Optional event name pattern
|
|
61
|
+
# @param subscriber [#call] An object that responds to +#call(event)+
|
|
62
|
+
# @return [ActiveSupport::Notifications::Fanout::Subscriber]
|
|
63
|
+
#
|
|
64
|
+
# @overload subscribe(pattern = /\.ask$/, &block)
|
|
65
|
+
# @param pattern [Regexp, String] Optional event name pattern
|
|
66
|
+
# @param block [Proc] The callback invoked on each matching event
|
|
67
|
+
# @return [ActiveSupport::Notifications::Fanout::Subscriber]
|
|
68
|
+
#
|
|
69
|
+
# @example Subscribe with a block
|
|
70
|
+
# Ask::Instrumentation.subscribe { |event| puts event.name }
|
|
71
|
+
#
|
|
72
|
+
# @example Subscribe with a callable object
|
|
73
|
+
# Ask::Instrumentation.subscribe(MySubscriber.new)
|
|
74
|
+
#
|
|
75
|
+
# @example Subscribe with a pattern and callable
|
|
76
|
+
# Ask::Instrumentation.subscribe(/chat\.ask/, MySubscriber.new)
|
|
77
|
+
def subscribe(pattern = /\.ask$/, subscriber = nil, &block)
|
|
78
|
+
ActiveSupport::Notifications.subscribe(pattern, subscriber || block)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Unsubscribe from events.
|
|
82
|
+
#
|
|
83
|
+
# Delegates to +ActiveSupport::Notifications.unsubscribe+. Accepts either
|
|
84
|
+
# the subscriber object returned from {subscribe} or a string/regexp
|
|
85
|
+
# pattern.
|
|
86
|
+
#
|
|
87
|
+
# @param subscriber_or_name [ActiveSupport::Notifications::Fanout::Subscriber, String, Regexp]
|
|
88
|
+
# The subscriber to remove
|
|
89
|
+
# @return [void]
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# subscriber = Ask::Instrumentation.subscribe { |e| puts e.name }
|
|
93
|
+
# Ask::Instrumentation.unsubscribe(subscriber)
|
|
94
|
+
def unsubscribe(subscriber_or_name)
|
|
95
|
+
ActiveSupport::Notifications.unsubscribe(subscriber_or_name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Instrument a block of code, wrapping it in an +ActiveSupport::Notifications+ event.
|
|
99
|
+
#
|
|
100
|
+
# The event name should follow the +{operation}.ask+ convention.
|
|
101
|
+
# The payload is automatically merged with the current thread metadata
|
|
102
|
+
# (set via {with_metadata}) before being emitted.
|
|
103
|
+
#
|
|
104
|
+
# @param name [String] The event name (e.g. +"chat.ask"+)
|
|
105
|
+
# @param payload [Hash] The event payload
|
|
106
|
+
# @yield The block to instrument
|
|
107
|
+
# @return [Object] The return value of the block
|
|
108
|
+
#
|
|
109
|
+
# @example
|
|
110
|
+
# Ask::Instrumentation.instrument("chat.ask", provider: "openai", model: "gpt-4") do
|
|
111
|
+
# # LLM call
|
|
112
|
+
# end
|
|
113
|
+
def instrument(name, payload = {})
|
|
114
|
+
merged = current_metadata.merge(payload)
|
|
115
|
+
ActiveSupport::Notifications.instrument(name, merged) { yield if block_given? }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Execute a block with thread-local metadata that gets merged into all
|
|
119
|
+
# events emitted within the block.
|
|
120
|
+
#
|
|
121
|
+
# Nested calls merge the inner metadata into the outer, with the inner
|
|
122
|
+
# values taking precedence for duplicate keys. The outer metadata is
|
|
123
|
+
# restored after the inner block completes.
|
|
124
|
+
#
|
|
125
|
+
# Metadata is useful for attaching context such as +user_id+,
|
|
126
|
+
# +session_id+, +request_id+, or +trace_id+ to every event.
|
|
127
|
+
#
|
|
128
|
+
# @param metadata [Hash] The metadata hash to attach
|
|
129
|
+
# @yield The block to execute with the metadata context
|
|
130
|
+
# @return [Object] The return value of the block
|
|
131
|
+
#
|
|
132
|
+
# @example
|
|
133
|
+
# Ask::Instrumentation.with_metadata(user_id: 42) do
|
|
134
|
+
# Ask::Instrumentation.instrument("chat.ask", provider: "openai", model: "gpt-4") do
|
|
135
|
+
# # ... event payload will include user_id: 42
|
|
136
|
+
# end
|
|
137
|
+
# end
|
|
138
|
+
def with_metadata(metadata = {})
|
|
139
|
+
previous = Thread.current[:ask_instrumentation_metadata]
|
|
140
|
+
Thread.current[:ask_instrumentation_metadata] = (previous || {}).merge(metadata)
|
|
141
|
+
yield
|
|
142
|
+
ensure
|
|
143
|
+
Thread.current[:ask_instrumentation_metadata] = previous
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Return the current thread's metadata hash.
|
|
147
|
+
#
|
|
148
|
+
# This is the metadata hash that will be merged into any event payload
|
|
149
|
+
# emitted via {instrument} in the current thread.
|
|
150
|
+
#
|
|
151
|
+
# @return [Hash] The current metadata (empty hash if none set)
|
|
152
|
+
#
|
|
153
|
+
# @example
|
|
154
|
+
# Ask::Instrumentation.current_metadata # => {}
|
|
155
|
+
# Ask::Instrumentation.with_metadata(user_id: 42) do
|
|
156
|
+
# Ask::Instrumentation.current_metadata # => { user_id: 42 }
|
|
157
|
+
# end
|
|
158
|
+
def current_metadata
|
|
159
|
+
Thread.current[:ask_instrumentation_metadata] || {}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "ask/instrumentation"
|
metadata
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ask-instrumentation
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kaka Ruto
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.25'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.25'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: mocha
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.1'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.1'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
description: Emits ActiveSupport::Notifications events for chat completions, embeddings,
|
|
69
|
+
tool calls, and image generation. Works with any LLM provider. Subscribe to events
|
|
70
|
+
for cost tracking, logging, analytics, or alerting.
|
|
71
|
+
email:
|
|
72
|
+
- kaka@myrrlabs.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- CHANGELOG.md
|
|
78
|
+
- LICENSE
|
|
79
|
+
- README.md
|
|
80
|
+
- lib/ask-instrumentation.rb
|
|
81
|
+
- lib/ask/instrumentation.rb
|
|
82
|
+
- lib/ask/instrumentation/chat.rb
|
|
83
|
+
- lib/ask/instrumentation/embedding.rb
|
|
84
|
+
- lib/ask/instrumentation/tool.rb
|
|
85
|
+
- lib/ask/instrumentation/version.rb
|
|
86
|
+
homepage: https://github.com/ask-rb/ask-instrumentation
|
|
87
|
+
licenses:
|
|
88
|
+
- MIT
|
|
89
|
+
metadata:
|
|
90
|
+
homepage_uri: https://github.com/ask-rb/ask-instrumentation
|
|
91
|
+
source_code_uri: https://github.com/ask-rb/ask-instrumentation
|
|
92
|
+
changelog_uri: https://github.com/ask-rb/ask-instrumentation/blob/master/CHANGELOG.md
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '3.2'
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 4.0.3
|
|
108
|
+
specification_version: 4
|
|
109
|
+
summary: LLM observability for the ask-rb ecosystem
|
|
110
|
+
test_files: []
|