activeagents-telemetry 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 +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/lib/activeagents/telemetry/batching_reporter.rb +94 -0
- data/lib/activeagents/telemetry/configuration.rb +160 -0
- data/lib/activeagents/telemetry/reporter.rb +166 -0
- data/lib/activeagents/telemetry/span.rb +172 -0
- data/lib/activeagents/telemetry/trace.rb +70 -0
- data/lib/activeagents/telemetry/version.rb +7 -0
- data/lib/activeagents/telemetry.rb +57 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6c38e176c17f5a182fcab038fd6d83d9c4892323e25396233b05c8ba77c4b5fe
|
|
4
|
+
data.tar.gz: f884e7ee222f1e58d6d839c03d36e8dd8cff4f364850eff485d7f8b99e840899
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c455c628880fa3394dd19cdcb9e332f6a66624c1c85ca786d034ee29b4b20d8c3606c9a328f25a979ca29ed35a93aebe42155bc27666f4cab54feeb4502b5a5d
|
|
7
|
+
data.tar.gz: c6d2eb0384422271e510528fa363fc5b961b9aceb393e2fe575985fa9326433765e014eda642acd891e75b038f1faf25a1101d74416ef325278e6a458948d020
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-08-10
|
|
6
|
+
|
|
7
|
+
First public release of both gems: `activeagents-telemetry` (the shared
|
|
8
|
+
core) and `activeagents-telemetry-ruby_llm` (the RubyLLM adapter).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Feature parity with the ActiveAgent framework's built-in telemetry, so the
|
|
13
|
+
framework can adopt this gem as a dependency: an `enabled` kill-switch,
|
|
14
|
+
`load_from_hash` for YAML-driven config, child spans (`Span#add_span`,
|
|
15
|
+
flattened by `Trace#to_h`), `Span#set_status`/`status_message`/`measure`,
|
|
16
|
+
`BatchingReporter` (buffered delivery with `batch_size`/`flush_interval`),
|
|
17
|
+
and a pluggable `local_store` for in-process persistence without HTTP.
|
|
18
|
+
- Attribute redaction is now actually applied at delivery: span attribute
|
|
19
|
+
keys matching `redact_attributes` (by dot-separated segment) are scrubbed.
|
|
20
|
+
The framework's previous implementation declared the option but never
|
|
21
|
+
enforced it.
|
|
22
|
+
|
|
23
|
+
- `activeagents-telemetry` — shared core extracted from the ActiveAgent gem's
|
|
24
|
+
telemetry and the RubyLLM adapter: `Configuration`, `Span`, `Trace`, and
|
|
25
|
+
`Reporter`, which together own the `POST /v1/traces` wire format.
|
|
26
|
+
- `activeagents-telemetry-ruby_llm` — the RubyLLM adapter, moved out of the
|
|
27
|
+
activeagents monorepo (`ruby_llm_telemetry/`) and ported onto the shared core.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- The adapter's namespace moved from `ActiveAgents::RubyLLMTelemetry` to
|
|
32
|
+
`ActiveAgents::Telemetry::RubyLLM`, and the gem name from
|
|
33
|
+
`active_agents-ruby_llm_telemetry` to `activeagents-telemetry-ruby_llm`. The
|
|
34
|
+
predecessor was never published to RubyGems, so there is no upgrade path to
|
|
35
|
+
maintain.
|
|
36
|
+
- `subscribe!` now inherits its destination from
|
|
37
|
+
`ActiveAgents::Telemetry.configuration` when arguments are omitted, so an app
|
|
38
|
+
configures the endpoint once for every adapter.
|
|
39
|
+
- Errors are reported through `Span#record_error`, which truncates the message
|
|
40
|
+
and never transmits a backtrace.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ActiveAgents
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# activeagents-telemetry
|
|
2
|
+
|
|
3
|
+
LLM tracing for Ruby apps. Reports what your app asked the model, how long it
|
|
4
|
+
took, what it cost in tokens, and which tools ran — to the
|
|
5
|
+
[ActiveAgents](https://activeagents.ai) platform or any self-hosted ActiveAgent
|
|
6
|
+
dashboard.
|
|
7
|
+
|
|
8
|
+
**[Configuration guide → the wiki](https://github.com/activeagents/activeagents-telemetry/wiki)**
|
|
9
|
+
|
|
10
|
+
## Gems in this repo
|
|
11
|
+
|
|
12
|
+
| Gem | Use it when |
|
|
13
|
+
|-----|-------------|
|
|
14
|
+
| `activeagents-telemetry` | Shared core — wire format, configuration, delivery. Every adapter depends on it. |
|
|
15
|
+
| `activeagents-telemetry-ruby_llm` | Your app calls models through [RubyLLM](https://github.com/crmne/ruby_llm). |
|
|
16
|
+
|
|
17
|
+
Apps built on the [ActiveAgent](https://github.com/activeagents/activeagent)
|
|
18
|
+
framework don't need an adapter — the framework reports telemetry on its own.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Gemfile
|
|
24
|
+
gem "activeagents-telemetry-ruby_llm"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# config/initializers/telemetry.rb
|
|
29
|
+
RubyLLM.configure do |config|
|
|
30
|
+
config.instrumenter = ActiveSupport::Notifications # RubyLLM 1.x; 2.x wires this up in Rails
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
ActiveAgents::Telemetry.configure do |config|
|
|
34
|
+
config.api_key = ENV["ACTIVEAGENTS_API_KEY"]
|
|
35
|
+
config.service_name = "my-app"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
ActiveAgents::Telemetry::RubyLLM.subscribe!
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it. Every chat turn now reports as a trace.
|
|
42
|
+
|
|
43
|
+
## What a trace looks like
|
|
44
|
+
|
|
45
|
+
One trace per conversation turn:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
root SupportBot.respond 1,240ms OK
|
|
49
|
+
└─ llm llm.generate gpt-4o 2 rounds 1,180ms 42 tokens
|
|
50
|
+
└─ tool tool.search_docs 310ms OK
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Tool arguments and results are never sent. Error messages are truncated and
|
|
54
|
+
backtraces are never transmitted.
|
|
55
|
+
|
|
56
|
+
## Self-hosting
|
|
57
|
+
|
|
58
|
+
Point the endpoint at your mounted dashboard — same gems, same wire format:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
config.endpoint = "https://your-app.example.com/active_agent/api/traces"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Development
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle install
|
|
68
|
+
bundle exec rake test_all # core + every adapter
|
|
69
|
+
bundle exec rake build_all # build all gems into pkg/
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgents
|
|
4
|
+
module Telemetry
|
|
5
|
+
# A Reporter that buffers traces and delivers them in batches — fewer
|
|
6
|
+
# HTTP requests under sustained traffic, at the cost of traces arriving
|
|
7
|
+
# up to flush_interval seconds late.
|
|
8
|
+
#
|
|
9
|
+
# A full buffer (configuration.batch_size) flushes immediately; a
|
|
10
|
+
# background thread flushes whatever accumulated every
|
|
11
|
+
# configuration.flush_interval seconds. Call #shutdown before process
|
|
12
|
+
# exit or the tail of the buffer is lost.
|
|
13
|
+
#
|
|
14
|
+
# Sampling happens on enqueue, so a dropped trace never occupies buffer
|
|
15
|
+
# space; enabled?/configured? are also checked on enqueue AND inherited
|
|
16
|
+
# from Reporter#report at delivery time.
|
|
17
|
+
class BatchingReporter < Reporter
|
|
18
|
+
def initialize(configuration, **options)
|
|
19
|
+
super
|
|
20
|
+
@buffer = []
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
@flusher = nil
|
|
23
|
+
@send_threads = []
|
|
24
|
+
@shutdown = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Enqueues a trace, flushing if the batch is full.
|
|
28
|
+
def report(traces)
|
|
29
|
+
return if @shutdown
|
|
30
|
+
|
|
31
|
+
accepted = normalize(traces).select { sample_trace? }
|
|
32
|
+
return if accepted.empty?
|
|
33
|
+
return unless configuration.enabled? && configuration.configured?
|
|
34
|
+
|
|
35
|
+
batch = nil
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@buffer.concat(accepted)
|
|
38
|
+
batch = @buffer.slice!(0..) if @buffer.size >= configuration.batch_size
|
|
39
|
+
start_flusher
|
|
40
|
+
end
|
|
41
|
+
deliver_batch(batch) if batch
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Delivers everything buffered, blocking until done.
|
|
46
|
+
def flush
|
|
47
|
+
batch = @mutex.synchronize { @buffer.slice!(0..) }
|
|
48
|
+
deliver_batch(batch, blocking: true) unless batch.empty?
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Flushes, waits out in-flight sends, and stops the background thread —
|
|
53
|
+
# traces reported just before process exit are delivered, not dropped.
|
|
54
|
+
# Idempotent.
|
|
55
|
+
def shutdown
|
|
56
|
+
@shutdown = true
|
|
57
|
+
flush
|
|
58
|
+
@mutex.synchronize { @send_threads.dup }.each { |thread| thread.join(configuration.timeout) }
|
|
59
|
+
@flusher&.kill
|
|
60
|
+
@flusher = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def deliver_batch(batch, blocking: false)
|
|
66
|
+
body = payload_for(batch)
|
|
67
|
+
return deliver(body) if blocking || !configuration.async?
|
|
68
|
+
|
|
69
|
+
thread = Thread.new { deliver(body) }
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
@send_threads.select!(&:alive?)
|
|
72
|
+
@send_threads << thread
|
|
73
|
+
end
|
|
74
|
+
thread
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
log("failed to build trace payload: #{e.class}: #{e.message}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Lazily started so a reporter constructed at boot in a process that
|
|
80
|
+
# never traces (a console, a one-off rake task) spawns no thread.
|
|
81
|
+
def start_flusher
|
|
82
|
+
return if @flusher&.alive? || @shutdown
|
|
83
|
+
|
|
84
|
+
@flusher = Thread.new do
|
|
85
|
+
Thread.current.name = "activeagents-telemetry-flusher"
|
|
86
|
+
until @shutdown
|
|
87
|
+
sleep(configuration.flush_interval)
|
|
88
|
+
flush
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgents
|
|
4
|
+
module Telemetry
|
|
5
|
+
# Where traces go and how they get there.
|
|
6
|
+
#
|
|
7
|
+
# Every adapter takes one of these, so an app configures the destination
|
|
8
|
+
# once and each adapter inherits it:
|
|
9
|
+
#
|
|
10
|
+
# ActiveAgents::Telemetry.configure do |config|
|
|
11
|
+
# config.api_key = ENV["ACTIVEAGENTS_API_KEY"]
|
|
12
|
+
# config.service_name = "my-app"
|
|
13
|
+
# config.environment = Rails.env
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# A self-hosted ActiveAgent dashboard is the same thing with a different
|
|
17
|
+
# endpoint — point it at "https://your-app.example.com/active_agent/api/traces".
|
|
18
|
+
class Configuration
|
|
19
|
+
# The hosted platform. Self-hosters override `endpoint`.
|
|
20
|
+
DEFAULT_ENDPOINT = "https://api.activeagents.ai/v1/traces"
|
|
21
|
+
|
|
22
|
+
# Path a mounted ActiveAgent::Dashboard::Engine serves traces on.
|
|
23
|
+
LOCAL_ENDPOINT_PATH = "/active_agent/api/traces"
|
|
24
|
+
|
|
25
|
+
# Attribute keys scrubbed from spans before delivery. Name-based — a
|
|
26
|
+
# secret inside free text is not caught; see the wiki's privacy page.
|
|
27
|
+
DEFAULT_REDACT_ATTRIBUTES = %w[password secret token key credential api_key].freeze
|
|
28
|
+
|
|
29
|
+
attr_accessor :endpoint, :api_key, :service_name, :environment,
|
|
30
|
+
:resource_attributes, :logger, :timeout, :open_timeout,
|
|
31
|
+
:async, :sample_rate, :error_message_limit,
|
|
32
|
+
:enabled, :batch_size, :flush_interval,
|
|
33
|
+
:capture_bodies, :redact_attributes, :local_store
|
|
34
|
+
|
|
35
|
+
def initialize
|
|
36
|
+
@enabled = true
|
|
37
|
+
@endpoint = DEFAULT_ENDPOINT
|
|
38
|
+
@api_key = nil
|
|
39
|
+
@service_name = nil
|
|
40
|
+
@environment = nil
|
|
41
|
+
@resource_attributes = {}
|
|
42
|
+
@logger = nil
|
|
43
|
+
@timeout = 10
|
|
44
|
+
@open_timeout = 10
|
|
45
|
+
@async = true
|
|
46
|
+
@sample_rate = 1.0
|
|
47
|
+
@error_message_limit = 200
|
|
48
|
+
@batch_size = 100
|
|
49
|
+
@flush_interval = 5
|
|
50
|
+
@capture_bodies = false
|
|
51
|
+
@redact_attributes = DEFAULT_REDACT_ATTRIBUTES.dup
|
|
52
|
+
@local_store = nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Traces are only sent when there is somewhere to send them: an endpoint
|
|
56
|
+
# plus something to authenticate with, or a local store that bypasses
|
|
57
|
+
# HTTP entirely.
|
|
58
|
+
def configured?
|
|
59
|
+
local_store? || (!endpoint.to_s.empty? && !api_key.to_s.empty?)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# An explicit kill-switch over and above configured?. Defaults to true —
|
|
63
|
+
# for adapters, the presence of an api_key remains the effective switch;
|
|
64
|
+
# frameworks that want opt-in default this to false.
|
|
65
|
+
def enabled?
|
|
66
|
+
@enabled == true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A callable (trace_hash, sdk_hash) that persists traces in-process —
|
|
70
|
+
# the ActiveAgent dashboard wires this to its TelemetryTrace model.
|
|
71
|
+
def local_store?
|
|
72
|
+
!@local_store.nil?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def capture_bodies?
|
|
76
|
+
@capture_bodies == true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def async?
|
|
80
|
+
@async == true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Head-based sampling: decided once per trace, at report time.
|
|
84
|
+
def sample?
|
|
85
|
+
return true if sample_rate >= 1.0
|
|
86
|
+
return false if sample_rate <= 0.0
|
|
87
|
+
|
|
88
|
+
rand < sample_rate
|
|
89
|
+
end
|
|
90
|
+
alias should_sample? sample?
|
|
91
|
+
|
|
92
|
+
FLOAT_SETTINGS = %w[sample_rate].freeze
|
|
93
|
+
INTEGER_SETTINGS = %w[batch_size flush_interval timeout open_timeout error_message_limit].freeze
|
|
94
|
+
|
|
95
|
+
# Loads settings from a hash — how config/*.yml files reach here.
|
|
96
|
+
# Unknown keys are ignored, so a newer config file works on an older
|
|
97
|
+
# gem; numeric settings are coerced because ERB-interpolated YAML
|
|
98
|
+
# values arrive as strings.
|
|
99
|
+
def load_from_hash(hash)
|
|
100
|
+
(hash || {}).each do |key, value|
|
|
101
|
+
writer = "#{key}="
|
|
102
|
+
next unless respond_to?(writer)
|
|
103
|
+
|
|
104
|
+
value = value.to_f if FLOAT_SETTINGS.include?(key.to_s)
|
|
105
|
+
value = value.to_i if INTEGER_SETTINGS.include?(key.to_s)
|
|
106
|
+
public_send(writer, value)
|
|
107
|
+
end
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Falls back to the Rails application name, then to a generic label, so
|
|
112
|
+
# traces are attributable even when the app never sets one.
|
|
113
|
+
def resolved_service_name
|
|
114
|
+
@service_name || rails_app_name || "ruby"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def resolved_environment
|
|
118
|
+
@environment || rails_env || ENV.fetch("RACK_ENV", "production")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def resolved_logger
|
|
122
|
+
@logger || rails_logger || Logger.new(File::NULL)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def to_h
|
|
126
|
+
{
|
|
127
|
+
enabled: enabled,
|
|
128
|
+
endpoint: endpoint,
|
|
129
|
+
api_key: api_key ? "[REDACTED]" : nil,
|
|
130
|
+
service_name: resolved_service_name,
|
|
131
|
+
environment: resolved_environment,
|
|
132
|
+
sample_rate: sample_rate,
|
|
133
|
+
async: async,
|
|
134
|
+
batch_size: batch_size,
|
|
135
|
+
flush_interval: flush_interval,
|
|
136
|
+
capture_bodies: capture_bodies,
|
|
137
|
+
local_store: local_store? ? "[CALLABLE]" : nil
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def rails_app_name
|
|
144
|
+
return nil unless defined?(Rails) && Rails.application
|
|
145
|
+
|
|
146
|
+
Rails.application.class.module_parent_name.underscore
|
|
147
|
+
rescue StandardError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def rails_env
|
|
152
|
+
defined?(Rails) && Rails.respond_to?(:env) ? Rails.env.to_s : nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def rails_logger
|
|
156
|
+
defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : nil
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module ActiveAgents
|
|
8
|
+
module Telemetry
|
|
9
|
+
# Delivers traces to the configured endpoint.
|
|
10
|
+
#
|
|
11
|
+
# Telemetry is never worth breaking an app over, so every failure path here
|
|
12
|
+
# ends in a log line: a bad endpoint, a down collector, a serialization bug
|
|
13
|
+
# in an adapter, and a nil api_key all degrade to "no traces" rather than an
|
|
14
|
+
# exception in the caller's request cycle.
|
|
15
|
+
#
|
|
16
|
+
# Delivery is fire-and-forget on a thread by default. Pass `async: false`
|
|
17
|
+
# for tests, or for scripts short enough that the process may exit before a
|
|
18
|
+
# background thread flushes.
|
|
19
|
+
class Reporter
|
|
20
|
+
SDK_NAME = "activeagents-telemetry"
|
|
21
|
+
|
|
22
|
+
attr_reader :configuration
|
|
23
|
+
|
|
24
|
+
# @param sdk_name [String] overridden by adapters so ingest can tell
|
|
25
|
+
# which integration produced a trace.
|
|
26
|
+
# @param sample [Boolean] pass false when the caller already applied
|
|
27
|
+
# head-based sampling at trace creation — sampling twice compounds
|
|
28
|
+
# the rate to rate².
|
|
29
|
+
def initialize(configuration, sdk_name: SDK_NAME, sdk_version: VERSION, sample: true)
|
|
30
|
+
@configuration = configuration
|
|
31
|
+
@sdk_name = sdk_name
|
|
32
|
+
@sdk_version = sdk_version
|
|
33
|
+
@sample = sample
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param traces [Trace, Hash, Array<Trace, Hash>] traces to deliver —
|
|
37
|
+
# Trace objects or already-serialized trace hashes
|
|
38
|
+
# @return [void]
|
|
39
|
+
def report(traces)
|
|
40
|
+
traces = normalize(traces)
|
|
41
|
+
return if traces.empty?
|
|
42
|
+
return unless configuration.enabled? && configuration.configured?
|
|
43
|
+
return unless sample_trace?
|
|
44
|
+
|
|
45
|
+
body = payload_for(traces)
|
|
46
|
+
configuration.async? ? Thread.new { deliver(body) } : deliver(body)
|
|
47
|
+
nil
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
log("failed to build trace payload: #{e.class}: #{e.message}")
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Blocking delivery, for tests and for at-exit flushes.
|
|
54
|
+
def report_now(traces)
|
|
55
|
+
traces = normalize(traces)
|
|
56
|
+
return if traces.empty?
|
|
57
|
+
return unless configuration.enabled? && configuration.configured?
|
|
58
|
+
|
|
59
|
+
deliver(payload_for(traces))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Array() is wrong here: it would explode a raw trace hash into
|
|
65
|
+
# key/value pairs. Traces arrive as Trace objects or serialized hashes,
|
|
66
|
+
# singly or in arrays.
|
|
67
|
+
def normalize(traces)
|
|
68
|
+
(traces.is_a?(Array) ? traces : [ traces ]).reject { |trace| trace.nil? || (trace.respond_to?(:empty?) && trace.empty?) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Trace#to_h is already string-keyed; raw hashes may arrive symbol-keyed
|
|
72
|
+
# and both local stores and ingest read string keys.
|
|
73
|
+
def serialize(trace)
|
|
74
|
+
trace.is_a?(Trace) ? trace.to_h : deep_stringify(trace)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def deep_stringify(value)
|
|
78
|
+
case value
|
|
79
|
+
when Hash then value.each_with_object({}) { |(key, inner), out| out[key.to_s] = deep_stringify(inner) }
|
|
80
|
+
when Array then value.map { |inner| deep_stringify(inner) }
|
|
81
|
+
else value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def sample_trace?
|
|
86
|
+
!@sample || configuration.sample?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def payload_for(traces)
|
|
90
|
+
{
|
|
91
|
+
"traces" => traces.map { |trace| redact(serialize(trace)) },
|
|
92
|
+
"sdk" => {
|
|
93
|
+
"name" => @sdk_name,
|
|
94
|
+
"version" => @sdk_version,
|
|
95
|
+
"language" => "ruby",
|
|
96
|
+
"runtime_version" => RUBY_VERSION
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Scrubs span attributes whose key matches the configured redaction
|
|
102
|
+
# list. Matching is by attribute key segment ("api_key", "http.api_key"),
|
|
103
|
+
# not by value — a secret inside free text is not caught here.
|
|
104
|
+
def redact(trace_hash)
|
|
105
|
+
redacted = Array(configuration.redact_attributes).map(&:to_s)
|
|
106
|
+
return trace_hash if redacted.empty?
|
|
107
|
+
|
|
108
|
+
spans = trace_hash["spans"] || trace_hash[:spans] || []
|
|
109
|
+
spans.each do |span|
|
|
110
|
+
attributes = span["attributes"] || span[:attributes]
|
|
111
|
+
next unless attributes.is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
attributes.each_key do |key|
|
|
114
|
+
segments = key.to_s.downcase.split(".")
|
|
115
|
+
attributes[key] = "[REDACTED]" if segments.any? { |segment| redacted.include?(segment) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
trace_hash
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def deliver(body)
|
|
122
|
+
return deliver_locally(body) if configuration.local_store?
|
|
123
|
+
|
|
124
|
+
uri = URI.parse(configuration.endpoint)
|
|
125
|
+
|
|
126
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
127
|
+
http.use_ssl = uri.scheme == "https"
|
|
128
|
+
http.open_timeout = configuration.open_timeout
|
|
129
|
+
http.read_timeout = configuration.timeout
|
|
130
|
+
|
|
131
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
132
|
+
request["Content-Type"] = "application/json"
|
|
133
|
+
request["Authorization"] = "Bearer #{configuration.api_key}"
|
|
134
|
+
request["User-Agent"] = "#{@sdk_name}/#{@sdk_version} Ruby/#{RUBY_VERSION}"
|
|
135
|
+
request["X-Service-Name"] = configuration.resolved_service_name.to_s
|
|
136
|
+
request["X-Environment"] = configuration.resolved_environment.to_s
|
|
137
|
+
request.body = JSON.generate(body)
|
|
138
|
+
|
|
139
|
+
response = http.request(request)
|
|
140
|
+
log("ingest rejected traces: #{response.code} #{response.message}") unless response.is_a?(Net::HTTPSuccess)
|
|
141
|
+
response
|
|
142
|
+
rescue StandardError => e
|
|
143
|
+
log("#{e.class}: #{e.message}")
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Hands each trace to the configured local store instead of HTTP —
|
|
148
|
+
# nothing leaves the process. Per-trace rescue: one bad row must not
|
|
149
|
+
# drop its batch-mates.
|
|
150
|
+
def deliver_locally(body)
|
|
151
|
+
body["traces"].each do |trace|
|
|
152
|
+
configuration.local_store.call(trace, body["sdk"])
|
|
153
|
+
rescue StandardError => e
|
|
154
|
+
log("local store failed: #{e.class}: #{e.message}")
|
|
155
|
+
end
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def log(message)
|
|
160
|
+
configuration.resolved_logger.error("[ActiveAgents::Telemetry] #{message}")
|
|
161
|
+
rescue StandardError
|
|
162
|
+
warn "[ActiveAgents::Telemetry] #{message}"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module ActiveAgents
|
|
6
|
+
module Telemetry
|
|
7
|
+
# A single operation inside a trace, serialized to the `/v1/traces` wire
|
|
8
|
+
# format. Adapters build these rather than hand-rolling hashes, so a span
|
|
9
|
+
# from the RubyLLM adapter and a span from any future adapter are shaped
|
|
10
|
+
# identically on the wire.
|
|
11
|
+
class Span
|
|
12
|
+
TYPES = %w[root prompt llm tool thinking embedding error].freeze
|
|
13
|
+
|
|
14
|
+
OK = "OK"
|
|
15
|
+
ERROR = "ERROR"
|
|
16
|
+
UNSET = "UNSET"
|
|
17
|
+
|
|
18
|
+
ZERO_TOKENS = { "input" => 0, "output" => 0, "thinking" => 0, "total" => 0 }.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :span_id, :name, :type, :attributes, :events, :children, :status_message
|
|
21
|
+
attr_accessor :parent_span_id, :start_time, :end_time, :status
|
|
22
|
+
|
|
23
|
+
def initialize(name, type:, trace_id: nil, parent_span_id: nil, span_id: nil, attributes: {}, start_time: nil)
|
|
24
|
+
@span_id = span_id || SecureRandom.hex(8)
|
|
25
|
+
@trace_id = trace_id
|
|
26
|
+
@parent_span_id = parent_span_id
|
|
27
|
+
@name = name.to_s
|
|
28
|
+
@type = type.to_s
|
|
29
|
+
@attributes = stringify(attributes)
|
|
30
|
+
@start_time = start_time || Time.now
|
|
31
|
+
@end_time = nil
|
|
32
|
+
@status = UNSET
|
|
33
|
+
@status_message = nil
|
|
34
|
+
@tokens = ZERO_TOKENS.dup
|
|
35
|
+
@events = []
|
|
36
|
+
@children = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :trace_id
|
|
40
|
+
|
|
41
|
+
# Stamping a trace_id propagates to children, so a subtree built before
|
|
42
|
+
# its trace exists still lands in one piece.
|
|
43
|
+
def trace_id=(value)
|
|
44
|
+
@trace_id = value
|
|
45
|
+
@children.each { |child| child.trace_id = value }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Builds a child span sharing this span's trace. Instrumentation that
|
|
49
|
+
# holds only a span — not the enclosing trace — nests through this;
|
|
50
|
+
# Trace#to_h flattens the tree back out for the wire.
|
|
51
|
+
def add_span(name, type: "root", **options)
|
|
52
|
+
child = Span.new(name, type: type, trace_id: trace_id, parent_span_id: span_id, **options)
|
|
53
|
+
@children << child
|
|
54
|
+
child
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def set_attribute(key, value)
|
|
58
|
+
@attributes[key.to_s] = value
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_attributes(attrs)
|
|
63
|
+
@attributes.merge!(stringify(attrs))
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def set_tokens(input: 0, output: 0, thinking: 0)
|
|
68
|
+
@tokens = {
|
|
69
|
+
"input" => input.to_i,
|
|
70
|
+
"output" => output.to_i,
|
|
71
|
+
"thinking" => thinking.to_i,
|
|
72
|
+
"total" => input.to_i + output.to_i + thinking.to_i
|
|
73
|
+
}
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def tokens
|
|
78
|
+
@tokens.dup
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Adds another span's token counts to this one — how a turn-level llm
|
|
82
|
+
# span accumulates the rounds it covers.
|
|
83
|
+
def add_tokens(other)
|
|
84
|
+
counts = other.respond_to?(:tokens) ? other.tokens : other
|
|
85
|
+
@tokens = @tokens.merge(stringify(counts)) { |_key, carried, added| carried.to_i + added.to_i }
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def add_event(name, attributes = {})
|
|
90
|
+
@events << { "name" => name.to_s, "timestamp" => iso8601(Time.now), "attributes" => stringify(attributes) }
|
|
91
|
+
self
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @param code [Symbol, String] :ok, :error, or :unset
|
|
95
|
+
def set_status(code, message = nil)
|
|
96
|
+
@status = { ok: OK, error: ERROR, unset: UNSET }.fetch(code.to_s.downcase.to_sym, UNSET)
|
|
97
|
+
@status_message = message
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Records an error without ever putting a backtrace or an untruncated
|
|
102
|
+
# message on the wire — telemetry payloads leave the app's trust boundary.
|
|
103
|
+
def record_error(error, message_limit: 200)
|
|
104
|
+
@status = ERROR
|
|
105
|
+
@status_message = truncate(error.message.to_s, message_limit)
|
|
106
|
+
set_attribute("error.type", error.class.name)
|
|
107
|
+
set_attribute("error.message", truncate(error.message.to_s, message_limit))
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def finish(at: nil)
|
|
112
|
+
@end_time = at || Time.now
|
|
113
|
+
@status = OK if @status == UNSET
|
|
114
|
+
self
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def finished?
|
|
118
|
+
!@end_time.nil?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def duration_ms
|
|
122
|
+
return nil unless finished?
|
|
123
|
+
|
|
124
|
+
((@end_time - @start_time) * 1000).round(2)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def to_h
|
|
128
|
+
{
|
|
129
|
+
"span_id" => span_id,
|
|
130
|
+
"trace_id" => trace_id,
|
|
131
|
+
"parent_span_id" => parent_span_id,
|
|
132
|
+
"name" => name,
|
|
133
|
+
"type" => type,
|
|
134
|
+
"start_time" => iso8601(start_time),
|
|
135
|
+
"end_time" => end_time ? iso8601(end_time) : nil,
|
|
136
|
+
"duration_ms" => duration_ms,
|
|
137
|
+
"status" => status,
|
|
138
|
+
"status_message" => status_message,
|
|
139
|
+
"attributes" => attributes,
|
|
140
|
+
"tokens" => tokens,
|
|
141
|
+
"events" => events
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Runs the block, recording its outcome and timing on the span.
|
|
146
|
+
def measure
|
|
147
|
+
result = yield(self)
|
|
148
|
+
set_status(:ok) if @status == UNSET
|
|
149
|
+
result
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
record_error(e)
|
|
152
|
+
raise
|
|
153
|
+
ensure
|
|
154
|
+
finish
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def stringify(hash)
|
|
160
|
+
(hash || {}).each_with_object({}) { |(key, value), out| out[key.to_s] = value }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def truncate(string, limit)
|
|
164
|
+
string.length > limit ? "#{string[0, limit - 1]}…" : string
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def iso8601(time)
|
|
168
|
+
time.utc.strftime("%Y-%m-%dT%H:%M:%S.%6N%:z")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module ActiveAgents
|
|
6
|
+
module Telemetry
|
|
7
|
+
# A collection of spans sharing a trace_id, plus the service metadata the
|
|
8
|
+
# ingest endpoint keys on. Adapters build one per unit of work — for chat
|
|
9
|
+
# adapters, one per conversation turn.
|
|
10
|
+
class Trace
|
|
11
|
+
attr_reader :trace_id, :spans
|
|
12
|
+
attr_accessor :service_name, :environment, :resource_attributes
|
|
13
|
+
|
|
14
|
+
def initialize(trace_id: nil, service_name: nil, environment: nil, resource_attributes: {})
|
|
15
|
+
@trace_id = trace_id || SecureRandom.hex(16)
|
|
16
|
+
@service_name = service_name
|
|
17
|
+
@environment = environment
|
|
18
|
+
@resource_attributes = resource_attributes || {}
|
|
19
|
+
@spans = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Adopts the span into this trace, stamping the trace_id so adapters
|
|
23
|
+
# cannot emit a span that belongs to no trace.
|
|
24
|
+
def add_span(span)
|
|
25
|
+
span.trace_id = trace_id
|
|
26
|
+
@spans << span
|
|
27
|
+
span
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Builds and adopts a span in one step.
|
|
31
|
+
def span(name, type:, parent: nil, **options)
|
|
32
|
+
add_span(Span.new(name, type: type, parent_span_id: parent&.span_id, **options))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def root
|
|
36
|
+
@spans.find { |span| span.type == "root" }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def empty?
|
|
40
|
+
@spans.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{
|
|
45
|
+
"trace_id" => trace_id,
|
|
46
|
+
"service_name" => service_name,
|
|
47
|
+
"environment" => environment,
|
|
48
|
+
"timestamp" => timestamp,
|
|
49
|
+
"resource_attributes" => resource_attributes,
|
|
50
|
+
"spans" => spans.flat_map { |span| flatten(span) }.map(&:to_h)
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Spans built through Span#add_span nest as children; the wire format is
|
|
57
|
+
# flat, with parentage carried by parent_span_id.
|
|
58
|
+
def flatten(span)
|
|
59
|
+
[ span ] + span.children.flat_map { |child| flatten(child) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The trace's own end time, so out-of-order delivery still lands the
|
|
63
|
+
# trace at the moment it actually finished.
|
|
64
|
+
def timestamp
|
|
65
|
+
finished = spans.map(&:end_time).compact.max || Time.now
|
|
66
|
+
finished.utc.strftime("%Y-%m-%dT%H:%M:%S.%6N%:z")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
require_relative "telemetry/version"
|
|
6
|
+
require_relative "telemetry/configuration"
|
|
7
|
+
require_relative "telemetry/span"
|
|
8
|
+
require_relative "telemetry/trace"
|
|
9
|
+
require_relative "telemetry/reporter"
|
|
10
|
+
require_relative "telemetry/batching_reporter"
|
|
11
|
+
|
|
12
|
+
# Shared core for reporting LLM traces to ActiveAgents.
|
|
13
|
+
#
|
|
14
|
+
# This gem carries no integration of its own — it owns the wire format, the
|
|
15
|
+
# configuration, and delivery. Install an adapter for whatever your app calls
|
|
16
|
+
# the model through:
|
|
17
|
+
#
|
|
18
|
+
# gem "activeagents-telemetry-ruby_llm" # apps built on RubyLLM
|
|
19
|
+
#
|
|
20
|
+
# Configure the destination once and every adapter inherits it:
|
|
21
|
+
#
|
|
22
|
+
# ActiveAgents::Telemetry.configure do |config|
|
|
23
|
+
# config.api_key = ENV["ACTIVEAGENTS_API_KEY"]
|
|
24
|
+
# config.service_name = "my-app"
|
|
25
|
+
# end
|
|
26
|
+
module ActiveAgents
|
|
27
|
+
module Telemetry
|
|
28
|
+
class << self
|
|
29
|
+
def configuration
|
|
30
|
+
@configuration ||= Configuration.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def configure
|
|
34
|
+
yield(configuration) if block_given?
|
|
35
|
+
configuration
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Replaces configuration wholesale; mostly useful in tests.
|
|
39
|
+
attr_writer :configuration
|
|
40
|
+
|
|
41
|
+
def reset!
|
|
42
|
+
@configuration = Configuration.new
|
|
43
|
+
@reporter = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reporter
|
|
47
|
+
@reporter ||= Reporter.new(configuration)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
attr_writer :reporter
|
|
51
|
+
|
|
52
|
+
def enabled?
|
|
53
|
+
configuration.configured?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activeagents-telemetry
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ActiveAgents
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: |
|
|
14
|
+
The wire format, configuration, and delivery layer behind ActiveAgents
|
|
15
|
+
telemetry. Install an adapter for whatever your app calls the model
|
|
16
|
+
through — activeagents-telemetry-ruby_llm for apps built on RubyLLM —
|
|
17
|
+
and traces land in the ActiveAgents platform or any self-hosted
|
|
18
|
+
ActiveAgent dashboard (POST /v1/traces). Nothing beyond stdlib.
|
|
19
|
+
email:
|
|
20
|
+
- hello@activeagents.ai
|
|
21
|
+
executables: []
|
|
22
|
+
extensions: []
|
|
23
|
+
extra_rdoc_files: []
|
|
24
|
+
files:
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- LICENSE.txt
|
|
27
|
+
- README.md
|
|
28
|
+
- lib/activeagents/telemetry.rb
|
|
29
|
+
- lib/activeagents/telemetry/batching_reporter.rb
|
|
30
|
+
- lib/activeagents/telemetry/configuration.rb
|
|
31
|
+
- lib/activeagents/telemetry/reporter.rb
|
|
32
|
+
- lib/activeagents/telemetry/span.rb
|
|
33
|
+
- lib/activeagents/telemetry/trace.rb
|
|
34
|
+
- lib/activeagents/telemetry/version.rb
|
|
35
|
+
homepage: https://github.com/activeagents/activeagents-telemetry
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata:
|
|
39
|
+
homepage_uri: https://github.com/activeagents/activeagents-telemetry
|
|
40
|
+
source_code_uri: https://github.com/activeagents/activeagents-telemetry
|
|
41
|
+
documentation_uri: https://github.com/activeagents/activeagents-telemetry/wiki
|
|
42
|
+
changelog_uri: https://github.com/activeagents/activeagents-telemetry/blob/main/CHANGELOG.md
|
|
43
|
+
rubygems_mfa_required: 'true'
|
|
44
|
+
post_install_message:
|
|
45
|
+
rdoc_options: []
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 3.2.0
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
requirements: []
|
|
59
|
+
rubygems_version: 3.5.22
|
|
60
|
+
signing_key:
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Shared core for reporting LLM traces to ActiveAgents
|
|
63
|
+
test_files: []
|