dspy 0.16.0 → 0.18.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.
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module DSPy
6
+ class Observability
7
+ class << self
8
+ attr_reader :enabled, :tracer, :endpoint
9
+
10
+ def configure!
11
+ @enabled = false
12
+
13
+ # Check for required Langfuse environment variables
14
+ public_key = ENV['LANGFUSE_PUBLIC_KEY']
15
+ secret_key = ENV['LANGFUSE_SECRET_KEY']
16
+
17
+ unless public_key && secret_key
18
+ return
19
+ end
20
+
21
+ # Determine endpoint based on host
22
+ host = ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
23
+ @endpoint = "#{host}/api/public/otel"
24
+
25
+ begin
26
+ # Load OpenTelemetry gems
27
+ require 'opentelemetry/sdk'
28
+ require 'opentelemetry/exporter/otlp'
29
+
30
+ # Generate Basic Auth header
31
+ auth_string = Base64.strict_encode64("#{public_key}:#{secret_key}")
32
+
33
+ # Configure OpenTelemetry SDK
34
+ OpenTelemetry::SDK.configure do |config|
35
+ config.service_name = 'dspy-ruby'
36
+ config.service_version = DSPy::VERSION
37
+
38
+ # Add OTLP exporter for Langfuse
39
+ config.add_span_processor(
40
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
41
+ OpenTelemetry::Exporter::OTLP::Exporter.new(
42
+ endpoint: @endpoint,
43
+ headers: {
44
+ 'Authorization' => "Basic #{auth_string}",
45
+ 'Content-Type' => 'application/x-protobuf'
46
+ },
47
+ compression: 'gzip'
48
+ )
49
+ )
50
+ )
51
+
52
+ # Add resource attributes
53
+ config.resource = OpenTelemetry::SDK::Resources::Resource.create({
54
+ 'service.name' => 'dspy-ruby',
55
+ 'service.version' => DSPy::VERSION,
56
+ 'telemetry.sdk.name' => 'opentelemetry',
57
+ 'telemetry.sdk.language' => 'ruby'
58
+ })
59
+ end
60
+
61
+ # Create tracer
62
+ @tracer = OpenTelemetry.tracer_provider.tracer('dspy', DSPy::VERSION)
63
+ @enabled = true
64
+
65
+ rescue LoadError => e
66
+ DSPy.log('observability.disabled', reason: 'OpenTelemetry gems not available')
67
+ rescue StandardError => e
68
+ DSPy.log('observability.error', error: e.message, class: e.class.name)
69
+ end
70
+ end
71
+
72
+ def enabled?
73
+ @enabled == true
74
+ end
75
+
76
+ def start_span(operation_name, attributes = {})
77
+ return nil unless enabled? && tracer
78
+
79
+ # Convert attribute keys to strings and filter out nil values
80
+ string_attributes = attributes.transform_keys(&:to_s)
81
+ .reject { |k, v| v.nil? }
82
+ string_attributes['operation.name'] = operation_name
83
+
84
+ tracer.start_span(
85
+ operation_name,
86
+ kind: :internal,
87
+ attributes: string_attributes
88
+ )
89
+ rescue StandardError => e
90
+ DSPy.log('observability.span_error', error: e.message, operation: operation_name)
91
+ nil
92
+ end
93
+
94
+ def finish_span(span)
95
+ return unless span
96
+
97
+ span.finish
98
+ rescue StandardError => e
99
+ DSPy.log('observability.span_finish_error', error: e.message)
100
+ end
101
+
102
+ def reset!
103
+ @enabled = false
104
+ @tracer = nil
105
+ @endpoint = nil
106
+ end
107
+ end
108
+ end
109
+ end
data/lib/dspy/predict.rb CHANGED
@@ -2,11 +2,9 @@
2
2
 
3
3
  require 'sorbet-runtime'
4
4
  require_relative 'module'
5
- require_relative 'instrumentation'
6
5
  require_relative 'prompt'
7
6
  require_relative 'mixins/struct_builder'
8
7
  require_relative 'mixins/type_coercion'
9
- require_relative 'mixins/instrumentation_helpers'
10
8
 
11
9
  module DSPy
12
10
  # Exception raised when prediction fails validation
@@ -27,7 +25,6 @@ module DSPy
27
25
  extend T::Sig
28
26
  include Mixins::StructBuilder
29
27
  include Mixins::TypeCoercion
30
- include Mixins::InstrumentationHelpers
31
28
 
32
29
  sig { returns(T.class_of(Signature)) }
33
30
  attr_reader :signature_class
@@ -85,12 +82,23 @@ module DSPy
85
82
 
86
83
  sig { params(input_values: T.untyped).returns(T.untyped) }
87
84
  def forward_untyped(**input_values)
88
- instrument_prediction('dspy.predict', @signature_class, input_values) do
85
+ # Wrap prediction in span tracking
86
+ DSPy::Context.with_span(
87
+ operation: "#{self.class.name}.forward",
88
+ 'dspy.module' => self.class.name,
89
+ 'dspy.signature' => @signature_class.name
90
+ ) do
89
91
  # Validate input
90
92
  validate_input_struct(input_values)
91
93
 
94
+ # Check if LM is configured
95
+ current_lm = lm
96
+ if current_lm.nil?
97
+ raise DSPy::ConfigurationError.missing_lm(self.class.name)
98
+ end
99
+
92
100
  # Call LM and process response
93
- output_attributes = lm.chat(self, input_values)
101
+ output_attributes = current_lm.chat(self, input_values)
94
102
  processed_output = process_lm_output(output_attributes)
95
103
 
96
104
  # Create combined result struct
@@ -105,7 +113,11 @@ module DSPy
105
113
  def validate_input_struct(input_values)
106
114
  @signature_class.input_struct_class.new(**input_values)
107
115
  rescue ArgumentError => e
108
- emit_validation_error(@signature_class, 'input', e.message)
116
+ DSPy.log('prediction.validation_error', **{
117
+ 'dspy.signature' => @signature_class.name,
118
+ 'prediction.validation_type' => 'input',
119
+ 'prediction.validation_errors' => { input: e.message }
120
+ })
109
121
  raise PredictionInvalidError.new({ input: e.message })
110
122
  end
111
123
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'sorbet-runtime'
4
- require_relative '../instrumentation'
5
4
  require_relative '../signature'
6
5
  require_relative '../predict'
7
6
 
@@ -104,12 +103,14 @@ module DSPy
104
103
  ).returns(ProposalResult)
105
104
  end
106
105
  def propose_instructions(signature_class, examples, few_shot_examples: nil, current_instruction: nil)
107
- Instrumentation.instrument('dspy.optimization.instruction_proposal_start', {
108
- signature_class: signature_class.name,
109
- num_examples: examples.size,
110
- has_few_shot: !few_shot_examples.nil?,
111
- has_current_instruction: !current_instruction.nil?
112
- }) do
106
+ DSPy::Context.with_span(
107
+ operation: 'optimization.instruction_proposal',
108
+ 'dspy.module' => 'GroundedProposer',
109
+ 'proposal.signature' => signature_class.name,
110
+ 'proposal.num_examples' => examples.size,
111
+ 'proposal.has_few_shot' => !few_shot_examples.nil?,
112
+ 'proposal.has_current_instruction' => !current_instruction.nil?
113
+ ) do
113
114
  # Analyze the task and training data
114
115
  analysis = analyze_task(signature_class, examples, few_shot_examples)
115
116
 
@@ -548,11 +549,11 @@ module DSPy
548
549
  # Emit instruction proposal completion event
549
550
  sig { params(result: ProposalResult).void }
550
551
  def emit_proposal_complete_event(result)
551
- Instrumentation.emit('dspy.optimization.instruction_proposal_complete', {
552
- num_candidates: result.num_candidates,
553
- best_instruction_length: result.best_instruction.length,
554
- analysis_themes: result.analysis[:common_themes] || [],
555
- model_used: @config.proposal_model
552
+ DSPy.log('optimization.instruction_proposal_complete', **{
553
+ 'proposal.num_candidates' => result.num_candidates,
554
+ 'proposal.best_instruction_length' => result.best_instruction.length,
555
+ 'proposal.analysis_themes' => result.analysis[:common_themes] || [],
556
+ 'proposal.model_used' => @config.proposal_model
556
557
  })
557
558
  end
558
559
  end
data/lib/dspy/re_act.rb CHANGED
@@ -6,9 +6,7 @@ require_relative 'predict'
6
6
  require_relative 'signature'
7
7
  require_relative 'chain_of_thought'
8
8
  require 'json'
9
- require_relative 'instrumentation'
10
9
  require_relative 'mixins/struct_builder'
11
- require_relative 'mixins/instrumentation_helpers'
12
10
 
13
11
  module DSPy
14
12
  # Define a simple struct for history entries with proper type annotations
@@ -67,7 +65,6 @@ module DSPy
67
65
  class ReAct < Predict
68
66
  extend T::Sig
69
67
  include Mixins::StructBuilder
70
- include Mixins::InstrumentationHelpers
71
68
 
72
69
  FINISH_ACTION = "finish"
73
70
  sig { returns(T.class_of(DSPy::Signature)) }
@@ -129,22 +126,14 @@ module DSPy
129
126
  lm = config.lm || DSPy.config.lm
130
127
  available_tools = @tools.keys
131
128
 
132
- # Instrument the entire ReAct agent lifecycle
133
- result = instrument_prediction('dspy.react', @original_signature_class, kwargs, {
134
- max_iterations: @max_iterations,
135
- available_tools: available_tools
136
- }) do
137
- # Validate input
138
- input_struct = @original_signature_class.input_struct_class.new(**kwargs)
129
+ # Validate input
130
+ input_struct = @original_signature_class.input_struct_class.new(**kwargs)
139
131
 
140
- # Execute ReAct reasoning loop
141
- reasoning_result = execute_react_reasoning_loop(input_struct)
132
+ # Execute ReAct reasoning loop
133
+ reasoning_result = execute_react_reasoning_loop(input_struct)
142
134
 
143
- # Create enhanced output with all ReAct data
144
- create_enhanced_result(kwargs, reasoning_result)
145
- end
146
-
147
- result
135
+ # Create enhanced output with all ReAct data
136
+ create_enhanced_result(kwargs, reasoning_result)
148
137
  end
149
138
 
150
139
  private
@@ -247,13 +236,15 @@ module DSPy
247
236
  # Executes a single iteration of the ReAct loop
248
237
  sig { params(input_struct: T.untyped, history: T::Array[HistoryEntry], available_tools_desc: T::Array[T::Hash[String, T.untyped]], iteration: Integer, tools_used: T::Array[String], last_observation: T.nilable(String)).returns(T::Hash[Symbol, T.untyped]) }
249
238
  def execute_single_iteration(input_struct, history, available_tools_desc, iteration, tools_used, last_observation)
250
- # Instrument each iteration
251
- Instrumentation.instrument('dspy.react.iteration', {
252
- iteration: iteration,
253
- max_iterations: @max_iterations,
254
- history_length: history.length,
255
- tools_used_so_far: tools_used.uniq
256
- }) do
239
+ # Track each iteration with span
240
+ DSPy::Context.with_span(
241
+ operation: 'react.iteration',
242
+ 'dspy.module' => 'ReAct',
243
+ 'react.iteration' => iteration,
244
+ 'react.max_iterations' => @max_iterations,
245
+ 'react.history_length' => history.length,
246
+ 'react.tools_used' => tools_used.uniq
247
+ ) do
257
248
  # Generate thought and action
258
249
  thought_obj = @thought_generator.forward(
259
250
  input_context: DSPy::TypeSerializer.serialize(input_struct).to_json,
@@ -357,11 +348,13 @@ module DSPy
357
348
  sig { params(action: T.nilable(String), action_input: T.untyped, iteration: Integer).returns(String) }
358
349
  def execute_tool_with_instrumentation(action, action_input, iteration)
359
350
  if action && @tools[action.downcase]
360
- Instrumentation.instrument('dspy.react.tool_call', {
361
- iteration: iteration,
362
- tool_name: action.downcase,
363
- tool_input: action_input
364
- }) do
351
+ DSPy::Context.with_span(
352
+ operation: 'react.tool_call',
353
+ 'dspy.module' => 'ReAct',
354
+ 'react.iteration' => iteration,
355
+ 'tool.name' => action.downcase,
356
+ 'tool.input' => action_input
357
+ ) do
365
358
  execute_action(action, action_input)
366
359
  end
367
360
  else
@@ -421,24 +414,24 @@ module DSPy
421
414
 
422
415
  sig { params(iteration: Integer, thought: String, action: String, action_input: T.untyped, observation: String, tools_used: T::Array[String]).void }
423
416
  def emit_iteration_complete_event(iteration, thought, action, action_input, observation, tools_used)
424
- Instrumentation.emit('dspy.react.iteration_complete', {
425
- iteration: iteration,
426
- thought: thought,
427
- action: action,
428
- action_input: action_input,
429
- observation: observation,
430
- tools_used: tools_used.uniq
417
+ DSPy.log('react.iteration_complete', **{
418
+ 'react.iteration' => iteration,
419
+ 'react.thought' => thought,
420
+ 'react.action' => action,
421
+ 'react.action_input' => action_input,
422
+ 'react.observation' => observation,
423
+ 'react.tools_used' => tools_used.uniq
431
424
  })
432
425
  end
433
426
 
434
427
  sig { params(iterations_count: Integer, final_answer: T.nilable(String), tools_used: T::Array[String], history: T::Array[HistoryEntry]).void }
435
428
  def handle_max_iterations_if_needed(iterations_count, final_answer, tools_used, history)
436
429
  if iterations_count >= @max_iterations && final_answer.nil?
437
- Instrumentation.emit('dspy.react.max_iterations', {
438
- iteration_count: iterations_count,
439
- max_iterations: @max_iterations,
440
- tools_used: tools_used.uniq,
441
- final_history_length: history.length
430
+ DSPy.log('react.max_iterations', **{
431
+ 'react.iteration_count' => iterations_count,
432
+ 'react.max_iterations' => @max_iterations,
433
+ 'react.tools_used' => tools_used.uniq,
434
+ 'react.final_history_length' => history.length
442
435
  })
443
436
  end
444
437
  end
@@ -482,21 +482,19 @@ module DSPy
482
482
 
483
483
  sig { params(signature_name: String, version: String).void }
484
484
  def emit_auto_deployment_event(signature_name, version)
485
- DSPy::Instrumentation.emit('dspy.registry.auto_deployment', {
486
- signature_name: signature_name,
487
- version: version,
488
- timestamp: Time.now.iso8601
485
+ DSPy.log('registry.auto_deployment', **{
486
+ 'registry.signature_name' => signature_name,
487
+ 'registry.version' => version
489
488
  })
490
489
  end
491
490
 
492
491
  sig { params(signature_name: String, current_score: Float, previous_score: Float).void }
493
492
  def emit_automatic_rollback_event(signature_name, current_score, previous_score)
494
- DSPy::Instrumentation.emit('dspy.registry.automatic_rollback', {
495
- signature_name: signature_name,
496
- current_score: current_score,
497
- previous_score: previous_score,
498
- performance_drop: previous_score - current_score,
499
- timestamp: Time.now.iso8601
493
+ DSPy.log('registry.automatic_rollback', **{
494
+ 'registry.signature_name' => signature_name,
495
+ 'registry.current_score' => current_score,
496
+ 'registry.previous_score' => previous_score,
497
+ 'registry.performance_drop' => previous_score - current_score
500
498
  })
501
499
  end
502
500
  end
@@ -611,113 +611,101 @@ module DSPy
611
611
  # Event emission methods
612
612
  sig { params(signature_name: String, version: T.nilable(String)).void }
613
613
  def emit_register_start_event(signature_name, version)
614
- DSPy::Instrumentation.emit('dspy.registry.register_start', {
615
- signature_name: signature_name,
616
- version: version,
617
- timestamp: Time.now.iso8601
614
+ DSPy.log('registry.register_start', **{
615
+ 'registry.signature_name' => signature_name,
616
+ 'registry.version' => version
618
617
  })
619
618
  end
620
619
 
621
620
  sig { params(version: SignatureVersion).void }
622
621
  def emit_register_complete_event(version)
623
- DSPy::Instrumentation.emit('dspy.registry.register_complete', {
624
- signature_name: version.signature_name,
625
- version: version.version,
626
- version_hash: version.version_hash,
627
- timestamp: Time.now.iso8601
622
+ DSPy.log('registry.register_complete', **{
623
+ 'registry.signature_name' => version.signature_name,
624
+ 'registry.version' => version.version,
625
+ 'registry.version_hash' => version.version_hash
628
626
  })
629
627
  end
630
628
 
631
629
  sig { params(signature_name: String, version: T.nilable(String), error: Exception).void }
632
630
  def emit_register_error_event(signature_name, version, error)
633
- DSPy::Instrumentation.emit('dspy.registry.register_error', {
634
- signature_name: signature_name,
635
- version: version,
636
- error: error.message,
637
- timestamp: Time.now.iso8601
631
+ DSPy.log('registry.register_error', **{
632
+ 'registry.signature_name' => signature_name,
633
+ 'registry.version' => version,
634
+ 'registry.error' => error.message
638
635
  })
639
636
  end
640
637
 
641
638
  sig { params(signature_name: String, version: String).void }
642
639
  def emit_deploy_start_event(signature_name, version)
643
- DSPy::Instrumentation.emit('dspy.registry.deploy_start', {
644
- signature_name: signature_name,
645
- version: version,
646
- timestamp: Time.now.iso8601
640
+ DSPy.log('registry.deploy_start', **{
641
+ 'registry.signature_name' => signature_name,
642
+ 'registry.version' => version
647
643
  })
648
644
  end
649
645
 
650
646
  sig { params(version: SignatureVersion).void }
651
647
  def emit_deploy_complete_event(version)
652
- DSPy::Instrumentation.emit('dspy.registry.deploy_complete', {
653
- signature_name: version.signature_name,
654
- version: version.version,
655
- performance_score: version.performance_score,
656
- timestamp: Time.now.iso8601
648
+ DSPy.log('registry.deploy_complete', **{
649
+ 'registry.signature_name' => version.signature_name,
650
+ 'registry.version' => version.version,
651
+ 'registry.performance_score' => version.performance_score
657
652
  })
658
653
  end
659
654
 
660
655
  sig { params(signature_name: String, version: String, error: Exception).void }
661
656
  def emit_deploy_error_event(signature_name, version, error)
662
- DSPy::Instrumentation.emit('dspy.registry.deploy_error', {
663
- signature_name: signature_name,
664
- version: version,
665
- error: error.message,
666
- timestamp: Time.now.iso8601
657
+ DSPy.log('registry.deploy_error', **{
658
+ 'registry.signature_name' => signature_name,
659
+ 'registry.version' => version,
660
+ 'registry.error' => error.message
667
661
  })
668
662
  end
669
663
 
670
664
  sig { params(signature_name: String).void }
671
665
  def emit_rollback_start_event(signature_name)
672
- DSPy::Instrumentation.emit('dspy.registry.rollback_start', {
673
- signature_name: signature_name,
674
- timestamp: Time.now.iso8601
666
+ DSPy.log('registry.rollback_start', **{
667
+ 'registry.signature_name' => signature_name
675
668
  })
676
669
  end
677
670
 
678
671
  sig { params(version: SignatureVersion).void }
679
672
  def emit_rollback_complete_event(version)
680
- DSPy::Instrumentation.emit('dspy.registry.rollback_complete', {
681
- signature_name: version.signature_name,
682
- version: version.version,
683
- timestamp: Time.now.iso8601
673
+ DSPy.log('registry.rollback_complete', **{
674
+ 'registry.signature_name' => version.signature_name,
675
+ 'registry.version' => version.version
684
676
  })
685
677
  end
686
678
 
687
679
  sig { params(signature_name: String, error_message: String).void }
688
680
  def emit_rollback_error_event(signature_name, error_message)
689
- DSPy::Instrumentation.emit('dspy.registry.rollback_error', {
690
- signature_name: signature_name,
691
- error: error_message,
692
- timestamp: Time.now.iso8601
681
+ DSPy.log('registry.rollback_error', **{
682
+ 'registry.signature_name' => signature_name,
683
+ 'registry.error' => error_message
693
684
  })
694
685
  end
695
686
 
696
687
  sig { params(version: SignatureVersion).void }
697
688
  def emit_performance_update_event(version)
698
- DSPy::Instrumentation.emit('dspy.registry.performance_update', {
699
- signature_name: version.signature_name,
700
- version: version.version,
701
- performance_score: version.performance_score,
702
- timestamp: Time.now.iso8601
689
+ DSPy.log('registry.performance_update', **{
690
+ 'registry.signature_name' => version.signature_name,
691
+ 'registry.version' => version.version,
692
+ 'registry.performance_score' => version.performance_score
703
693
  })
704
694
  end
705
695
 
706
696
  sig { params(export_path: String, signature_count: Integer).void }
707
697
  def emit_export_event(export_path, signature_count)
708
- DSPy::Instrumentation.emit('dspy.registry.export', {
709
- export_path: export_path,
710
- signature_count: signature_count,
711
- timestamp: Time.now.iso8601
698
+ DSPy.log('registry.export', **{
699
+ 'registry.export_path' => export_path,
700
+ 'registry.signature_count' => signature_count
712
701
  })
713
702
  end
714
703
 
715
704
  sig { params(import_path: String, signature_count: Integer).void }
716
705
  def emit_import_event(import_path, signature_count)
717
- DSPy::Instrumentation.emit('dspy.registry.import', {
718
- import_path: import_path,
719
- signature_count: signature_count,
720
- timestamp: Time.now.iso8601
706
+ DSPy.log('registry.import', **{
707
+ 'registry.import_path' => import_path,
708
+ 'registry.signature_count' => signature_count
721
709
  })
722
710
  end
723
711
  end
@@ -357,84 +357,75 @@ module DSPy
357
357
  # Event emission methods
358
358
  sig { params(program_id: T.nilable(String)).void }
359
359
  def emit_save_start_event(program_id)
360
- DSPy::Instrumentation.emit('dspy.storage.save_start', {
361
- program_id: program_id,
362
- storage_path: @storage_path,
363
- timestamp: Time.now.iso8601
360
+ DSPy.log('storage.save_start', **{
361
+ 'storage.program_id' => program_id,
362
+ 'storage.path' => @storage_path
364
363
  })
365
364
  end
366
365
 
367
366
  sig { params(saved_program: SavedProgram).void }
368
367
  def emit_save_complete_event(saved_program)
369
- DSPy::Instrumentation.emit('dspy.storage.save_complete', {
370
- program_id: saved_program.program_id,
371
- best_score: saved_program.optimization_result[:best_score_value],
372
- file_size_bytes: File.size(program_file_path(saved_program.program_id)),
373
- timestamp: Time.now.iso8601
368
+ DSPy.log('storage.save_complete', **{
369
+ 'storage.program_id' => saved_program.program_id,
370
+ 'storage.best_score' => saved_program.optimization_result[:best_score_value],
371
+ 'storage.file_size' => File.size(program_file_path(saved_program.program_id))
374
372
  })
375
373
  end
376
374
 
377
375
  sig { params(program_id: T.nilable(String), error: Exception).void }
378
376
  def emit_save_error_event(program_id, error)
379
- DSPy::Instrumentation.emit('dspy.storage.save_error', {
380
- program_id: program_id,
381
- error: error.message,
382
- error_class: error.class.name,
383
- timestamp: Time.now.iso8601
377
+ DSPy.log('storage.save_error', **{
378
+ 'storage.program_id' => program_id,
379
+ 'storage.error' => error.message,
380
+ 'storage.error_class' => error.class.name
384
381
  })
385
382
  end
386
383
 
387
384
  sig { params(program_id: String).void }
388
385
  def emit_load_start_event(program_id)
389
- DSPy::Instrumentation.emit('dspy.storage.load_start', {
390
- program_id: program_id,
391
- timestamp: Time.now.iso8601
386
+ DSPy.log('storage.load_start', **{
387
+ 'storage.program_id' => program_id
392
388
  })
393
389
  end
394
390
 
395
391
  sig { params(saved_program: SavedProgram).void }
396
392
  def emit_load_complete_event(saved_program)
397
- DSPy::Instrumentation.emit('dspy.storage.load_complete', {
398
- program_id: saved_program.program_id,
399
- saved_at: saved_program.saved_at.iso8601,
400
- age_hours: ((Time.now - saved_program.saved_at) / 3600).round(2),
401
- timestamp: Time.now.iso8601
393
+ DSPy.log('storage.load_complete', **{
394
+ 'storage.program_id' => saved_program.program_id,
395
+ 'storage.saved_at' => saved_program.saved_at.iso8601,
396
+ 'storage.age_hours' => ((Time.now - saved_program.saved_at) / 3600).round(2)
402
397
  })
403
398
  end
404
399
 
405
400
  sig { params(program_id: String, error: T.any(String, Exception)).void }
406
401
  def emit_load_error_event(program_id, error)
407
402
  error_message = error.is_a?(Exception) ? error.message : error.to_s
408
- DSPy::Instrumentation.emit('dspy.storage.load_error', {
409
- program_id: program_id,
410
- error: error_message,
411
- timestamp: Time.now.iso8601
403
+ DSPy.log('storage.load_error', **{
404
+ 'storage.program_id' => program_id,
405
+ 'storage.error' => error_message
412
406
  })
413
407
  end
414
408
 
415
409
  sig { params(program_id: String).void }
416
410
  def emit_delete_event(program_id)
417
- DSPy::Instrumentation.emit('dspy.storage.delete', {
418
- program_id: program_id,
419
- timestamp: Time.now.iso8601
411
+ DSPy.log('storage.delete', **{
412
+ 'storage.program_id' => program_id
420
413
  })
421
414
  end
422
415
 
423
416
  sig { params(export_path: String, program_count: Integer).void }
424
417
  def emit_export_event(export_path, program_count)
425
- DSPy::Instrumentation.emit('dspy.storage.export', {
426
- export_path: export_path,
427
- program_count: program_count,
428
- timestamp: Time.now.iso8601
418
+ DSPy.log('storage.export', **{
419
+ 'storage.export_path' => export_path,
420
+ 'storage.program_count' => program_count
429
421
  })
430
422
  end
431
423
 
432
424
  sig { params(import_path: String, program_count: Integer).void }
433
425
  def emit_import_event(import_path, program_count)
434
- DSPy::Instrumentation.emit('dspy.storage.import', {
435
- import_path: import_path,
436
- program_count: program_count,
437
- timestamp: Time.now.iso8601
426
+ DSPy.log('storage.import', **{
427
+ 'storage.import_path' => import_path,
428
+ 'storage.program_count' => program_count
438
429
  })
439
430
  end
440
431
  end
@@ -250,10 +250,9 @@ module DSPy
250
250
  end
251
251
  end
252
252
 
253
- DSPy::Instrumentation.emit('dspy.storage.cleanup', {
254
- deleted_count: deleted_count,
255
- remaining_count: @config.max_stored_programs,
256
- timestamp: Time.now.iso8601
253
+ DSPy.log('storage.cleanup', **{
254
+ 'storage.deleted_count' => deleted_count,
255
+ 'storage.remaining_count' => @config.max_stored_programs
257
256
  })
258
257
 
259
258
  deleted_count