agentbill-sdk 5.0.1 → 9.4.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 +4 -4
- data/.github/workflows/ci.yml +5 -4
- data/CHANGELOG.md +53 -1
- data/examples/ollama_basic.rb +81 -0
- data/examples/perplexity_basic.rb +66 -0
- data/lib/agentbill/agents.rb +226 -0
- data/lib/agentbill/attributes.rb +95 -0
- data/lib/agentbill/customers.rb +164 -0
- data/lib/agentbill/distributed.rb +109 -0
- data/lib/agentbill/exceptions.rb +84 -0
- data/lib/agentbill/ollama_wrapper.rb +133 -0
- data/lib/agentbill/orders.rb +283 -0
- data/lib/agentbill/perplexity_wrapper.rb +75 -0
- data/lib/agentbill/signal_types.rb +179 -0
- data/lib/agentbill/signals.rb +271 -0
- data/lib/agentbill/tracer.rb +68 -11
- data/lib/agentbill/tracing.rb +343 -0
- data/lib/agentbill/version.rb +1 -1
- data/lib/agentbill/wrappers.rb +384 -0
- data/lib/agentbill.rb +269 -82
- metadata +16 -2
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'time'
|
|
9
|
+
|
|
10
|
+
require_relative 'distributed'
|
|
11
|
+
|
|
12
|
+
module AgentBill
|
|
13
|
+
# Signal module for tracking business events and linking revenue to AI traces.
|
|
14
|
+
module Signals
|
|
15
|
+
BASE_URL = 'https://api.agentbill.io'
|
|
16
|
+
VERSION = '9.4.0'
|
|
17
|
+
|
|
18
|
+
@config = {}
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
attr_accessor :config
|
|
22
|
+
|
|
23
|
+
def set_config(options)
|
|
24
|
+
@config = options
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_config
|
|
28
|
+
@config
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Emit a business signal/event and link it to AI traces.
|
|
35
|
+
#
|
|
36
|
+
# @param event_name [String] Name of the business event
|
|
37
|
+
# @param options [Hash] Signal options
|
|
38
|
+
# @option options [Float] :revenue Revenue amount
|
|
39
|
+
# @option options [Hash] :metadata Additional metadata
|
|
40
|
+
# @option options [String] :customer_id Customer ID
|
|
41
|
+
# @option options [String] :session_id Session ID
|
|
42
|
+
# @option options [String] :trace_id Trace ID to link to
|
|
43
|
+
# @option options [String] :span_id Span ID to link to
|
|
44
|
+
# @option options [String] :parent_span_id Parent span ID
|
|
45
|
+
# @option options [String] :currency Currency (default: USD)
|
|
46
|
+
# @option options [String] :event_type Event type
|
|
47
|
+
# @option options [Float] :event_value Event value
|
|
48
|
+
# @option options [String] :order_id Order ID to link to (v7.8.0)
|
|
49
|
+
# @option options [String] :order_external_id External order ID (v7.8.0)
|
|
50
|
+
# @option options [Hash] :group_by Business grouping keys (v9.2.0) — session_id, workflow_id, batch_id, correlation_id
|
|
51
|
+
# @option options [String] :idempotency_key Dedup key (v9.2.0) — auto-generated if not provided
|
|
52
|
+
# @option options [Hash] :cost_data Manual cost/usage data (v9.2.0 Phase 3)
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash] Result with success status
|
|
55
|
+
#
|
|
56
|
+
# @example
|
|
57
|
+
# AgentBill.signal('purchase', revenue: 99.99, metadata: { product_id: 'prod-123' })
|
|
58
|
+
def signal(event_name, **options)
|
|
59
|
+
config = Signals.get_config
|
|
60
|
+
|
|
61
|
+
api_key = config[:api_key]
|
|
62
|
+
raise ArgumentError, 'AgentBill not initialized. Call AgentBill.init() first or use AgentBill.tracing context.' unless api_key
|
|
63
|
+
|
|
64
|
+
effective_customer_id = options[:customer_id] || config[:customer_id]
|
|
65
|
+
|
|
66
|
+
# Auto-detect trace context if not provided
|
|
67
|
+
ctx = Distributed.get_trace_context
|
|
68
|
+
trace_id = options[:trace_id]
|
|
69
|
+
parent_span_id = options[:parent_span_id]
|
|
70
|
+
|
|
71
|
+
if ctx
|
|
72
|
+
trace_id ||= ctx[:trace_id]
|
|
73
|
+
parent_span_id ||= ctx[:span_id] unless parent_span_id
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
generated_span_id = options[:span_id] || SecureRandom.hex(8)
|
|
77
|
+
generated_trace_id = trace_id || SecureRandom.hex(16)
|
|
78
|
+
now_ns = (Time.now.to_f * 1_000_000_000).to_i
|
|
79
|
+
timestamp_s = Time.now.to_i
|
|
80
|
+
|
|
81
|
+
# Auto-generate idempotency key
|
|
82
|
+
idempotency_key = options[:idempotency_key] || Digest::SHA256.hexdigest("#{generated_trace_id}:#{event_name}:#{timestamp_s}")[0, 32]
|
|
83
|
+
|
|
84
|
+
# Build OTEL-compliant span attributes
|
|
85
|
+
attributes = [
|
|
86
|
+
{ key: 'agentbill.event_name', value: { stringValue: event_name } },
|
|
87
|
+
{ key: 'agentbill.is_business_event', value: { boolValue: true } }
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
attributes << { key: 'agentbill.customer_id', value: { stringValue: effective_customer_id } } if effective_customer_id
|
|
91
|
+
attributes << { key: 'agentbill.agent_id', value: { stringValue: config[:agent_id] } } if config[:agent_id]
|
|
92
|
+
|
|
93
|
+
if options[:revenue]
|
|
94
|
+
attributes << { key: 'agentbill.revenue', value: { doubleValue: options[:revenue] } }
|
|
95
|
+
attributes << { key: 'agentbill.currency', value: { stringValue: options[:currency] || 'USD' } }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
attributes << { key: 'agentbill.event_value', value: { doubleValue: options[:event_value] } } if options[:event_value]
|
|
99
|
+
attributes << { key: 'agentbill.event_type', value: { stringValue: options[:event_type] } } if options[:event_type]
|
|
100
|
+
attributes << { key: 'agentbill.session_id', value: { stringValue: options[:session_id] } } if options[:session_id]
|
|
101
|
+
attributes << { key: 'agentbill.parent_span_id', value: { stringValue: parent_span_id } } if parent_span_id
|
|
102
|
+
# v7.8.0: Order linking
|
|
103
|
+
attributes << { key: 'agentbill.order_id', value: { stringValue: options[:order_id] } } if options[:order_id]
|
|
104
|
+
attributes << { key: 'agentbill.order_external_id', value: { stringValue: options[:order_external_id] } } if options[:order_external_id]
|
|
105
|
+
attributes << { key: 'agentbill.metadata', value: { stringValue: JSON.generate(options[:metadata]) } } if options[:metadata]
|
|
106
|
+
|
|
107
|
+
# v9.2.0: Idempotency key
|
|
108
|
+
attributes << { key: 'agentbill.idempotency_key', value: { stringValue: idempotency_key } }
|
|
109
|
+
|
|
110
|
+
# v9.2.0: Business grouping (whitelisted keys only)
|
|
111
|
+
grouping_keys = %w[session_id workflow_id batch_id correlation_id].freeze
|
|
112
|
+
if options[:group_by].is_a?(Hash)
|
|
113
|
+
options[:group_by].each do |key, value|
|
|
114
|
+
attributes << { key: "agentbill.#{key}", value: { stringValue: value.to_s } } if grouping_keys.include?(key.to_s)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# v9.4.0: enable_cost_tracing flag
|
|
119
|
+
if config[:enable_cost_tracing]
|
|
120
|
+
attributes << { key: 'agentbill.cost_tracing.enabled', value: { boolValue: true } }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# v9.2.0: Manual cost (Phase 3 – two-variant model)
|
|
124
|
+
if options[:cost_data].is_a?(Hash)
|
|
125
|
+
cd = options[:cost_data]
|
|
126
|
+
attributes << { key: 'agentbill.cost.provider', value: { stringValue: cd[:vendor] } } if cd[:vendor]
|
|
127
|
+
if cd[:cost].is_a?(Hash)
|
|
128
|
+
attributes << { key: 'agentbill.cost.provided', value: { boolValue: true } }
|
|
129
|
+
attributes << { key: 'agentbill.cost.amount', value: { doubleValue: cd[:cost][:amount] } }
|
|
130
|
+
attributes << { key: 'agentbill.cost.currency', value: { stringValue: cd[:cost][:currency] || 'USD' } }
|
|
131
|
+
end
|
|
132
|
+
if cd[:attributes].is_a?(Hash)
|
|
133
|
+
attributes << { key: 'gen_ai.request.model', value: { stringValue: cd[:attributes][:model] } } if cd[:attributes][:model]
|
|
134
|
+
attributes << { key: 'gen_ai.usage.prompt_tokens', value: { intValue: cd[:attributes][:input_tokens] } } if cd[:attributes][:input_tokens]
|
|
135
|
+
attributes << { key: 'gen_ai.usage.completion_tokens', value: { intValue: cd[:attributes][:output_tokens] } } if cd[:attributes][:output_tokens]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
# Build OTEL payload
|
|
139
|
+
payload = {
|
|
140
|
+
resourceSpans: [{
|
|
141
|
+
resource: {
|
|
142
|
+
attributes: [
|
|
143
|
+
{ key: 'service.name', value: { stringValue: 'agentbill-ruby-sdk' } },
|
|
144
|
+
{ key: 'agentbill.customer_id', value: { stringValue: effective_customer_id || '' } },
|
|
145
|
+
{ key: 'agentbill.agent_id', value: { stringValue: config[:agent_id] || '' } }
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
scopeSpans: [{
|
|
149
|
+
scope: { name: 'agentbill.signals', version: Signals::VERSION },
|
|
150
|
+
spans: [{
|
|
151
|
+
traceId: generated_trace_id,
|
|
152
|
+
spanId: generated_span_id,
|
|
153
|
+
parentSpanId: parent_span_id || '',
|
|
154
|
+
name: 'agentbill.trace.signal',
|
|
155
|
+
kind: 1,
|
|
156
|
+
startTimeUnixNano: now_ns.to_s,
|
|
157
|
+
endTimeUnixNano: now_ns.to_s,
|
|
158
|
+
attributes: attributes,
|
|
159
|
+
status: { code: 1 }
|
|
160
|
+
}]
|
|
161
|
+
}]
|
|
162
|
+
}]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
base_url = config[:base_url] || Signals::BASE_URL
|
|
166
|
+
debug = config[:debug] || false
|
|
167
|
+
|
|
168
|
+
begin
|
|
169
|
+
url = URI("#{base_url}/functions/v1/otel-collector")
|
|
170
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
171
|
+
http.use_ssl = true
|
|
172
|
+
http.read_timeout = 10
|
|
173
|
+
|
|
174
|
+
request = Net::HTTP::Post.new(url)
|
|
175
|
+
request['x-api-key'] = api_key
|
|
176
|
+
request['Content-Type'] = 'application/json'
|
|
177
|
+
request.body = JSON.generate(payload)
|
|
178
|
+
|
|
179
|
+
response = http.request(request)
|
|
180
|
+
|
|
181
|
+
response_data = {}
|
|
182
|
+
body_success = true
|
|
183
|
+
begin
|
|
184
|
+
response_data = JSON.parse(response.body)
|
|
185
|
+
body_success = response_data['success'] != false
|
|
186
|
+
rescue StandardError
|
|
187
|
+
# If can't parse JSON, fall back to HTTP status check
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
is_success = response.code.to_i == 200 && body_success
|
|
191
|
+
|
|
192
|
+
if debug
|
|
193
|
+
if is_success
|
|
194
|
+
puts "[AgentBill] ✓ Signal '#{event_name}' tracked via OTEL"
|
|
195
|
+
puts "[AgentBill] Revenue: $#{'%.2f' % options[:revenue]} #{options[:currency] || 'USD'}" if options[:revenue]
|
|
196
|
+
puts "[AgentBill] Trace ID: #{generated_trace_id}"
|
|
197
|
+
else
|
|
198
|
+
puts "[AgentBill] ⚠️ Signal tracking failed: #{response_data['error'] || response.message}"
|
|
199
|
+
response_data['errors']&.each { |err| puts "[AgentBill] - #{err}" }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
{
|
|
204
|
+
success: is_success,
|
|
205
|
+
status_code: response.code.to_i,
|
|
206
|
+
trace_id: generated_trace_id,
|
|
207
|
+
span_id: generated_span_id,
|
|
208
|
+
error: is_success ? nil : response_data['error'],
|
|
209
|
+
errors: is_success ? nil : response_data['errors']
|
|
210
|
+
}
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
puts "[AgentBill] ⚠️ Signal tracking error: #{e.message}" if debug
|
|
213
|
+
{
|
|
214
|
+
success: false,
|
|
215
|
+
error: e.message,
|
|
216
|
+
trace_id: generated_trace_id
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Track a conversion event for revenue attribution.
|
|
222
|
+
#
|
|
223
|
+
# @param event_type [String] Type of conversion
|
|
224
|
+
# @param event_value [Float] Value of the conversion
|
|
225
|
+
# @param options [Hash] Additional options
|
|
226
|
+
#
|
|
227
|
+
# @return [Hash] Result with status
|
|
228
|
+
def track_conversion(event_type, event_value, **options)
|
|
229
|
+
signal(
|
|
230
|
+
"conversion_#{event_type}",
|
|
231
|
+
revenue: event_value,
|
|
232
|
+
event_type: event_type,
|
|
233
|
+
event_value: event_value,
|
|
234
|
+
**options
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Create a signal explicitly linked to an AI trace for cost attribution.
|
|
239
|
+
#
|
|
240
|
+
# This is the recommended way to link business events to AI costs.
|
|
241
|
+
# Requires trace_id from the AI call you want to attribute costs to.
|
|
242
|
+
# Sets enable_cost_tracing=true automatically.
|
|
243
|
+
#
|
|
244
|
+
# v9.4.0: Added as part of P2 Architecture Alignment (Python parity).
|
|
245
|
+
#
|
|
246
|
+
# @param event_name [String] Name of the business event
|
|
247
|
+
# @param trace_id [String] Trace ID from the AI call
|
|
248
|
+
# @param revenue [Float] Revenue amount
|
|
249
|
+
# @param options [Hash] Additional signal options
|
|
250
|
+
# @return [Hash] Result with status
|
|
251
|
+
#
|
|
252
|
+
# @example
|
|
253
|
+
# AgentBill.cost_attributed_signal('purchase', 'abc123', 99.99, metadata: { product_id: 'prod-123' })
|
|
254
|
+
def cost_attributed_signal(event_name, trace_id:, revenue:, **options)
|
|
255
|
+
config = Signals.get_config
|
|
256
|
+
prev_config = config.dup
|
|
257
|
+
Signals.set_config(config.merge(enable_cost_tracing: true))
|
|
258
|
+
|
|
259
|
+
begin
|
|
260
|
+
signal(
|
|
261
|
+
event_name,
|
|
262
|
+
trace_id: trace_id,
|
|
263
|
+
revenue: revenue,
|
|
264
|
+
**options
|
|
265
|
+
)
|
|
266
|
+
ensure
|
|
267
|
+
Signals.set_config(prev_config)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
data/lib/agentbill/tracer.rb
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
require 'net/http'
|
|
2
2
|
require 'json'
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require_relative 'distributed'
|
|
4
5
|
|
|
5
6
|
module AgentBill
|
|
6
7
|
class Span
|
|
7
|
-
attr_accessor :name, :trace_id, :span_id, :attributes, :start_time, :end_time, :status
|
|
8
|
+
attr_accessor :name, :trace_id, :span_id, :parent_span_id, :attributes, :start_time, :end_time, :status
|
|
8
9
|
|
|
9
|
-
def initialize(name, trace_id, span_id, attributes)
|
|
10
|
+
def initialize(name, trace_id, span_id, parent_span_id, attributes)
|
|
10
11
|
@name = name
|
|
11
12
|
@trace_id = trace_id
|
|
12
13
|
@span_id = span_id
|
|
14
|
+
@parent_span_id = parent_span_id
|
|
13
15
|
@attributes = attributes
|
|
14
16
|
@start_time = (Time.now.to_f * 1_000_000_000).to_i
|
|
15
17
|
@end_time = nil
|
|
@@ -30,9 +32,21 @@ module AgentBill
|
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
class Tracer
|
|
35
|
+
# OpenTelemetry tracer for AgentBill
|
|
36
|
+
#
|
|
37
|
+
# IMPORTANT: This tracer integrates with Distributed module for trace context.
|
|
38
|
+
# - If a trace context exists (via Distributed.get_trace_context), it reuses the trace_id
|
|
39
|
+
# - Always generates a new span_id for each span
|
|
40
|
+
# - Updates the global trace context so get_trace_context returns correct values
|
|
41
|
+
#
|
|
42
|
+
# This ensures that:
|
|
43
|
+
# 1. get_trace_context always returns the trace_id that matches what's sent to the portal
|
|
44
|
+
# 2. Distributed tracing works correctly across services
|
|
45
|
+
# 3. Parent-child span relationships are properly maintained
|
|
46
|
+
|
|
33
47
|
def initialize(config)
|
|
34
48
|
@config = config
|
|
35
|
-
@base_url = config[:base_url] || 'https://
|
|
49
|
+
@base_url = config[:base_url] || 'https://api.agentbill.io'
|
|
36
50
|
@api_key = config[:api_key]
|
|
37
51
|
@customer_id = config[:customer_id]
|
|
38
52
|
@debug = config[:debug] || false
|
|
@@ -40,14 +54,37 @@ module AgentBill
|
|
|
40
54
|
end
|
|
41
55
|
|
|
42
56
|
def start_span(name, attributes)
|
|
43
|
-
|
|
57
|
+
# Check for existing trace context from Distributed module
|
|
58
|
+
ctx = Distributed.get_trace_context
|
|
59
|
+
parent_span_id = nil
|
|
60
|
+
|
|
61
|
+
if ctx
|
|
62
|
+
# Reuse existing trace_id for correlation
|
|
63
|
+
trace_id = ctx[:trace_id]
|
|
64
|
+
parent_span_id = ctx[:span_id]
|
|
65
|
+
puts "[AgentBill Tracer] Using existing trace context: trace_id=#{trace_id[0..7]}..." if @debug
|
|
66
|
+
else
|
|
67
|
+
# Generate new trace_id for new trace
|
|
68
|
+
trace_id = SecureRandom.hex(16)
|
|
69
|
+
puts "[AgentBill Tracer] Creating new trace: trace_id=#{trace_id[0..7]}..." if @debug
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Always generate new span_id for this span
|
|
44
73
|
span_id = SecureRandom.hex(8)
|
|
45
74
|
|
|
75
|
+
# Update global trace context so get_trace_context returns correct values
|
|
76
|
+
Distributed.set_trace_context(trace_id, span_id)
|
|
77
|
+
|
|
46
78
|
attributes['service.name'] = 'agentbill-ruby-sdk'
|
|
47
79
|
attributes['customer.id'] = @customer_id if @customer_id
|
|
48
80
|
|
|
49
|
-
span = Span.new(name, trace_id, span_id, attributes)
|
|
81
|
+
span = Span.new(name, trace_id, span_id, parent_span_id, attributes)
|
|
50
82
|
@spans << span
|
|
83
|
+
|
|
84
|
+
if @debug
|
|
85
|
+
puts "[AgentBill Tracer] Started span: #{name}, trace_id=#{trace_id[0..7]}..., span_id=#{span_id[0..7]}..."
|
|
86
|
+
end
|
|
87
|
+
|
|
51
88
|
span
|
|
52
89
|
end
|
|
53
90
|
|
|
@@ -77,16 +114,31 @@ module AgentBill
|
|
|
77
114
|
private
|
|
78
115
|
|
|
79
116
|
def build_otlp_payload
|
|
117
|
+
# Build resource attributes - agent_id and customer_id must be sent here
|
|
118
|
+
# for the otel-collector to extract them correctly
|
|
119
|
+
resource_attrs = [
|
|
120
|
+
{ key: 'service.name', value: { stringValue: 'agentbill-ruby-sdk' } },
|
|
121
|
+
{ key: 'service.version', value: { stringValue: '6.8.8' } }
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# Add customer_id as resource attribute if configured
|
|
125
|
+
if @customer_id
|
|
126
|
+
resource_attrs << { key: 'customer.id', value: { stringValue: @customer_id } }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Add agent_id as resource attribute if configured
|
|
130
|
+
agent_id = @config[:agent_id]
|
|
131
|
+
if agent_id
|
|
132
|
+
resource_attrs << { key: 'agent.id', value: { stringValue: agent_id } }
|
|
133
|
+
end
|
|
134
|
+
|
|
80
135
|
{
|
|
81
136
|
resourceSpans: [{
|
|
82
137
|
resource: {
|
|
83
|
-
attributes:
|
|
84
|
-
{ key: 'service.name', value: { stringValue: 'agentbill-ruby-sdk' } },
|
|
85
|
-
{ key: 'service.version', value: { stringValue: '1.0.0' } }
|
|
86
|
-
]
|
|
138
|
+
attributes: resource_attrs
|
|
87
139
|
},
|
|
88
140
|
scopeSpans: [{
|
|
89
|
-
scope: { name: 'agentbill', version: '
|
|
141
|
+
scope: { name: 'agentbill', version: '6.8.8' },
|
|
90
142
|
spans: @spans.map { |span| span_to_otlp(span) }
|
|
91
143
|
}]
|
|
92
144
|
}]
|
|
@@ -94,7 +146,7 @@ module AgentBill
|
|
|
94
146
|
end
|
|
95
147
|
|
|
96
148
|
def span_to_otlp(span)
|
|
97
|
-
{
|
|
149
|
+
otlp_span = {
|
|
98
150
|
traceId: span.trace_id,
|
|
99
151
|
spanId: span.span_id,
|
|
100
152
|
name: span.name,
|
|
@@ -104,6 +156,11 @@ module AgentBill
|
|
|
104
156
|
attributes: span.attributes.map { |k, v| { key: k, value: value_to_otlp(v) } },
|
|
105
157
|
status: span.status
|
|
106
158
|
}
|
|
159
|
+
|
|
160
|
+
# Add parent span ID if this is a child span
|
|
161
|
+
otlp_span[:parentSpanId] = span.parent_span_id if span.parent_span_id
|
|
162
|
+
|
|
163
|
+
otlp_span
|
|
107
164
|
end
|
|
108
165
|
|
|
109
166
|
def value_to_otlp(value)
|