tracekit 0.2.2 → 0.2.3
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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +101 -1
- data/lib/tracekit/config.rb +6 -2
- data/lib/tracekit/llm/anthropic_instrumentation.rb +218 -0
- data/lib/tracekit/llm/common.rb +118 -0
- data/lib/tracekit/llm/openai_instrumentation.rb +201 -0
- data/lib/tracekit/sdk.rb +29 -0
- data/lib/tracekit/version.rb +1 -1
- data/lib/tracekit.rb +9 -0
- metadata +9 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e67785497c76c4ff7cbeed1a4cd229c5f70fb15a83c4ed1189cb109ad7963dc
|
|
4
|
+
data.tar.gz: 00a2956e0e8ec619accbc8f033dfe1ef5f09ac8be0d2484d345afbd056ff7bda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 35bfcf1a1ab26d68b05ca50fff003d8a53f30b4ce42afe69394b57fbe80dcd1b3595568b80545ec45d8bead93e6f1e02ef4f3ec2ebfabd952b37dad587798e60
|
|
7
|
+
data.tar.gz: 22b9312c9b1af79507f9b240ec98cd4d7c6855484d86703928f74a54a4b1f2558662b78dc6e18ef4ca27dcb72ab5e5d71c36b89a40f1333161fb8973b888c3b7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.3] - 2026-03-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- LLM auto-instrumentation for OpenAI and Anthropic APIs via Module#prepend
|
|
12
|
+
- Streaming support for both OpenAI (SSE chunks) and Anthropic (SSE events) chat completions
|
|
13
|
+
- Automatic capture of gen_ai.* semantic convention attributes (model, provider, tokens, cost, latency, finish_reason)
|
|
14
|
+
- Content capture option for request/response messages (TRACEKIT_LLM_CAPTURE_CONTENT env var)
|
|
15
|
+
- Tool call detection and instrumentation for function calling
|
|
16
|
+
- PII scrubbing for captured LLM content
|
|
17
|
+
- Provider auto-detection via LoadError handling
|
|
18
|
+
- StreamWrapper for OpenAI and AnthropicStreamWrapper for Anthropic streaming responses
|
|
19
|
+
- Anthropic cache token tracking (cache_creation_input_tokens, cache_read_input_tokens)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- SDK init auto-detects and patches OpenAI::Client#chat and Anthropic::Client::Messages#create when gems are present
|
|
23
|
+
|
|
8
24
|
## [0.1.0] - 2024-02-04
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -136,3 +152,4 @@ This is the first production-ready release of the TraceKit Ruby SDK. It provides
|
|
|
136
152
|
---
|
|
137
153
|
|
|
138
154
|
[0.1.0]: https://github.com/Tracekit-Dev/ruby-sdk/releases/tag/v0.1.0
|
|
155
|
+
[0.2.3]: https://github.com/Tracekit-Dev/ruby-sdk/releases/tag/v0.2.3
|
data/README.md
CHANGED
|
@@ -24,6 +24,7 @@ TraceKit Ruby SDK provides production-ready distributed tracing, metrics, and co
|
|
|
24
24
|
- **Code Monitoring**: Live production debugging with non-breaking snapshots
|
|
25
25
|
- **Security Scanning**: Automatic detection of sensitive data (PII, credentials)
|
|
26
26
|
- **Local UI Auto-Detection**: Automatically sends traces to local TraceKit UI
|
|
27
|
+
- **LLM Auto-Instrumentation**: Zero-config tracing of OpenAI and Anthropic API calls via Module#prepend
|
|
27
28
|
- **Rails Auto-Configuration**: Zero-configuration setup via Railtie
|
|
28
29
|
- **Rack Middleware**: Automatic request instrumentation for any Rack application
|
|
29
30
|
- **Thread-Safe Metrics**: Concurrent metric collection with automatic buffering
|
|
@@ -377,6 +378,100 @@ sdk.capture_snapshot("process-data", { batch_size: 100 })
|
|
|
377
378
|
- The SDK automatically retries after the cooldown period
|
|
378
379
|
- Thread-safe via `Mutex` — safe for multi-threaded Ruby applications (Puma, Sidekiq)
|
|
379
380
|
|
|
381
|
+
## LLM Instrumentation
|
|
382
|
+
|
|
383
|
+
TraceKit automatically instruments OpenAI and Anthropic API calls when the gems are present. No manual setup required — the SDK patches clients at init via `Module#prepend`.
|
|
384
|
+
|
|
385
|
+
### Supported Gems
|
|
386
|
+
|
|
387
|
+
- **[ruby-openai](https://github.com/alexrudall/ruby-openai)** (~> 7.0) — `OpenAI::Client#chat`
|
|
388
|
+
- **[anthropic](https://github.com/alexrudall/anthropic)** (~> 0.3) — `Anthropic::Client#messages`
|
|
389
|
+
|
|
390
|
+
### Usage
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
# Just use the gems normally — TraceKit instruments automatically
|
|
394
|
+
|
|
395
|
+
# OpenAI
|
|
396
|
+
client = OpenAI::Client.new(access_token: ENV["OPENAI_API_KEY"])
|
|
397
|
+
response = client.chat(parameters: {
|
|
398
|
+
model: "gpt-4o-mini",
|
|
399
|
+
messages: [{ role: "user", content: "Hello!" }],
|
|
400
|
+
max_tokens: 100
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
# Anthropic
|
|
404
|
+
client = Anthropic::Client.new(access_token: ENV["ANTHROPIC_API_KEY"])
|
|
405
|
+
response = client.messages(parameters: {
|
|
406
|
+
model: "claude-sonnet-4-20250514",
|
|
407
|
+
max_tokens: 100,
|
|
408
|
+
messages: [{ role: "user", content: "Hello!" }]
|
|
409
|
+
})
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Streaming
|
|
413
|
+
|
|
414
|
+
Both streaming and non-streaming calls are instrumented:
|
|
415
|
+
|
|
416
|
+
```ruby
|
|
417
|
+
# OpenAI streaming
|
|
418
|
+
client.chat(parameters: {
|
|
419
|
+
model: "gpt-4o-mini",
|
|
420
|
+
messages: [{ role: "user", content: "Tell me a story" }],
|
|
421
|
+
stream: proc { |chunk, _bytesize|
|
|
422
|
+
print chunk.dig("choices", 0, "delta", "content")
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
# Anthropic streaming
|
|
427
|
+
client.messages(parameters: {
|
|
428
|
+
model: "claude-sonnet-4-20250514",
|
|
429
|
+
max_tokens: 200,
|
|
430
|
+
messages: [{ role: "user", content: "Tell me a story" }],
|
|
431
|
+
stream: proc { |event|
|
|
432
|
+
if event["type"] == "content_block_delta"
|
|
433
|
+
print event.dig("delta", "text")
|
|
434
|
+
end
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Captured Attributes
|
|
440
|
+
|
|
441
|
+
Each LLM call creates a span with [GenAI semantic convention](https://opentelemetry.io/docs/specs/semconv/gen-ai/) attributes:
|
|
442
|
+
|
|
443
|
+
| Attribute | Description |
|
|
444
|
+
|-----------|-------------|
|
|
445
|
+
| `gen_ai.system` | `openai` or `anthropic` |
|
|
446
|
+
| `gen_ai.request.model` | Model name (e.g., `gpt-4o-mini`) |
|
|
447
|
+
| `gen_ai.request.max_tokens` | Max tokens requested |
|
|
448
|
+
| `gen_ai.response.model` | Model used in response |
|
|
449
|
+
| `gen_ai.response.id` | Response ID |
|
|
450
|
+
| `gen_ai.response.finish_reason` | `stop`, `end_turn`, etc. |
|
|
451
|
+
| `gen_ai.usage.input_tokens` | Prompt tokens used |
|
|
452
|
+
| `gen_ai.usage.output_tokens` | Completion tokens used |
|
|
453
|
+
|
|
454
|
+
### Content Capture
|
|
455
|
+
|
|
456
|
+
Input/output content capture is **disabled by default** for privacy. Enable it with:
|
|
457
|
+
|
|
458
|
+
```bash
|
|
459
|
+
TRACEKIT_LLM_CAPTURE_CONTENT=true
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### Configuration
|
|
463
|
+
|
|
464
|
+
LLM instrumentation is enabled by default when OpenAI or Anthropic gems are detected. To disable:
|
|
465
|
+
|
|
466
|
+
```ruby
|
|
467
|
+
Tracekit.configure do |config|
|
|
468
|
+
config.llm = { enabled: false } # Disable all LLM instrumentation
|
|
469
|
+
config.llm = { openai: false } # Disable OpenAI only
|
|
470
|
+
config.llm = { anthropic: false } # Disable Anthropic only
|
|
471
|
+
config.llm = { capture_content: true } # Enable content capture via config
|
|
472
|
+
end
|
|
473
|
+
```
|
|
474
|
+
|
|
380
475
|
## Distributed Tracing
|
|
381
476
|
|
|
382
477
|
The SDK automatically:
|
|
@@ -452,6 +547,10 @@ ruby-sdk/
|
|
|
452
547
|
│ │ ├── sdk.rb # Main SDK class
|
|
453
548
|
│ │ ├── railtie.rb # Rails auto-configuration
|
|
454
549
|
│ │ ├── middleware.rb # Rack middleware
|
|
550
|
+
│ │ ├── llm/ # LLM auto-instrumentation
|
|
551
|
+
│ │ │ ├── common.rb # Shared helpers, PII scrubbing
|
|
552
|
+
│ │ │ ├── openai_instrumentation.rb # OpenAI Module#prepend
|
|
553
|
+
│ │ │ └── anthropic_instrumentation.rb # Anthropic Module#prepend
|
|
455
554
|
│ │ ├── metrics/ # Metrics implementation
|
|
456
555
|
│ │ ├── security/ # Security scanning
|
|
457
556
|
│ │ └── snapshots/ # Code monitoring
|
|
@@ -523,6 +622,7 @@ bundle exec rails server -p 5002
|
|
|
523
622
|
- `GET /api/call-go` - Call Go test service
|
|
524
623
|
- `GET /api/call-node` - Call Node test service
|
|
525
624
|
- `GET /api/call-all` - Call all test services
|
|
625
|
+
- `GET /api/llm` - LLM instrumentation test (OpenAI + Anthropic, streaming + non-streaming)
|
|
526
626
|
|
|
527
627
|
See [ruby-test/README.md](ruby-test/README.md) for details.
|
|
528
628
|
|
|
@@ -597,4 +697,4 @@ Built on [OpenTelemetry](https://opentelemetry.io/) - the industry standard for
|
|
|
597
697
|
---
|
|
598
698
|
|
|
599
699
|
**Repository**: git@github.com:Tracekit-Dev/ruby-sdk.git
|
|
600
|
-
**Version**: v0.2.
|
|
700
|
+
**Version**: v0.2.3
|
data/lib/tracekit/config.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Tracekit
|
|
|
6
6
|
class Config
|
|
7
7
|
attr_reader :api_key, :service_name, :endpoint, :use_ssl, :environment,
|
|
8
8
|
:service_version, :enable_code_monitoring,
|
|
9
|
-
:code_monitoring_poll_interval, :local_ui_port, :sampling_rate
|
|
9
|
+
:code_monitoring_poll_interval, :local_ui_port, :sampling_rate,
|
|
10
|
+
:llm
|
|
10
11
|
|
|
11
12
|
def initialize(builder)
|
|
12
13
|
@api_key = builder.api_key
|
|
@@ -19,6 +20,7 @@ module Tracekit
|
|
|
19
20
|
@code_monitoring_poll_interval = builder.code_monitoring_poll_interval || 30
|
|
20
21
|
@local_ui_port = builder.local_ui_port || 9999
|
|
21
22
|
@sampling_rate = builder.sampling_rate || 1.0
|
|
23
|
+
@llm = (builder.llm || {}).freeze
|
|
22
24
|
|
|
23
25
|
validate!
|
|
24
26
|
freeze # Make configuration immutable
|
|
@@ -35,7 +37,8 @@ module Tracekit
|
|
|
35
37
|
class Builder
|
|
36
38
|
attr_accessor :api_key, :service_name, :endpoint, :use_ssl, :environment,
|
|
37
39
|
:service_version, :enable_code_monitoring,
|
|
38
|
-
:code_monitoring_poll_interval, :local_ui_port, :sampling_rate
|
|
40
|
+
:code_monitoring_poll_interval, :local_ui_port, :sampling_rate,
|
|
41
|
+
:llm
|
|
39
42
|
|
|
40
43
|
def initialize
|
|
41
44
|
# Set defaults in builder
|
|
@@ -47,6 +50,7 @@ module Tracekit
|
|
|
47
50
|
@code_monitoring_poll_interval = 30
|
|
48
51
|
@local_ui_port = 9999
|
|
49
52
|
@sampling_rate = 1.0
|
|
53
|
+
@llm = { enabled: true, openai: true, anthropic: true, capture_content: false }
|
|
50
54
|
end
|
|
51
55
|
end
|
|
52
56
|
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "common"
|
|
4
|
+
|
|
5
|
+
module Tracekit
|
|
6
|
+
module LLM
|
|
7
|
+
module AnthropicInstrumentation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def install(tracer)
|
|
11
|
+
begin
|
|
12
|
+
require "anthropic"
|
|
13
|
+
rescue LoadError
|
|
14
|
+
# anthropic gem not available, check if it's already defined (e.g. in tests)
|
|
15
|
+
return false unless defined?(::Anthropic::Client)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
return false unless defined?(::Anthropic::Client)
|
|
19
|
+
|
|
20
|
+
instrumentation_mod = Module.new do
|
|
21
|
+
define_method(:messages) do |**params|
|
|
22
|
+
# When called with no parameters, return the Messages::Client (for batches etc.)
|
|
23
|
+
return super(**params) unless params[:parameters]
|
|
24
|
+
|
|
25
|
+
parameters = params[:parameters]
|
|
26
|
+
model = parameters[:model] || parameters["model"] || "unknown"
|
|
27
|
+
stream_proc = parameters[:stream] || parameters["stream"]
|
|
28
|
+
is_streaming = stream_proc.is_a?(Proc)
|
|
29
|
+
capture = Common.capture_content?
|
|
30
|
+
|
|
31
|
+
span = tracer.start_span("chat #{model}", kind: :client)
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
Common.set_request_attributes(span,
|
|
35
|
+
provider: "anthropic",
|
|
36
|
+
model: model,
|
|
37
|
+
max_tokens: parameters[:max_tokens] || parameters["max_tokens"],
|
|
38
|
+
temperature: parameters[:temperature] || parameters["temperature"],
|
|
39
|
+
top_p: parameters[:top_p] || parameters["top_p"]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Capture input content
|
|
43
|
+
if capture
|
|
44
|
+
system_prompt = parameters[:system] || parameters["system"]
|
|
45
|
+
Common.capture_system_instructions(span, system_prompt) if system_prompt
|
|
46
|
+
messages = parameters[:messages] || parameters["messages"]
|
|
47
|
+
Common.capture_input_messages(span, messages) if messages
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if is_streaming
|
|
51
|
+
# Wrap the user's stream proc to accumulate span data
|
|
52
|
+
accumulator = AnthropicStreamAccumulator.new(span, capture)
|
|
53
|
+
wrapper_proc = proc do |event|
|
|
54
|
+
accumulator.process_event(event)
|
|
55
|
+
stream_proc.call(event)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Replace stream proc with our wrapper
|
|
59
|
+
wrapped_params = parameters.merge(stream: wrapper_proc)
|
|
60
|
+
result = super(parameters: wrapped_params)
|
|
61
|
+
accumulator.finalize
|
|
62
|
+
result
|
|
63
|
+
else
|
|
64
|
+
result = super(**params)
|
|
65
|
+
handle_anthropic_response(span, result, capture)
|
|
66
|
+
result
|
|
67
|
+
end
|
|
68
|
+
rescue => e
|
|
69
|
+
Common.set_error_attributes(span, e)
|
|
70
|
+
span.finish
|
|
71
|
+
raise
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def handle_anthropic_response(span, result, capture)
|
|
78
|
+
# Anthropic response: { id, type, role, content, model, stop_reason, usage }
|
|
79
|
+
content_blocks = result["content"] || result[:content] || []
|
|
80
|
+
usage = result["usage"] || result[:usage] || {}
|
|
81
|
+
|
|
82
|
+
Common.set_response_attributes(span,
|
|
83
|
+
model: result["model"] || result[:model],
|
|
84
|
+
id: result["id"] || result[:id],
|
|
85
|
+
finish_reasons: [(result["stop_reason"] || result[:stop_reason])].compact,
|
|
86
|
+
input_tokens: usage["input_tokens"] || usage[:input_tokens],
|
|
87
|
+
output_tokens: usage["output_tokens"] || usage[:output_tokens]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Cache tokens (Anthropic-specific)
|
|
91
|
+
cache_creation = usage["cache_creation_input_tokens"] || usage[:cache_creation_input_tokens]
|
|
92
|
+
cache_read = usage["cache_read_input_tokens"] || usage[:cache_read_input_tokens]
|
|
93
|
+
span.set_attribute("gen_ai.usage.cache_creation.input_tokens", cache_creation) if cache_creation
|
|
94
|
+
span.set_attribute("gen_ai.usage.cache_read.input_tokens", cache_read) if cache_read
|
|
95
|
+
|
|
96
|
+
# Tool calls from content blocks
|
|
97
|
+
content_blocks.each do |block|
|
|
98
|
+
block_type = block["type"] || block[:type]
|
|
99
|
+
if block_type == "tool_use"
|
|
100
|
+
input_val = block["input"] || block[:input]
|
|
101
|
+
args = input_val.is_a?(String) ? input_val : JSON.generate(input_val)
|
|
102
|
+
Common.record_tool_call(span,
|
|
103
|
+
name: block["name"] || block[:name] || "unknown",
|
|
104
|
+
id: block["id"] || block[:id],
|
|
105
|
+
arguments: args
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Output content capture
|
|
111
|
+
if capture && content_blocks.any?
|
|
112
|
+
Common.capture_output_messages(span, content_blocks)
|
|
113
|
+
end
|
|
114
|
+
rescue => _e
|
|
115
|
+
# Never break user code
|
|
116
|
+
ensure
|
|
117
|
+
span.finish
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
::Anthropic::Client.prepend(instrumentation_mod)
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Accumulates streaming event data for span attributes
|
|
126
|
+
class AnthropicStreamAccumulator
|
|
127
|
+
def initialize(span, capture_content)
|
|
128
|
+
@span = span
|
|
129
|
+
@capture = capture_content
|
|
130
|
+
@model = nil
|
|
131
|
+
@id = nil
|
|
132
|
+
@stop_reason = nil
|
|
133
|
+
@input_tokens = nil
|
|
134
|
+
@output_tokens = nil
|
|
135
|
+
@cache_creation_tokens = nil
|
|
136
|
+
@cache_read_tokens = nil
|
|
137
|
+
@output_chunks = []
|
|
138
|
+
@tool_calls = {}
|
|
139
|
+
@current_block_index = 0
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def process_event(event)
|
|
143
|
+
event_type = event["type"] || event[:type]
|
|
144
|
+
|
|
145
|
+
case event_type
|
|
146
|
+
when "message_start"
|
|
147
|
+
message = event["message"] || event[:message] || {}
|
|
148
|
+
@model = message["model"] || message[:model]
|
|
149
|
+
@id = message["id"] || message[:id]
|
|
150
|
+
usage = message["usage"] || message[:usage] || {}
|
|
151
|
+
@input_tokens = usage["input_tokens"] || usage[:input_tokens]
|
|
152
|
+
@cache_creation_tokens = usage["cache_creation_input_tokens"] || usage[:cache_creation_input_tokens]
|
|
153
|
+
@cache_read_tokens = usage["cache_read_input_tokens"] || usage[:cache_read_input_tokens]
|
|
154
|
+
|
|
155
|
+
when "content_block_start"
|
|
156
|
+
@current_block_index = event["index"] || event[:index] || @current_block_index
|
|
157
|
+
cb = event["content_block"] || event[:content_block] || {}
|
|
158
|
+
if (cb["type"] || cb[:type]) == "tool_use"
|
|
159
|
+
@tool_calls[@current_block_index] = {
|
|
160
|
+
name: cb["name"] || cb[:name] || "unknown",
|
|
161
|
+
id: cb["id"] || cb[:id],
|
|
162
|
+
arguments: ""
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
when "content_block_delta"
|
|
167
|
+
delta = event["delta"] || event[:delta] || {}
|
|
168
|
+
delta_type = delta["type"] || delta[:type]
|
|
169
|
+
if delta_type == "text_delta" && @capture
|
|
170
|
+
text = delta["text"] || delta[:text]
|
|
171
|
+
@output_chunks << text if text
|
|
172
|
+
elsif delta_type == "input_json_delta"
|
|
173
|
+
partial = delta["partial_json"] || delta[:partial_json]
|
|
174
|
+
idx = event["index"] || event[:index] || @current_block_index
|
|
175
|
+
if partial && @tool_calls[idx]
|
|
176
|
+
@tool_calls[idx][:arguments] += partial
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
when "message_delta"
|
|
181
|
+
delta = event["delta"] || event[:delta] || {}
|
|
182
|
+
@stop_reason = delta["stop_reason"] || delta[:stop_reason] if delta["stop_reason"] || delta[:stop_reason]
|
|
183
|
+
usage = event["usage"] || event[:usage] || {}
|
|
184
|
+
@output_tokens = usage["output_tokens"] || usage[:output_tokens] if usage["output_tokens"] || usage[:output_tokens]
|
|
185
|
+
end
|
|
186
|
+
rescue => _e
|
|
187
|
+
# Never fail on event processing
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def finalize
|
|
191
|
+
Common.set_response_attributes(@span,
|
|
192
|
+
model: @model,
|
|
193
|
+
id: @id,
|
|
194
|
+
finish_reasons: @stop_reason ? [@stop_reason] : nil,
|
|
195
|
+
input_tokens: @input_tokens,
|
|
196
|
+
output_tokens: @output_tokens
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@span.set_attribute("gen_ai.usage.cache_creation.input_tokens", @cache_creation_tokens) if @cache_creation_tokens
|
|
200
|
+
@span.set_attribute("gen_ai.usage.cache_read.input_tokens", @cache_read_tokens) if @cache_read_tokens
|
|
201
|
+
|
|
202
|
+
@tool_calls.each_value do |tc|
|
|
203
|
+
Common.record_tool_call(@span, **tc)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
if @capture && @output_chunks.any?
|
|
207
|
+
full_content = @output_chunks.join
|
|
208
|
+
Common.capture_output_messages(@span, [{ "type" => "text", "text" => full_content }])
|
|
209
|
+
end
|
|
210
|
+
rescue => _e
|
|
211
|
+
# Never break user code
|
|
212
|
+
ensure
|
|
213
|
+
@span.finish
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Tracekit
|
|
6
|
+
module LLM
|
|
7
|
+
module Common
|
|
8
|
+
# Pattern-based PII regexes (all replaced with plain [REDACTED])
|
|
9
|
+
SENSITIVE_KEY_PATTERN = /\A(password|passwd|pwd|secret|token|key|credential|api_key|apikey)\z/i
|
|
10
|
+
EMAIL_PATTERN = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/
|
|
11
|
+
SSN_PATTERN = /\b\d{3}-\d{2}-\d{4}\b/
|
|
12
|
+
CREDIT_CARD_PATTERN = /\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b/
|
|
13
|
+
AWS_KEY_PATTERN = /\bAKIA[0-9A-Z]{16}\b/
|
|
14
|
+
BEARER_PATTERN = /Bearer\s+[A-Za-z0-9\-._~+\/]+=*/
|
|
15
|
+
STRIPE_PATTERN = /\bsk_live_[a-zA-Z0-9]+/
|
|
16
|
+
JWT_PATTERN = /\beyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/
|
|
17
|
+
PRIVATE_KEY_PATTERN = /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/
|
|
18
|
+
|
|
19
|
+
CONTENT_PATTERNS = [
|
|
20
|
+
EMAIL_PATTERN, SSN_PATTERN, CREDIT_CARD_PATTERN, AWS_KEY_PATTERN,
|
|
21
|
+
BEARER_PATTERN, STRIPE_PATTERN, JWT_PATTERN, PRIVATE_KEY_PATTERN
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
def scrub_pii(content)
|
|
27
|
+
# Try JSON key-based scrubbing first
|
|
28
|
+
begin
|
|
29
|
+
parsed = JSON.parse(content)
|
|
30
|
+
scrubbed = scrub_object(parsed)
|
|
31
|
+
return JSON.generate(scrubbed)
|
|
32
|
+
rescue JSON::ParserError
|
|
33
|
+
# Not JSON, fall through to pattern scrubbing
|
|
34
|
+
end
|
|
35
|
+
scrub_patterns(content)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def scrub_patterns(str)
|
|
39
|
+
result = str.dup
|
|
40
|
+
CONTENT_PATTERNS.each { |pat| result.gsub!(pat, "[REDACTED]") }
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def scrub_object(obj)
|
|
45
|
+
case obj
|
|
46
|
+
when Hash
|
|
47
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
48
|
+
if SENSITIVE_KEY_PATTERN.match?(k.to_s)
|
|
49
|
+
h[k] = "[REDACTED]"
|
|
50
|
+
else
|
|
51
|
+
h[k] = scrub_object(v)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
when Array
|
|
55
|
+
obj.map { |item| scrub_object(item) }
|
|
56
|
+
when String
|
|
57
|
+
scrub_patterns(obj)
|
|
58
|
+
else
|
|
59
|
+
obj
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def capture_content?
|
|
64
|
+
env_val = ENV["TRACEKIT_LLM_CAPTURE_CONTENT"]
|
|
65
|
+
return env_val.downcase == "true" || env_val == "1" if env_val
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def set_request_attributes(span, provider:, model:, max_tokens: nil, temperature: nil, top_p: nil)
|
|
70
|
+
span.set_attribute("gen_ai.operation.name", "chat")
|
|
71
|
+
span.set_attribute("gen_ai.system", provider)
|
|
72
|
+
span.set_attribute("gen_ai.request.model", model)
|
|
73
|
+
span.set_attribute("gen_ai.request.max_tokens", max_tokens) if max_tokens
|
|
74
|
+
span.set_attribute("gen_ai.request.temperature", temperature) if temperature
|
|
75
|
+
span.set_attribute("gen_ai.request.top_p", top_p) if top_p
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def set_response_attributes(span, model: nil, id: nil, finish_reasons: nil, input_tokens: nil, output_tokens: nil)
|
|
79
|
+
span.set_attribute("gen_ai.response.model", model) if model
|
|
80
|
+
span.set_attribute("gen_ai.response.id", id) if id
|
|
81
|
+
span.set_attribute("gen_ai.response.finish_reasons", finish_reasons) if finish_reasons&.any?
|
|
82
|
+
span.set_attribute("gen_ai.usage.input_tokens", input_tokens) if input_tokens
|
|
83
|
+
span.set_attribute("gen_ai.usage.output_tokens", output_tokens) if output_tokens
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def set_error_attributes(span, error)
|
|
87
|
+
span.set_attribute("error.type", error.class.name)
|
|
88
|
+
span.status = OpenTelemetry::Trace::Status.error(error.message)
|
|
89
|
+
span.record_exception(error)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def record_tool_call(span, name:, id: nil, arguments: nil)
|
|
93
|
+
attrs = { "gen_ai.tool.name" => name }
|
|
94
|
+
attrs["gen_ai.tool.call.id"] = id if id
|
|
95
|
+
attrs["gen_ai.tool.call.arguments"] = arguments if arguments
|
|
96
|
+
span.add_event("gen_ai.tool.call", attributes: attrs)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def capture_input_messages(span, messages)
|
|
100
|
+
return unless messages
|
|
101
|
+
serialized = JSON.generate(messages)
|
|
102
|
+
span.set_attribute("gen_ai.input.messages", scrub_pii(serialized))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def capture_output_messages(span, content)
|
|
106
|
+
return unless content
|
|
107
|
+
serialized = JSON.generate(content)
|
|
108
|
+
span.set_attribute("gen_ai.output.messages", scrub_pii(serialized))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def capture_system_instructions(span, system)
|
|
112
|
+
return unless system
|
|
113
|
+
serialized = system.is_a?(String) ? system : JSON.generate(system)
|
|
114
|
+
span.set_attribute("gen_ai.system_instructions", scrub_pii(serialized))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "common"
|
|
4
|
+
|
|
5
|
+
module Tracekit
|
|
6
|
+
module LLM
|
|
7
|
+
module OpenAIInstrumentation
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def install(tracer)
|
|
11
|
+
# Try to load the OpenAI gem
|
|
12
|
+
begin
|
|
13
|
+
require "openai"
|
|
14
|
+
rescue LoadError
|
|
15
|
+
# openai gem not available, check if it's already defined (e.g. in tests)
|
|
16
|
+
return false unless defined?(::OpenAI::Client)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
client_class = ::OpenAI::Client
|
|
20
|
+
return false unless client_class
|
|
21
|
+
|
|
22
|
+
# Create the prepend module dynamically with tracer closure
|
|
23
|
+
instrumentation_mod = Module.new do
|
|
24
|
+
define_method(:chat) do |parameters: {}|
|
|
25
|
+
model = parameters[:model] || parameters["model"] || "unknown"
|
|
26
|
+
stream_proc = parameters[:stream] || parameters["stream"]
|
|
27
|
+
is_streaming = stream_proc.is_a?(Proc)
|
|
28
|
+
capture = Common.capture_content?
|
|
29
|
+
|
|
30
|
+
span = tracer.start_span("chat #{model}", kind: :client)
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
Common.set_request_attributes(span,
|
|
34
|
+
provider: "openai",
|
|
35
|
+
model: model,
|
|
36
|
+
max_tokens: parameters[:max_tokens] || parameters["max_tokens"] || parameters[:max_completion_tokens] || parameters["max_completion_tokens"],
|
|
37
|
+
temperature: parameters[:temperature] || parameters["temperature"],
|
|
38
|
+
top_p: parameters[:top_p] || parameters["top_p"]
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Capture input content
|
|
42
|
+
if capture
|
|
43
|
+
messages = parameters[:messages] || parameters["messages"]
|
|
44
|
+
if messages
|
|
45
|
+
system_msgs = messages.select { |m| (m[:role] || m["role"]) == "system" }
|
|
46
|
+
non_system = messages.reject { |m| (m[:role] || m["role"]) == "system" }
|
|
47
|
+
Common.capture_system_instructions(span, system_msgs) if system_msgs.any?
|
|
48
|
+
Common.capture_input_messages(span, non_system)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if is_streaming
|
|
53
|
+
# ruby-openai handles streaming via proc callback internally.
|
|
54
|
+
# The chat method returns the final response hash, not an enumerator.
|
|
55
|
+
# We wrap the user's proc to accumulate span data from each chunk.
|
|
56
|
+
accumulator = OpenAIStreamAccumulator.new(span, capture)
|
|
57
|
+
wrapper_proc = proc do |chunk, bytesize|
|
|
58
|
+
accumulator.process_chunk(chunk)
|
|
59
|
+
# Call original proc with same args
|
|
60
|
+
if stream_proc.arity == 2 || stream_proc.arity < 0
|
|
61
|
+
stream_proc.call(chunk, bytesize)
|
|
62
|
+
else
|
|
63
|
+
stream_proc.call(chunk)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Inject stream_options.include_usage for token counting
|
|
68
|
+
params = parameters.dup
|
|
69
|
+
so = params[:stream_options] || params["stream_options"] || {}
|
|
70
|
+
unless so[:include_usage] || so["include_usage"]
|
|
71
|
+
params[:stream_options] = so.merge(include_usage: true)
|
|
72
|
+
end
|
|
73
|
+
params[:stream] = wrapper_proc
|
|
74
|
+
|
|
75
|
+
result = super(parameters: params)
|
|
76
|
+
accumulator.finalize
|
|
77
|
+
result
|
|
78
|
+
else
|
|
79
|
+
result = super(parameters: parameters)
|
|
80
|
+
|
|
81
|
+
# Non-streaming response handling
|
|
82
|
+
handle_response(span, result, capture)
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
rescue => e
|
|
86
|
+
Common.set_error_attributes(span, e)
|
|
87
|
+
span.finish
|
|
88
|
+
raise
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def handle_response(span, result, capture)
|
|
95
|
+
choices = result.dig("choices") || []
|
|
96
|
+
Common.set_response_attributes(span,
|
|
97
|
+
model: result["model"],
|
|
98
|
+
id: result["id"],
|
|
99
|
+
finish_reasons: choices.map { |c| c["finish_reason"] }.compact,
|
|
100
|
+
input_tokens: result.dig("usage", "prompt_tokens"),
|
|
101
|
+
output_tokens: result.dig("usage", "completion_tokens")
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Tool calls
|
|
105
|
+
choices.each do |choice|
|
|
106
|
+
(choice.dig("message", "tool_calls") || []).each do |tc|
|
|
107
|
+
Common.record_tool_call(span,
|
|
108
|
+
name: tc.dig("function", "name") || "unknown",
|
|
109
|
+
id: tc["id"],
|
|
110
|
+
arguments: tc.dig("function", "arguments")
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Output content capture
|
|
116
|
+
if capture && choices.any?
|
|
117
|
+
output_msgs = choices.map { |c| c["message"] }.compact
|
|
118
|
+
Common.capture_output_messages(span, output_msgs) if output_msgs.any?
|
|
119
|
+
end
|
|
120
|
+
rescue => _e
|
|
121
|
+
# Never break user code
|
|
122
|
+
ensure
|
|
123
|
+
span.finish
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
client_class.prepend(instrumentation_mod)
|
|
128
|
+
true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Accumulates streaming chunk data for span attributes via proc interception
|
|
132
|
+
class OpenAIStreamAccumulator
|
|
133
|
+
def initialize(span, capture_content)
|
|
134
|
+
@span = span
|
|
135
|
+
@capture = capture_content
|
|
136
|
+
@model = nil
|
|
137
|
+
@id = nil
|
|
138
|
+
@finish_reason = nil
|
|
139
|
+
@input_tokens = nil
|
|
140
|
+
@output_tokens = nil
|
|
141
|
+
@output_chunks = []
|
|
142
|
+
@tool_calls = {}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def process_chunk(chunk)
|
|
146
|
+
@model ||= chunk.dig("model")
|
|
147
|
+
@id ||= chunk.dig("id")
|
|
148
|
+
|
|
149
|
+
if (usage = chunk["usage"])
|
|
150
|
+
@input_tokens = usage["prompt_tokens"] if usage["prompt_tokens"]
|
|
151
|
+
@output_tokens = usage["completion_tokens"] if usage["completion_tokens"]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
(chunk["choices"] || []).each do |choice|
|
|
155
|
+
@finish_reason = choice["finish_reason"] if choice["finish_reason"]
|
|
156
|
+
delta = choice["delta"] || {}
|
|
157
|
+
@output_chunks << delta["content"] if @capture && delta["content"]
|
|
158
|
+
|
|
159
|
+
(delta["tool_calls"] || []).each do |tc|
|
|
160
|
+
idx = tc["index"] || 0
|
|
161
|
+
if @tool_calls[idx]
|
|
162
|
+
@tool_calls[idx][:arguments] = (@tool_calls[idx][:arguments] || "") + (tc.dig("function", "arguments") || "")
|
|
163
|
+
else
|
|
164
|
+
@tool_calls[idx] = {
|
|
165
|
+
name: tc.dig("function", "name") || "unknown",
|
|
166
|
+
id: tc["id"],
|
|
167
|
+
arguments: tc.dig("function", "arguments") || ""
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
rescue => _e
|
|
173
|
+
# Never fail on chunk processing
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def finalize
|
|
177
|
+
Common.set_response_attributes(@span,
|
|
178
|
+
model: @model,
|
|
179
|
+
id: @id,
|
|
180
|
+
finish_reasons: @finish_reason ? [@finish_reason] : nil,
|
|
181
|
+
input_tokens: @input_tokens,
|
|
182
|
+
output_tokens: @output_tokens
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@tool_calls.each_value do |tc|
|
|
186
|
+
Common.record_tool_call(@span, **tc)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if @capture && @output_chunks.any?
|
|
190
|
+
full_content = @output_chunks.join
|
|
191
|
+
Common.capture_output_messages(@span, [{ "role" => "assistant", "content" => full_content }])
|
|
192
|
+
end
|
|
193
|
+
rescue => _e
|
|
194
|
+
# Never break user code
|
|
195
|
+
ensure
|
|
196
|
+
@span.finish
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
data/lib/tracekit/sdk.rb
CHANGED
|
@@ -90,6 +90,9 @@ module Tracekit
|
|
|
90
90
|
# Initialize OpenTelemetry tracer
|
|
91
91
|
setup_tracing(traces_endpoint)
|
|
92
92
|
|
|
93
|
+
# Initialize LLM instrumentation (auto-detect providers)
|
|
94
|
+
setup_llm_instrumentation if defined?(Tracekit::LLM)
|
|
95
|
+
|
|
93
96
|
# Initialize metrics registry
|
|
94
97
|
@metrics_registry = Metrics::Registry.new(metrics_endpoint, config.api_key, config.service_name)
|
|
95
98
|
|
|
@@ -152,6 +155,32 @@ module Tracekit
|
|
|
152
155
|
|
|
153
156
|
private
|
|
154
157
|
|
|
158
|
+
def setup_llm_instrumentation
|
|
159
|
+
llm_config = @config.llm || {}
|
|
160
|
+
return unless llm_config.fetch(:enabled, true)
|
|
161
|
+
|
|
162
|
+
tracer = OpenTelemetry.tracer_provider.tracer("tracekit-llm", Tracekit::VERSION)
|
|
163
|
+
|
|
164
|
+
# Set capture_content env var from config if not already set
|
|
165
|
+
if llm_config[:capture_content] && !ENV.key?("TRACEKIT_LLM_CAPTURE_CONTENT")
|
|
166
|
+
ENV["TRACEKIT_LLM_CAPTURE_CONTENT"] = "true"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if llm_config.fetch(:openai, true)
|
|
170
|
+
if Tracekit::LLM::OpenAIInstrumentation.install(tracer)
|
|
171
|
+
puts "TraceKit: OpenAI LLM instrumentation enabled"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if llm_config.fetch(:anthropic, true)
|
|
176
|
+
if Tracekit::LLM::AnthropicInstrumentation.install(tracer)
|
|
177
|
+
puts "TraceKit: Anthropic LLM instrumentation enabled"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
rescue => e
|
|
181
|
+
puts "TraceKit: LLM instrumentation setup failed: #{e.message}"
|
|
182
|
+
end
|
|
183
|
+
|
|
155
184
|
def setup_tracing(traces_endpoint)
|
|
156
185
|
OpenTelemetry::SDK.configure do |c|
|
|
157
186
|
c.service_name = @config.service_name
|
data/lib/tracekit/version.rb
CHANGED
data/lib/tracekit.rb
CHANGED
|
@@ -23,6 +23,15 @@ require_relative "tracekit/local_ui_detector"
|
|
|
23
23
|
require_relative "tracekit/snapshots/models"
|
|
24
24
|
require_relative "tracekit/snapshots/client"
|
|
25
25
|
|
|
26
|
+
# LLM instrumentation
|
|
27
|
+
begin
|
|
28
|
+
require_relative "tracekit/llm/common"
|
|
29
|
+
require_relative "tracekit/llm/openai_instrumentation"
|
|
30
|
+
require_relative "tracekit/llm/anthropic_instrumentation"
|
|
31
|
+
rescue LoadError
|
|
32
|
+
# LLM instrumentation not available
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
# Core SDK
|
|
27
36
|
require_relative "tracekit/sdk"
|
|
28
37
|
require_relative "tracekit/middleware"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tracekit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- TraceKit
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: opentelemetry-sdk
|
|
@@ -150,6 +150,9 @@ files:
|
|
|
150
150
|
- lib/tracekit.rb
|
|
151
151
|
- lib/tracekit/config.rb
|
|
152
152
|
- lib/tracekit/endpoint_resolver.rb
|
|
153
|
+
- lib/tracekit/llm/anthropic_instrumentation.rb
|
|
154
|
+
- lib/tracekit/llm/common.rb
|
|
155
|
+
- lib/tracekit/llm/openai_instrumentation.rb
|
|
153
156
|
- lib/tracekit/local_ui/detector.rb
|
|
154
157
|
- lib/tracekit/local_ui_detector.rb
|
|
155
158
|
- lib/tracekit/metrics/counter.rb
|
|
@@ -173,7 +176,7 @@ metadata:
|
|
|
173
176
|
homepage_uri: https://github.com/Tracekit-Dev/ruby-sdk
|
|
174
177
|
source_code_uri: https://github.com/Tracekit-Dev/ruby-sdk
|
|
175
178
|
changelog_uri: https://github.com/Tracekit-Dev/ruby-sdk/blob/main/CHANGELOG.md
|
|
176
|
-
post_install_message:
|
|
179
|
+
post_install_message:
|
|
177
180
|
rdoc_options: []
|
|
178
181
|
require_paths:
|
|
179
182
|
- lib
|
|
@@ -188,8 +191,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
188
191
|
- !ruby/object:Gem::Version
|
|
189
192
|
version: '0'
|
|
190
193
|
requirements: []
|
|
191
|
-
rubygems_version: 3.
|
|
192
|
-
signing_key:
|
|
194
|
+
rubygems_version: 3.0.3.1
|
|
195
|
+
signing_key:
|
|
193
196
|
specification_version: 4
|
|
194
197
|
summary: TraceKit Ruby SDK - OpenTelemetry-based APM for Ruby applications
|
|
195
198
|
test_files: []
|