simforge 0.5.1 → 0.6.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/README.md +342 -0
- data/lib/simforge/client.rb +93 -22
- data/lib/simforge/http_client.rb +9 -0
- data/lib/simforge/span_context.rb +109 -0
- data/lib/simforge/version.rb +1 -1
- data/lib/simforge.rb +59 -2
- metadata +40 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0eaad388f5419c3933530ac93808928f219682b1e32d35fff040f691cbd4f410
|
|
4
|
+
data.tar.gz: a7922a5c2a3f063b5f6c13f43e5f6defed320c82d9b8d887a3cc12293ab03808
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32b1df24c8a1dd4aa8acb6b5ca6828736c1a3b5586982a169865acaaa753075b4e06009f6c2d0d6843e9ec29539cba0a28c4e0df83eaaeef65f31b98e1449637
|
|
7
|
+
data.tar.gz: 2abbabfde142faa7c8ca0adc6e7433787f16b6b60aaafd28d26bd23c0fc890be852bdae342312540fc97e081bea05d163af66ae1b043f393c05c1f63ddd94d44
|
data/README.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Simforge Ruby SDK
|
|
2
|
+
|
|
3
|
+
Ruby client library for [Simforge](https://simforge.goharvest.ai) - trace and monitor your Ruby application's function execution with nested span support.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your `Gemfile`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'simforge'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install simforge
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Ruby >= 3.1
|
|
22
|
+
- No external runtime dependencies (uses stdlib only)
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
require 'simforge'
|
|
28
|
+
|
|
29
|
+
# Configure once at application startup
|
|
30
|
+
Simforge.configure(
|
|
31
|
+
api_key: ENV.fetch('SIMFORGE_API_KEY')
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Add tracing to your classes
|
|
35
|
+
class OrderService
|
|
36
|
+
include Simforge::Traceable
|
|
37
|
+
simforge_function "order-processing"
|
|
38
|
+
|
|
39
|
+
simforge_span :process_order, type: "function"
|
|
40
|
+
def process_order(order_id)
|
|
41
|
+
# Your code here
|
|
42
|
+
{ status: "completed" }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Use your code normally - spans are sent automatically
|
|
47
|
+
service = OrderService.new
|
|
48
|
+
service.process_order("order-123")
|
|
49
|
+
|
|
50
|
+
# Flush traces before exit (automatic via at_exit hook)
|
|
51
|
+
Simforge.flush_traces
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
### Basic Configuration
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
Simforge.configure(
|
|
60
|
+
api_key: "your-api-key"
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Custom Service URL
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
Simforge.configure(
|
|
68
|
+
api_key: "your-api-key",
|
|
69
|
+
service_url: "https://custom.example.com"
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Disabling Span Sending
|
|
74
|
+
|
|
75
|
+
The `enabled` option controls whether spans are sent to Simforge. When disabled, your code executes normally but no spans are created or sent.
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# Disable tracing (useful for development/test environments)
|
|
79
|
+
Simforge.configure(
|
|
80
|
+
api_key: ENV.fetch('SIMFORGE_API_KEY', 'dummy-key'),
|
|
81
|
+
enabled: false
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Common patterns:**
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
# Rails: Enable only in production
|
|
89
|
+
Simforge.configure(
|
|
90
|
+
api_key: ENV.fetch('SIMFORGE_API_KEY', 'dummy-key'),
|
|
91
|
+
enabled: Rails.env.production?
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Environment variable control
|
|
95
|
+
Simforge.configure(
|
|
96
|
+
api_key: ENV.fetch('SIMFORGE_API_KEY', 'dummy-key'),
|
|
97
|
+
enabled: ENV.fetch('SIMFORGE_ENABLED', 'false') == 'true'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Multi-environment control
|
|
101
|
+
Simforge.configure(
|
|
102
|
+
api_key: ENV.fetch('SIMFORGE_API_KEY', 'dummy-key'),
|
|
103
|
+
enabled: ['production', 'staging'].include?(ENV['RACK_ENV'])
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When `enabled: false`:
|
|
108
|
+
- ✅ Code executes normally with no performance impact
|
|
109
|
+
- ✅ Return values and errors work as expected
|
|
110
|
+
- ✅ Nested spans are properly skipped
|
|
111
|
+
- ❌ No HTTP requests are made
|
|
112
|
+
- ❌ No span data is collected or sent
|
|
113
|
+
|
|
114
|
+
## Usage
|
|
115
|
+
|
|
116
|
+
### Class-Level Trace Function Key
|
|
117
|
+
|
|
118
|
+
All spans in the class share the same trace function key:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
class PaymentService
|
|
122
|
+
include Simforge::Traceable
|
|
123
|
+
simforge_function "payment-processing"
|
|
124
|
+
|
|
125
|
+
simforge_span :charge_card, type: "function"
|
|
126
|
+
def charge_card(amount)
|
|
127
|
+
# Traced automatically
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
simforge_span :refund, type: "function"
|
|
131
|
+
def refund(transaction_id)
|
|
132
|
+
# Also uses "payment-processing" key
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Per-Span Trace Function Key
|
|
138
|
+
|
|
139
|
+
Each span can declare its own key:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
class NotificationService
|
|
143
|
+
include Simforge::Traceable
|
|
144
|
+
|
|
145
|
+
simforge_span :send_email, trace_function_key: "email-notifications", type: "function"
|
|
146
|
+
def send_email(to, subject)
|
|
147
|
+
# Uses "email-notifications" key
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
simforge_span :send_sms, trace_function_key: "sms-notifications", type: "function"
|
|
151
|
+
def send_sms(to, message)
|
|
152
|
+
# Uses "sms-notifications" key
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Span Types
|
|
158
|
+
|
|
159
|
+
Simforge supports the following span types:
|
|
160
|
+
|
|
161
|
+
- `"llm"` - LLM API calls
|
|
162
|
+
- `"agent"` - Agent decision loops
|
|
163
|
+
- `"function"` - Business logic functions
|
|
164
|
+
- `"guardrail"` - Validation and safety checks
|
|
165
|
+
- `"handoff"` - Human-in-the-loop interactions
|
|
166
|
+
- `"custom"` - Custom span types (default)
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
simforge_span :validate_input, type: "guardrail"
|
|
170
|
+
simforge_span :call_openai, type: "llm"
|
|
171
|
+
simforge_span :agent_loop, type: "agent"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Custom Span Names
|
|
175
|
+
|
|
176
|
+
By default, the method name is used as the span name. Override it:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
simforge_span :process_order, name: "ProcessOrderV2", type: "function"
|
|
180
|
+
def process_order(order_id)
|
|
181
|
+
# Span will be named "ProcessOrderV2"
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Nested Spans
|
|
186
|
+
|
|
187
|
+
Spans automatically track parent-child relationships:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
class OrderPipeline
|
|
191
|
+
include Simforge::Traceable
|
|
192
|
+
simforge_function "order-pipeline"
|
|
193
|
+
|
|
194
|
+
simforge_span :process, type: "function"
|
|
195
|
+
def process(order_id)
|
|
196
|
+
validate(order_id)
|
|
197
|
+
# More processing
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
simforge_span :validate, type: "guardrail"
|
|
201
|
+
def validate(order_id)
|
|
202
|
+
check_fraud(order_id)
|
|
203
|
+
# More validation
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
simforge_span :check_fraud, type: "guardrail"
|
|
207
|
+
def check_fraud(order_id)
|
|
208
|
+
# Fraud checking logic
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Creates 3 nested spans:
|
|
213
|
+
# process (parent)
|
|
214
|
+
# └─ validate (child)
|
|
215
|
+
# └─ check_fraud (grandchild)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Input/Output Capture
|
|
219
|
+
|
|
220
|
+
Positional and keyword arguments are automatically captured:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
simforge_span :process_order, type: "function"
|
|
224
|
+
def process_order(order_id, priority: :normal)
|
|
225
|
+
# Input captured: { "order_id" => "123", "priority" => "normal" }
|
|
226
|
+
{ status: "completed" }
|
|
227
|
+
# Output captured: { "status" => "completed" }
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Metadata
|
|
232
|
+
|
|
233
|
+
Add custom metadata to spans:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
# Definition-time metadata
|
|
237
|
+
simforge_span :process_order,
|
|
238
|
+
type: "function",
|
|
239
|
+
metadata: { "region" => "us-east", "version" => "v2" }
|
|
240
|
+
def process_order(order_id)
|
|
241
|
+
# ...
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Runtime metadata (inside a span)
|
|
245
|
+
simforge_span :process_order, type: "function"
|
|
246
|
+
def process_order(order_id)
|
|
247
|
+
Simforge.current_span.add_metadata(
|
|
248
|
+
"user_id" => current_user.id,
|
|
249
|
+
"request_id" => request.id
|
|
250
|
+
)
|
|
251
|
+
# Runtime metadata merges with definition-time metadata
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Wrapping External Code
|
|
256
|
+
|
|
257
|
+
Wrap third-party library methods without modifying them:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
class ExternalHttpClient
|
|
261
|
+
def get(url)
|
|
262
|
+
# Third-party code
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Add tracing via wrap
|
|
267
|
+
Simforge::Traceable.wrap(
|
|
268
|
+
ExternalHttpClient,
|
|
269
|
+
:get,
|
|
270
|
+
trace_function_key: "http-client",
|
|
271
|
+
type: "function"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Now traced automatically
|
|
275
|
+
client = ExternalHttpClient.new
|
|
276
|
+
client.get("https://api.example.com")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Error Handling
|
|
280
|
+
|
|
281
|
+
Errors are automatically captured and re-raised:
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
simforge_span :risky_operation, type: "function"
|
|
285
|
+
def risky_operation
|
|
286
|
+
raise StandardError, "Something went wrong"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
begin
|
|
290
|
+
service.risky_operation
|
|
291
|
+
rescue StandardError => e
|
|
292
|
+
# Error is captured in span and re-raised
|
|
293
|
+
puts "Caught: #{e.message}"
|
|
294
|
+
end
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Lifecycle
|
|
298
|
+
|
|
299
|
+
### Automatic Flush
|
|
300
|
+
|
|
301
|
+
Spans are sent in background threads and automatically flushed on exit via `at_exit` hook.
|
|
302
|
+
|
|
303
|
+
### Manual Flush
|
|
304
|
+
|
|
305
|
+
Wait for all pending spans to be sent:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
Simforge.flush_traces
|
|
309
|
+
# Blocks until all background threads complete
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Reset Client
|
|
313
|
+
|
|
314
|
+
Clear the global client (useful for testing):
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
Simforge.reset!
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Thread Safety
|
|
321
|
+
|
|
322
|
+
- Each thread has its own span context stack (using `Thread.current`)
|
|
323
|
+
- Nested spans only work within the same thread
|
|
324
|
+
- Background span sending is thread-safe
|
|
325
|
+
|
|
326
|
+
## Examples
|
|
327
|
+
|
|
328
|
+
See [simforge-ruby-example/](../simforge-ruby-example/) for complete working examples:
|
|
329
|
+
|
|
330
|
+
- [test_span.rb](../simforge-ruby-example/scripts/test_span.rb) - Basic span creation
|
|
331
|
+
- [test_nested_spans.rb](../simforge-ruby-example/scripts/test_nested_spans.rb) - Nested span hierarchies
|
|
332
|
+
- [test_wrap.rb](../simforge-ruby-example/scripts/test_wrap.rb) - Wrapping external code
|
|
333
|
+
- [test_enabled.rb](../simforge-ruby-example/scripts/test_enabled.rb) - Using the enabled flag
|
|
334
|
+
- [test_metadata.rb](../simforge-ruby-example/scripts/test_metadata.rb) - Custom metadata
|
|
335
|
+
|
|
336
|
+
## Development
|
|
337
|
+
|
|
338
|
+
See [DEVELOPMENT.md](DEVELOPMENT.md) for development setup, testing, and publishing instructions.
|
|
339
|
+
|
|
340
|
+
## License
|
|
341
|
+
|
|
342
|
+
MIT
|
data/lib/simforge/client.rb
CHANGED
|
@@ -12,51 +12,86 @@ module Simforge
|
|
|
12
12
|
class Client
|
|
13
13
|
SPAN_TYPES = %w[llm agent function guardrail handoff custom].freeze
|
|
14
14
|
|
|
15
|
-
attr_reader :api_key, :service_url
|
|
15
|
+
attr_reader :api_key, :service_url, :enabled
|
|
16
16
|
|
|
17
|
-
def initialize(api_key:, service_url: nil)
|
|
17
|
+
def initialize(api_key:, service_url: nil, enabled: true)
|
|
18
18
|
@api_key = api_key
|
|
19
19
|
@service_url = service_url || DEFAULT_SERVICE_URL
|
|
20
|
+
@enabled = enabled
|
|
21
|
+
if @enabled && (@api_key.nil? || @api_key.to_s.strip.empty?)
|
|
22
|
+
warn "Simforge: api_key is empty — tracing is disabled. Provide a valid API key to enable tracing."
|
|
23
|
+
@enabled = false
|
|
24
|
+
end
|
|
20
25
|
@http_client = HttpClient.new(api_key:, service_url: @service_url)
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
# Execute a block inside a span context, sending trace data on completion.
|
|
24
29
|
# Called by Traceable — not intended for direct use.
|
|
25
30
|
def execute_span(trace_function_key:, span_name:, span_type:, function_name:, args:, kwargs:)
|
|
26
|
-
|
|
31
|
+
return yield unless @enabled
|
|
27
32
|
|
|
28
33
|
parent = SpanContext.current
|
|
29
34
|
trace_id = parent ? parent[:trace_id] : SecureRandom.uuid
|
|
30
35
|
span_id = SecureRandom.uuid
|
|
31
36
|
parent_span_id = parent&.dig(:span_id)
|
|
37
|
+
is_root_span = parent_span_id.nil?
|
|
32
38
|
started_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
33
39
|
|
|
40
|
+
# Register trace state for root spans
|
|
41
|
+
if is_root_span && !TraceState.get(trace_id)
|
|
42
|
+
TraceState.create(trace_id)
|
|
43
|
+
end
|
|
44
|
+
|
|
34
45
|
result = nil
|
|
35
46
|
error = nil
|
|
47
|
+
span_contexts = nil
|
|
36
48
|
|
|
37
49
|
begin
|
|
38
|
-
|
|
50
|
+
SpanContext.with_span(trace_id:, span_id:) do
|
|
51
|
+
result = yield
|
|
52
|
+
ensure
|
|
53
|
+
# Capture contexts before the span context is popped
|
|
54
|
+
span_contexts = SpanContext.current&.dig(:contexts)
|
|
55
|
+
end
|
|
39
56
|
rescue => e
|
|
40
57
|
error = e.message
|
|
41
58
|
raise
|
|
42
59
|
ensure
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
# Never crash the host app due to span building/sending
|
|
61
|
+
begin
|
|
62
|
+
ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
63
|
+
|
|
64
|
+
send_span(
|
|
65
|
+
trace_function_key:,
|
|
66
|
+
trace_id:,
|
|
67
|
+
span_id:,
|
|
68
|
+
parent_span_id:,
|
|
69
|
+
span_name:,
|
|
70
|
+
span_type:,
|
|
71
|
+
function_name:,
|
|
72
|
+
contexts: span_contexts,
|
|
73
|
+
args:,
|
|
74
|
+
kwargs:,
|
|
75
|
+
result:,
|
|
76
|
+
error:,
|
|
77
|
+
started_at:,
|
|
78
|
+
ended_at:
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# For root spans, also send trace completion
|
|
82
|
+
if is_root_span
|
|
83
|
+
send_trace_completion(
|
|
84
|
+
trace_function_key:,
|
|
85
|
+
trace_id:,
|
|
86
|
+
started_at:,
|
|
87
|
+
ended_at:
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
91
|
+
# Silently ignore — user's result/exception takes priority
|
|
92
|
+
# Catches Exception (not just StandardError) to handle SystemStackError
|
|
93
|
+
# from deeply nested serialization
|
|
94
|
+
end
|
|
60
95
|
end
|
|
61
96
|
|
|
62
97
|
result
|
|
@@ -70,8 +105,43 @@ module Simforge
|
|
|
70
105
|
raise ArgumentError, "Invalid span type '#{type}'. Must be one of: #{SPAN_TYPES.join(", ")}"
|
|
71
106
|
end
|
|
72
107
|
|
|
108
|
+
def send_trace_completion(trace_function_key:, trace_id:, started_at:, ended_at:)
|
|
109
|
+
trace_state = TraceState.get(trace_id)
|
|
110
|
+
trace_started_at = trace_state&.dig(:started_at) || started_at
|
|
111
|
+
|
|
112
|
+
raw_trace = {
|
|
113
|
+
"id" => trace_id,
|
|
114
|
+
"started_at" => trace_started_at,
|
|
115
|
+
"ended_at" => ended_at
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if trace_state&.dig(:metadata)
|
|
119
|
+
raw_trace["metadata"] = trace_state[:metadata]
|
|
120
|
+
end
|
|
121
|
+
if trace_state&.dig(:contexts)
|
|
122
|
+
raw_trace["contexts"] = trace_state[:contexts]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
payload = {
|
|
126
|
+
"type" => "sdk-function",
|
|
127
|
+
"source" => "ruby-sdk-function",
|
|
128
|
+
"traceFunctionKey" => trace_function_key,
|
|
129
|
+
"externalTrace" => raw_trace,
|
|
130
|
+
"completed" => true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if trace_state&.dig(:session_id)
|
|
134
|
+
payload["sessionId"] = trace_state[:session_id]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
@http_client.send_external_trace(payload)
|
|
138
|
+
|
|
139
|
+
# Clean up trace state
|
|
140
|
+
TraceState.delete(trace_id)
|
|
141
|
+
end
|
|
142
|
+
|
|
73
143
|
def send_span(trace_function_key:, trace_id:, span_id:, parent_span_id:,
|
|
74
|
-
span_name:, span_type:, function_name:, args:, kwargs:, result:, error:,
|
|
144
|
+
span_name:, span_type:, function_name:, contexts:, args:, kwargs:, result:, error:,
|
|
75
145
|
started_at:, ended_at:)
|
|
76
146
|
# Human-readable JSON (input/output fields)
|
|
77
147
|
human_inputs = Serialize.serialize_inputs(args, kwargs)
|
|
@@ -92,6 +162,7 @@ module Simforge
|
|
|
92
162
|
span_data["input_serialized"] = marshalled_input if marshalled_input
|
|
93
163
|
span_data["output_serialized"] = marshalled_output if marshalled_output
|
|
94
164
|
span_data["error"] = error if error
|
|
165
|
+
span_data["contexts"] = contexts if contexts&.any?
|
|
95
166
|
|
|
96
167
|
raw_span = {
|
|
97
168
|
"id" => span_id,
|
data/lib/simforge/http_client.rb
CHANGED
|
@@ -66,6 +66,15 @@ module Simforge
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# Send an external trace (fire-and-forget in background thread).
|
|
70
|
+
def send_external_trace(payload)
|
|
71
|
+
merged = payload.merge("sdkVersion" => VERSION)
|
|
72
|
+
|
|
73
|
+
Simforge._run_in_background do
|
|
74
|
+
request("/api/sdk/externalTraces", merged, timeout: 10)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
private
|
|
70
79
|
|
|
71
80
|
def headers
|
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Simforge
|
|
4
|
+
# Handle to the current active span, allowing context to be added.
|
|
5
|
+
class CurrentSpan
|
|
6
|
+
def initialize(span_state)
|
|
7
|
+
@span_state = span_state
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# The trace ID for the current span.
|
|
11
|
+
def trace_id
|
|
12
|
+
@span_state[:trace_id]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Add a context entry to this span.
|
|
16
|
+
# The entire hash is pushed as a single entry in the contexts array.
|
|
17
|
+
# Context entries are accumulated - multiple calls add to the list.
|
|
18
|
+
#
|
|
19
|
+
# @param context [Hash] key-value pairs to add as a single context entry
|
|
20
|
+
def add_context(context)
|
|
21
|
+
return unless context.is_a?(Hash)
|
|
22
|
+
|
|
23
|
+
@span_state[:contexts] ||= []
|
|
24
|
+
@span_state[:contexts] << context
|
|
25
|
+
rescue
|
|
26
|
+
# Silently ignore - never crash the host app
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Handle to the current active trace, allowing trace-level context to be set.
|
|
31
|
+
class CurrentTrace
|
|
32
|
+
def initialize(trace_id)
|
|
33
|
+
@trace_id = trace_id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Set the session ID for this trace.
|
|
37
|
+
# Session ID is used to group traces from the same user session.
|
|
38
|
+
# This is stored as a database column.
|
|
39
|
+
#
|
|
40
|
+
# @param session_id [String] the session ID to set
|
|
41
|
+
def set_session_id(session_id)
|
|
42
|
+
trace_state = get_or_create_trace_state
|
|
43
|
+
trace_state[:session_id] = session_id
|
|
44
|
+
rescue
|
|
45
|
+
# Silently ignore - never crash the host app
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set metadata for this trace.
|
|
49
|
+
# Metadata is stored in the raw trace data. Subsequent calls merge with
|
|
50
|
+
# existing metadata, with later values taking precedence.
|
|
51
|
+
#
|
|
52
|
+
# @param metadata [Hash] key-value pairs to store as trace metadata
|
|
53
|
+
def set_metadata(metadata)
|
|
54
|
+
return unless metadata.is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
trace_state = get_or_create_trace_state
|
|
57
|
+
trace_state[:metadata] = (trace_state[:metadata] || {}).merge(metadata)
|
|
58
|
+
rescue
|
|
59
|
+
# Silently ignore - never crash the host app
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Add a context entry to this trace.
|
|
63
|
+
# The entire hash is pushed as a single entry in the contexts array.
|
|
64
|
+
# Context entries are accumulated - multiple calls add to the list.
|
|
65
|
+
#
|
|
66
|
+
# @param context [Hash] key-value pairs to add as a single context entry
|
|
67
|
+
def add_context(context)
|
|
68
|
+
return unless context.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
trace_state = get_or_create_trace_state
|
|
71
|
+
trace_state[:contexts] ||= []
|
|
72
|
+
trace_state[:contexts] << context
|
|
73
|
+
rescue
|
|
74
|
+
# Silently ignore - never crash the host app
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def get_or_create_trace_state
|
|
80
|
+
TraceState.get(@trace_id) || TraceState.create(@trace_id)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
4
84
|
# Thread-local span stack for tracking nested spans.
|
|
5
85
|
# Each entry is a Hash with :trace_id and :span_id keys.
|
|
6
86
|
module SpanContext
|
|
@@ -26,4 +106,33 @@ module Simforge
|
|
|
26
106
|
stack.pop
|
|
27
107
|
end
|
|
28
108
|
end
|
|
109
|
+
|
|
110
|
+
# Global storage for trace states (trace_id -> state hash)
|
|
111
|
+
module TraceState
|
|
112
|
+
@states_mutex = Mutex.new
|
|
113
|
+
@states = {}
|
|
114
|
+
|
|
115
|
+
module_function
|
|
116
|
+
|
|
117
|
+
def get(trace_id)
|
|
118
|
+
@states_mutex.synchronize { @states[trace_id] }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def create(trace_id)
|
|
122
|
+
@states_mutex.synchronize do
|
|
123
|
+
@states[trace_id] ||= {
|
|
124
|
+
trace_id:,
|
|
125
|
+
started_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def delete(trace_id)
|
|
131
|
+
@states_mutex.synchronize { @states.delete(trace_id) }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def clear_all
|
|
135
|
+
@states_mutex.synchronize { @states.clear }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
29
138
|
end
|
data/lib/simforge/version.rb
CHANGED
data/lib/simforge.rb
CHANGED
|
@@ -9,6 +9,37 @@ require_relative "simforge/client"
|
|
|
9
9
|
require_relative "simforge/traceable"
|
|
10
10
|
|
|
11
11
|
module Simforge
|
|
12
|
+
# No-op span handle returned when outside a span context.
|
|
13
|
+
# All methods do nothing, preventing crashes when called outside traced code.
|
|
14
|
+
class NoOpCurrentSpan
|
|
15
|
+
def trace_id
|
|
16
|
+
""
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add_context(_context)
|
|
20
|
+
# No-op
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# No-op trace handle returned when outside a span context.
|
|
25
|
+
# All methods do nothing, preventing crashes when called outside traced code.
|
|
26
|
+
class NoOpCurrentTrace
|
|
27
|
+
def set_session_id(_session_id)
|
|
28
|
+
# No-op
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set_metadata(_metadata)
|
|
32
|
+
# No-op
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_context(_context)
|
|
36
|
+
# No-op
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
NO_OP_SPAN = NoOpCurrentSpan.new.freeze
|
|
41
|
+
NO_OP_TRACE = NoOpCurrentTrace.new.freeze
|
|
42
|
+
|
|
12
43
|
class << self
|
|
13
44
|
# Configure the global Simforge client.
|
|
14
45
|
#
|
|
@@ -18,8 +49,8 @@ module Simforge
|
|
|
18
49
|
# @example
|
|
19
50
|
# Simforge.configure(api_key: ENV["SIMFORGE_API_KEY"])
|
|
20
51
|
#
|
|
21
|
-
def configure(api_key:, service_url: nil)
|
|
22
|
-
@client = Client.new(api_key:, service_url:)
|
|
52
|
+
def configure(api_key:, service_url: nil, enabled: true)
|
|
53
|
+
@client = Client.new(api_key:, service_url:, enabled:)
|
|
23
54
|
end
|
|
24
55
|
|
|
25
56
|
# Returns the global client, raising if not configured.
|
|
@@ -31,5 +62,31 @@ module Simforge
|
|
|
31
62
|
def reset!
|
|
32
63
|
@client = nil
|
|
33
64
|
end
|
|
65
|
+
|
|
66
|
+
# Get a handle to the current active span.
|
|
67
|
+
#
|
|
68
|
+
# Call this from inside a traced method to get a span handle that allows
|
|
69
|
+
# setting metadata at runtime.
|
|
70
|
+
#
|
|
71
|
+
# @return [CurrentSpan, NoOpCurrentSpan] the current span, or a no-op if outside a span context
|
|
72
|
+
def current_span
|
|
73
|
+
entry = SpanContext.current
|
|
74
|
+
return NO_OP_SPAN unless entry
|
|
75
|
+
|
|
76
|
+
CurrentSpan.new(entry)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get a handle to the current active trace.
|
|
80
|
+
#
|
|
81
|
+
# Call this from inside a traced method to get a trace handle that allows
|
|
82
|
+
# setting trace-level context at runtime.
|
|
83
|
+
#
|
|
84
|
+
# @return [CurrentTrace, NoOpCurrentTrace] the current trace, or a no-op if outside a span context
|
|
85
|
+
def current_trace
|
|
86
|
+
entry = SpanContext.current
|
|
87
|
+
return NO_OP_TRACE unless entry
|
|
88
|
+
|
|
89
|
+
CurrentTrace.new(entry[:trace_id])
|
|
90
|
+
end
|
|
34
91
|
end
|
|
35
92
|
end
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: simforge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: bin
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-13 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rake
|
|
@@ -29,40 +30,68 @@ dependencies:
|
|
|
29
30
|
requirements:
|
|
30
31
|
- - "~>"
|
|
31
32
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '3.
|
|
33
|
+
version: '3.13'
|
|
33
34
|
type: :development
|
|
34
35
|
prerelease: false
|
|
35
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
37
|
requirements:
|
|
37
38
|
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.13'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rubocop
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rubocop-standard
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
38
67
|
- !ruby/object:Gem::Version
|
|
39
68
|
version: '3.0'
|
|
40
69
|
- !ruby/object:Gem::Dependency
|
|
41
70
|
name: standard
|
|
42
71
|
requirement: !ruby/object:Gem::Requirement
|
|
43
72
|
requirements:
|
|
44
|
-
- - "
|
|
73
|
+
- - ">="
|
|
45
74
|
- !ruby/object:Gem::Version
|
|
46
75
|
version: '1.0'
|
|
47
76
|
type: :development
|
|
48
77
|
prerelease: false
|
|
49
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
79
|
requirements:
|
|
51
|
-
- - "
|
|
80
|
+
- - ">="
|
|
52
81
|
- !ruby/object:Gem::Version
|
|
53
82
|
version: '1.0'
|
|
54
83
|
- !ruby/object:Gem::Dependency
|
|
55
84
|
name: webmock
|
|
56
85
|
requirement: !ruby/object:Gem::Requirement
|
|
57
86
|
requirements:
|
|
58
|
-
- - "
|
|
87
|
+
- - ">="
|
|
59
88
|
- !ruby/object:Gem::Version
|
|
60
89
|
version: '3.0'
|
|
61
90
|
type: :development
|
|
62
91
|
prerelease: false
|
|
63
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
93
|
requirements:
|
|
65
|
-
- - "
|
|
94
|
+
- - ">="
|
|
66
95
|
- !ruby/object:Gem::Version
|
|
67
96
|
version: '3.0'
|
|
68
97
|
description: Client library for sending function execution spans to the Simforge API.
|
|
@@ -73,6 +102,7 @@ executables: []
|
|
|
73
102
|
extensions: []
|
|
74
103
|
extra_rdoc_files: []
|
|
75
104
|
files:
|
|
105
|
+
- README.md
|
|
76
106
|
- lib/simforge.rb
|
|
77
107
|
- lib/simforge/client.rb
|
|
78
108
|
- lib/simforge/constants.rb
|
|
@@ -88,6 +118,7 @@ metadata:
|
|
|
88
118
|
homepage_uri: https://simforge.goharvest.ai
|
|
89
119
|
source_code_uri: https://simforge.goharvest.ai
|
|
90
120
|
rubygems_mfa_required: 'true'
|
|
121
|
+
post_install_message:
|
|
91
122
|
rdoc_options: []
|
|
92
123
|
require_paths:
|
|
93
124
|
- lib
|
|
@@ -102,7 +133,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
102
133
|
- !ruby/object:Gem::Version
|
|
103
134
|
version: '0'
|
|
104
135
|
requirements: []
|
|
105
|
-
rubygems_version: 3.
|
|
136
|
+
rubygems_version: 3.3.26
|
|
137
|
+
signing_key:
|
|
106
138
|
specification_version: 4
|
|
107
139
|
summary: Simforge Ruby SDK for function tracing and span management
|
|
108
140
|
test_files: []
|