decision_agent 0.1.3 → 0.1.4
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/lib/decision_agent/ab_testing/ab_test.rb +197 -0
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +152 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
- data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +148 -3
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent.rb +7 -0
- data/lib/generators/decision_agent/install/install_generator.rb +37 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +330 -0
- data/spec/ab_testing/ab_test_spec.rb +270 -0
- data/spec/examples.txt +612 -548
- data/spec/issue_verification_spec.rb +95 -21
- data/spec/monitoring/metrics_collector_spec.rb +2 -2
- data/spec/monitoring/monitored_agent_spec.rb +1 -1
- data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +346 -0
- data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
- metadata +26 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :decision_agent do
|
|
4
|
+
namespace :monitoring do
|
|
5
|
+
desc "Cleanup old monitoring metrics (default: older than 30 days)"
|
|
6
|
+
task cleanup: :environment do
|
|
7
|
+
older_than = ENV.fetch("OLDER_THAN", 30 * 24 * 3600).to_i # Default: 30 days
|
|
8
|
+
|
|
9
|
+
puts "Cleaning up metrics older than #{older_than / 86_400} days..."
|
|
10
|
+
|
|
11
|
+
count = 0
|
|
12
|
+
count += DecisionLog.where("created_at < ?", Time.now - older_than).delete_all
|
|
13
|
+
count += EvaluationMetric.where("created_at < ?", Time.now - older_than).delete_all
|
|
14
|
+
count += PerformanceMetric.where("created_at < ?", Time.now - older_than).delete_all
|
|
15
|
+
count += ErrorMetric.where("created_at < ?", Time.now - older_than).delete_all
|
|
16
|
+
|
|
17
|
+
puts "Deleted #{count} old metric records"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Show monitoring statistics"
|
|
21
|
+
task stats: :environment do
|
|
22
|
+
time_range = ENV.fetch("TIME_RANGE", 3600).to_i # Default: 1 hour
|
|
23
|
+
|
|
24
|
+
puts "\n=== Decision Agent Monitoring Statistics ==="
|
|
25
|
+
puts "Time range: Last #{time_range / 3600.0} hours\n\n"
|
|
26
|
+
|
|
27
|
+
decisions = DecisionLog.recent(time_range)
|
|
28
|
+
evaluations = EvaluationMetric.recent(time_range)
|
|
29
|
+
performance = PerformanceMetric.recent(time_range)
|
|
30
|
+
errors = ErrorMetric.recent(time_range)
|
|
31
|
+
|
|
32
|
+
puts "Decisions:"
|
|
33
|
+
puts " Total: #{decisions.count}"
|
|
34
|
+
puts " Average confidence: #{decisions.average(:confidence)&.round(4) || 'N/A'}"
|
|
35
|
+
puts " Success rate: #{(DecisionLog.success_rate(time_range: time_range) * 100).round(2)}%"
|
|
36
|
+
puts " By decision: #{decisions.group(:decision).count}"
|
|
37
|
+
|
|
38
|
+
puts "\nEvaluations:"
|
|
39
|
+
puts " Total: #{evaluations.count}"
|
|
40
|
+
puts " By evaluator: #{evaluations.group(:evaluator_name).count}"
|
|
41
|
+
|
|
42
|
+
puts "\nPerformance:"
|
|
43
|
+
puts " Total operations: #{performance.count}"
|
|
44
|
+
puts " Average duration: #{performance.average_duration(time_range: time_range)&.round(2) || 'N/A'} ms"
|
|
45
|
+
puts " P95 latency: #{performance.p95(time_range: time_range).round(2)} ms"
|
|
46
|
+
puts " P99 latency: #{performance.p99(time_range: time_range).round(2)} ms"
|
|
47
|
+
puts " Success rate: #{(performance.success_rate(time_range: time_range) * 100).round(2)}%"
|
|
48
|
+
|
|
49
|
+
puts "\nErrors:"
|
|
50
|
+
puts " Total: #{errors.count}"
|
|
51
|
+
puts " By type: #{errors.group(:error_type).count}"
|
|
52
|
+
puts " By severity: #{errors.group(:severity).count}"
|
|
53
|
+
puts " Critical: #{errors.critical.count}"
|
|
54
|
+
|
|
55
|
+
puts "\n=== End of Statistics ===\n"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
desc "Archive old metrics to JSON file"
|
|
59
|
+
task :archive, [:output_file] => :environment do |_t, args|
|
|
60
|
+
output_file = args[:output_file] || "metrics_archive_#{Time.now.to_i}.json"
|
|
61
|
+
older_than = ENV.fetch("OLDER_THAN", 30 * 24 * 3600).to_i
|
|
62
|
+
cutoff_time = Time.now - older_than
|
|
63
|
+
|
|
64
|
+
puts "Archiving metrics older than #{older_than / 86_400} days to #{output_file}..."
|
|
65
|
+
|
|
66
|
+
archive_data = {
|
|
67
|
+
archived_at: Time.now.iso8601,
|
|
68
|
+
cutoff_time: cutoff_time.iso8601,
|
|
69
|
+
decisions: DecisionLog.where("created_at < ?", cutoff_time).map do |d|
|
|
70
|
+
{
|
|
71
|
+
decision: d.decision,
|
|
72
|
+
confidence: d.confidence,
|
|
73
|
+
context: d.parsed_context,
|
|
74
|
+
status: d.status,
|
|
75
|
+
created_at: d.created_at.iso8601
|
|
76
|
+
}
|
|
77
|
+
end,
|
|
78
|
+
evaluations: EvaluationMetric.where("created_at < ?", cutoff_time).map do |e|
|
|
79
|
+
{
|
|
80
|
+
evaluator_name: e.evaluator_name,
|
|
81
|
+
score: e.score,
|
|
82
|
+
success: e.success,
|
|
83
|
+
details: e.parsed_details,
|
|
84
|
+
created_at: e.created_at.iso8601
|
|
85
|
+
}
|
|
86
|
+
end,
|
|
87
|
+
performance: PerformanceMetric.where("created_at < ?", cutoff_time).map do |p|
|
|
88
|
+
{
|
|
89
|
+
operation: p.operation,
|
|
90
|
+
duration_ms: p.duration_ms,
|
|
91
|
+
status: p.status,
|
|
92
|
+
metadata: p.parsed_metadata,
|
|
93
|
+
created_at: p.created_at.iso8601
|
|
94
|
+
}
|
|
95
|
+
end,
|
|
96
|
+
errors: ErrorMetric.where("created_at < ?", cutoff_time).map do |e|
|
|
97
|
+
{
|
|
98
|
+
error_type: e.error_type,
|
|
99
|
+
message: e.message,
|
|
100
|
+
severity: e.severity,
|
|
101
|
+
context: e.parsed_context,
|
|
102
|
+
created_at: e.created_at.iso8601
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
File.write(output_file, JSON.pretty_generate(archive_data))
|
|
108
|
+
|
|
109
|
+
total = archive_data.values.sum { |v| v.is_a?(Array) ? v.size : 0 }
|
|
110
|
+
puts "Archived #{total} metrics to #{output_file}"
|
|
111
|
+
puts "Run 'rake decision_agent:monitoring:cleanup' to delete these records from the database"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class DecisionLog < ApplicationRecord
|
|
4
|
+
has_many :evaluation_metrics, dependent: :destroy
|
|
5
|
+
|
|
6
|
+
validates :decision, presence: true
|
|
7
|
+
validates :confidence, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 1 }, allow_nil: true
|
|
8
|
+
validates :status, inclusion: { in: %w[success failure error] }, allow_nil: true
|
|
9
|
+
|
|
10
|
+
scope :recent, ->(time_range = 3600) { where("created_at >= ?", Time.now - time_range) }
|
|
11
|
+
scope :successful, -> { where(status: "success") }
|
|
12
|
+
scope :failed, -> { where(status: "failure") }
|
|
13
|
+
scope :with_errors, -> { where(status: "error") }
|
|
14
|
+
scope :by_decision, ->(decision) { where(decision: decision) }
|
|
15
|
+
scope :low_confidence, ->(threshold = 0.5) { where("confidence < ?", threshold) }
|
|
16
|
+
scope :high_confidence, ->(threshold = 0.8) { where("confidence >= ?", threshold) }
|
|
17
|
+
|
|
18
|
+
# Time series aggregation helpers
|
|
19
|
+
def self.count_by_time_bucket(bucket_size: 60, time_range: 3600)
|
|
20
|
+
recent(time_range)
|
|
21
|
+
.group("(EXTRACT(EPOCH FROM created_at)::bigint / #{bucket_size}) * #{bucket_size}")
|
|
22
|
+
.count
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.average_confidence_by_time(bucket_size: 60, time_range: 3600)
|
|
26
|
+
recent(time_range)
|
|
27
|
+
.where.not(confidence: nil)
|
|
28
|
+
.group("(EXTRACT(EPOCH FROM created_at)::bigint / #{bucket_size}) * #{bucket_size}")
|
|
29
|
+
.average(:confidence)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.success_rate(time_range: 3600)
|
|
33
|
+
total = recent(time_range).where.not(status: nil).count
|
|
34
|
+
return 0.0 if total.zero?
|
|
35
|
+
|
|
36
|
+
successful_count = recent(time_range).successful.count
|
|
37
|
+
successful_count.to_f / total
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Parse JSON context field
|
|
41
|
+
def parsed_context
|
|
42
|
+
return {} if context.nil?
|
|
43
|
+
|
|
44
|
+
JSON.parse(context, symbolize_names: true)
|
|
45
|
+
rescue JSON::ParserError
|
|
46
|
+
{}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Parse JSON metadata field
|
|
50
|
+
def parsed_metadata
|
|
51
|
+
return {} if metadata.nil?
|
|
52
|
+
|
|
53
|
+
JSON.parse(metadata, symbolize_names: true)
|
|
54
|
+
rescue JSON::ParserError
|
|
55
|
+
{}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ErrorMetric < ApplicationRecord
|
|
4
|
+
validates :error_type, presence: true
|
|
5
|
+
validates :severity, inclusion: { in: %w[low medium high critical] }, allow_nil: true
|
|
6
|
+
|
|
7
|
+
scope :recent, ->(time_range = 3600) { where("created_at >= ?", Time.now - time_range) }
|
|
8
|
+
scope :by_type, ->(error_type) { where(error_type: error_type) }
|
|
9
|
+
scope :by_severity, ->(severity) { where(severity: severity) }
|
|
10
|
+
scope :critical, -> { where(severity: "critical") }
|
|
11
|
+
scope :high_severity, -> { where(severity: %w[high critical]) }
|
|
12
|
+
|
|
13
|
+
# Aggregation helpers
|
|
14
|
+
def self.count_by_type(time_range: 3600)
|
|
15
|
+
recent(time_range).group(:error_type).count
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.count_by_severity(time_range: 3600)
|
|
19
|
+
recent(time_range).group(:severity).count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.error_rate(time_range: 3600, total_operations: nil)
|
|
23
|
+
error_count = recent(time_range).count
|
|
24
|
+
return 0.0 if total_operations.nil? || total_operations.zero?
|
|
25
|
+
|
|
26
|
+
error_count.to_f / total_operations
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Time series aggregation
|
|
30
|
+
def self.count_by_time_bucket(bucket_size: 60, time_range: 3600)
|
|
31
|
+
recent(time_range)
|
|
32
|
+
.group("(EXTRACT(EPOCH FROM created_at)::bigint / #{bucket_size}) * #{bucket_size}")
|
|
33
|
+
.count
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Parse JSON context field
|
|
37
|
+
def parsed_context
|
|
38
|
+
return {} if context.nil?
|
|
39
|
+
|
|
40
|
+
JSON.parse(context, symbolize_names: true)
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
{}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse JSON stack_trace field
|
|
46
|
+
def parsed_stack_trace
|
|
47
|
+
return [] if stack_trace.nil?
|
|
48
|
+
|
|
49
|
+
JSON.parse(stack_trace)
|
|
50
|
+
rescue JSON::ParserError
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class EvaluationMetric < ApplicationRecord
|
|
4
|
+
belongs_to :decision_log, optional: true
|
|
5
|
+
|
|
6
|
+
validates :evaluator_name, presence: true
|
|
7
|
+
validates :score, numericality: true, allow_nil: true
|
|
8
|
+
|
|
9
|
+
scope :recent, ->(time_range = 3600) { where("created_at >= ?", Time.now - time_range) }
|
|
10
|
+
scope :by_evaluator, ->(evaluator) { where(evaluator_name: evaluator) }
|
|
11
|
+
scope :successful, -> { where(success: true) }
|
|
12
|
+
scope :failed, -> { where(success: false) }
|
|
13
|
+
|
|
14
|
+
# Aggregation helpers
|
|
15
|
+
def self.average_score_by_evaluator(time_range: 3600)
|
|
16
|
+
recent(time_range)
|
|
17
|
+
.where.not(score: nil)
|
|
18
|
+
.group(:evaluator_name)
|
|
19
|
+
.average(:score)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.success_rate_by_evaluator(time_range: 3600)
|
|
23
|
+
recent(time_range)
|
|
24
|
+
.where.not(success: nil)
|
|
25
|
+
.group(:evaluator_name)
|
|
26
|
+
.select("evaluator_name, AVG(CASE WHEN success THEN 1.0 ELSE 0.0 END) as success_rate")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.count_by_evaluator(time_range: 3600)
|
|
30
|
+
recent(time_range)
|
|
31
|
+
.group(:evaluator_name)
|
|
32
|
+
.count
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parse JSON details field
|
|
36
|
+
def parsed_details
|
|
37
|
+
return {} if details.nil?
|
|
38
|
+
|
|
39
|
+
JSON.parse(details, symbolize_names: true)
|
|
40
|
+
rescue JSON::ParserError
|
|
41
|
+
{}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateDecisionAgentMonitoringTables < ActiveRecord::Migration[7.0]
|
|
4
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
5
|
+
def change
|
|
6
|
+
# Decision logs table
|
|
7
|
+
create_table :decision_logs do |t|
|
|
8
|
+
t.string :decision, null: false
|
|
9
|
+
t.float :confidence
|
|
10
|
+
t.integer :evaluations_count, default: 0
|
|
11
|
+
t.float :duration_ms
|
|
12
|
+
t.string :status # success, failure, error
|
|
13
|
+
t.text :context # JSON
|
|
14
|
+
t.text :metadata # JSON
|
|
15
|
+
|
|
16
|
+
t.timestamps
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
add_index :decision_logs, :decision
|
|
20
|
+
add_index :decision_logs, :status
|
|
21
|
+
add_index :decision_logs, :confidence
|
|
22
|
+
add_index :decision_logs, :created_at
|
|
23
|
+
add_index :decision_logs, %i[decision created_at]
|
|
24
|
+
add_index :decision_logs, %i[status created_at]
|
|
25
|
+
|
|
26
|
+
# Evaluation metrics table
|
|
27
|
+
create_table :evaluation_metrics do |t|
|
|
28
|
+
t.references :decision_log, foreign_key: true, index: true
|
|
29
|
+
t.string :evaluator_name, null: false
|
|
30
|
+
t.float :score
|
|
31
|
+
t.boolean :success
|
|
32
|
+
t.float :duration_ms
|
|
33
|
+
t.text :details # JSON
|
|
34
|
+
|
|
35
|
+
t.timestamps
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
add_index :evaluation_metrics, :evaluator_name
|
|
39
|
+
add_index :evaluation_metrics, :success
|
|
40
|
+
add_index :evaluation_metrics, :created_at
|
|
41
|
+
add_index :evaluation_metrics, %i[evaluator_name created_at]
|
|
42
|
+
add_index :evaluation_metrics, %i[evaluator_name success]
|
|
43
|
+
|
|
44
|
+
# Performance metrics table
|
|
45
|
+
create_table :performance_metrics do |t|
|
|
46
|
+
t.string :operation, null: false
|
|
47
|
+
t.float :duration_ms
|
|
48
|
+
t.string :status # success, failure, error
|
|
49
|
+
t.text :metadata # JSON
|
|
50
|
+
|
|
51
|
+
t.timestamps
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
add_index :performance_metrics, :operation
|
|
55
|
+
add_index :performance_metrics, :status
|
|
56
|
+
add_index :performance_metrics, :duration_ms
|
|
57
|
+
add_index :performance_metrics, :created_at
|
|
58
|
+
add_index :performance_metrics, %i[operation created_at]
|
|
59
|
+
add_index :performance_metrics, %i[status created_at]
|
|
60
|
+
|
|
61
|
+
# Error metrics table
|
|
62
|
+
create_table :error_metrics do |t|
|
|
63
|
+
t.string :error_type, null: false
|
|
64
|
+
t.text :message
|
|
65
|
+
t.text :stack_trace # JSON array
|
|
66
|
+
t.string :severity # low, medium, high, critical
|
|
67
|
+
t.text :context # JSON
|
|
68
|
+
|
|
69
|
+
t.timestamps
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
add_index :error_metrics, :error_type
|
|
73
|
+
add_index :error_metrics, :severity
|
|
74
|
+
add_index :error_metrics, :created_at
|
|
75
|
+
add_index :error_metrics, %i[error_type created_at]
|
|
76
|
+
add_index :error_metrics, %i[severity created_at]
|
|
77
|
+
|
|
78
|
+
# PostgreSQL-specific optimizations (optional)
|
|
79
|
+
return unless adapter_name == "PostgreSQL"
|
|
80
|
+
|
|
81
|
+
# Partial indexes for active records (recent data)
|
|
82
|
+
execute <<-SQL
|
|
83
|
+
CREATE INDEX index_decision_logs_on_recent
|
|
84
|
+
ON decision_logs (created_at DESC)
|
|
85
|
+
WHERE created_at >= NOW() - INTERVAL '7 days';
|
|
86
|
+
SQL
|
|
87
|
+
|
|
88
|
+
execute <<-SQL
|
|
89
|
+
CREATE INDEX index_performance_metrics_on_recent
|
|
90
|
+
ON performance_metrics (created_at DESC)
|
|
91
|
+
WHERE created_at >= NOW() - INTERVAL '7 days';
|
|
92
|
+
SQL
|
|
93
|
+
|
|
94
|
+
execute <<-SQL
|
|
95
|
+
CREATE INDEX index_error_metrics_on_recent_critical
|
|
96
|
+
ON error_metrics (created_at DESC)
|
|
97
|
+
WHERE severity IN ('high', 'critical') AND created_at >= NOW() - INTERVAL '7 days';
|
|
98
|
+
SQL
|
|
99
|
+
|
|
100
|
+
# Consider table partitioning for large-scale deployments
|
|
101
|
+
# Example: Partition by month for decision_logs
|
|
102
|
+
# This is commented out by default - enable if needed
|
|
103
|
+
# execute <<-SQL
|
|
104
|
+
# CREATE TABLE decision_logs_partitioned (LIKE decision_logs INCLUDING ALL)
|
|
105
|
+
# PARTITION BY RANGE (created_at);
|
|
106
|
+
# SQL
|
|
107
|
+
end
|
|
108
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
109
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PerformanceMetric < ApplicationRecord
|
|
4
|
+
validates :operation, presence: true
|
|
5
|
+
validates :duration_ms, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
|
6
|
+
validates :status, inclusion: { in: %w[success failure error] }, allow_nil: true
|
|
7
|
+
|
|
8
|
+
scope :recent, ->(time_range = 3600) { where("created_at >= ?", Time.now - time_range) }
|
|
9
|
+
scope :by_operation, ->(operation) { where(operation: operation) }
|
|
10
|
+
scope :successful, -> { where(status: "success") }
|
|
11
|
+
scope :failed, -> { where(status: "failure") }
|
|
12
|
+
scope :with_errors, -> { where(status: "error") }
|
|
13
|
+
scope :slow, ->(threshold_ms = 1000) { where("duration_ms > ?", threshold_ms) }
|
|
14
|
+
|
|
15
|
+
# Performance statistics
|
|
16
|
+
def self.average_duration(time_range: 3600)
|
|
17
|
+
recent(time_range).where.not(duration_ms: nil).average(:duration_ms).to_f
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.percentile(pct, time_range: 3600)
|
|
21
|
+
durations = recent(time_range).where.not(duration_ms: nil).order(:duration_ms).pluck(:duration_ms)
|
|
22
|
+
return 0.0 if durations.empty?
|
|
23
|
+
|
|
24
|
+
index = ((durations.length - 1) * pct).ceil
|
|
25
|
+
durations[index].to_f
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.p50(time_range: 3600)
|
|
29
|
+
percentile(0.50, time_range: time_range)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.p95(time_range: 3600)
|
|
33
|
+
percentile(0.95, time_range: time_range)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.p99(time_range: 3600)
|
|
37
|
+
percentile(0.99, time_range: time_range)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.max_duration(time_range: 3600)
|
|
41
|
+
recent(time_range).maximum(:duration_ms).to_f
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.min_duration(time_range: 3600)
|
|
45
|
+
recent(time_range).minimum(:duration_ms).to_f
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.success_rate(time_range: 3600)
|
|
49
|
+
total = recent(time_range).where.not(status: nil).count
|
|
50
|
+
return 0.0 if total.zero?
|
|
51
|
+
|
|
52
|
+
successful_count = recent(time_range).successful.count
|
|
53
|
+
successful_count.to_f / total
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.count_by_operation(time_range: 3600)
|
|
57
|
+
recent(time_range).group(:operation).count
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Time series aggregation
|
|
61
|
+
def self.average_duration_by_time(bucket_size: 60, time_range: 3600)
|
|
62
|
+
recent(time_range)
|
|
63
|
+
.where.not(duration_ms: nil)
|
|
64
|
+
.group("(EXTRACT(EPOCH FROM created_at)::bigint / #{bucket_size}) * #{bucket_size}")
|
|
65
|
+
.average(:duration_ms)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Parse JSON metadata field
|
|
69
|
+
def parsed_metadata
|
|
70
|
+
return {} if metadata.nil?
|
|
71
|
+
|
|
72
|
+
JSON.parse(metadata, symbolize_names: true)
|
|
73
|
+
rescue JSON::ParserError
|
|
74
|
+
{}
|
|
75
|
+
end
|
|
76
|
+
end
|