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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 712b471d28942f55c0ac0a78d05507f1060396de60c584173d3824f1e8c6d9b7
4
- data.tar.gz: efa9dfc4e4173382d853327ee4edeafa54114482e04d97c86a3933465ee24285
3
+ metadata.gz: 0eaad388f5419c3933530ac93808928f219682b1e32d35fff040f691cbd4f410
4
+ data.tar.gz: a7922a5c2a3f063b5f6c13f43e5f6defed320c82d9b8d887a3cc12293ab03808
5
5
  SHA512:
6
- metadata.gz: 1ca7cb706679154120fc89cac714b7d2eaa45017389f33f726afee7a77a0a0d1203dd7e14da32c8aa042d828381654bb87fe42fcb75c276edf61c52a27ba83e8
7
- data.tar.gz: b9d9f5e70bf640447f046f9e272054c5f27f2aba849f42df20c6424c7a11c923c3403f89b23d0eb6b9eb50d53c39d0292b99213a5c4e2b745c7580948613a7d9
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
@@ -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
- validate_span_type!(span_type)
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
- result = SpanContext.with_span(trace_id:, span_id:) { yield }
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
- ended_at = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
44
-
45
- send_span(
46
- trace_function_key:,
47
- trace_id:,
48
- span_id:,
49
- parent_span_id:,
50
- span_name:,
51
- span_type:,
52
- function_name:,
53
- args:,
54
- kwargs:,
55
- result:,
56
- error:,
57
- started_at:,
58
- ended_at:
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,
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Simforge
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
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.5.1
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-01-29 00:00:00.000000000 Z
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.0'
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.6.6
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: []