decision_agent 0.3.0 → 1.0.1
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 +272 -7
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
- data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
- data/lib/decision_agent/dsl/schema_validator.rb +51 -13
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/index.html +49 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +65 -0
- data/lib/decision_agent/web/server.rb +594 -23
- data/lib/decision_agent.rb +60 -2
- metadata +53 -73
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -778
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dmn/decision_graph_spec.rb +0 -282
- data/spec/dmn/decision_tree_spec.rb +0 -203
- data/spec/dmn/feel/errors_spec.rb +0 -18
- data/spec/dmn/feel/functions_spec.rb +0 -400
- data/spec/dmn/feel/simple_parser_spec.rb +0 -274
- data/spec/dmn/feel/types_spec.rb +0 -176
- data/spec/dmn/feel_parser_spec.rb +0 -489
- data/spec/dmn/hit_policy_spec.rb +0 -202
- data/spec/dmn/integration_spec.rb +0 -226
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1909
- data/spec/fixtures/dmn/complex_decision.dmn +0 -81
- data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
- data/spec/fixtures/dmn/simple_decision.dmn +0 -40
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -501
- data/spec/monitoring/monitored_agent_spec.rb +0 -225
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -493
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -490
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -2134
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "decision_agent/monitoring/metrics_collector"
|
|
3
|
-
require "decision_agent/monitoring/monitored_agent"
|
|
4
|
-
|
|
5
|
-
RSpec.describe DecisionAgent::Monitoring::MonitoredAgent do
|
|
6
|
-
let(:collector) { DecisionAgent::Monitoring::MetricsCollector.new(storage: :memory) }
|
|
7
|
-
let(:evaluator) do
|
|
8
|
-
DecisionAgent::Evaluators::JsonRuleEvaluator.new(
|
|
9
|
-
rules_json: {
|
|
10
|
-
version: "1.0",
|
|
11
|
-
ruleset: "test",
|
|
12
|
-
rules: [{
|
|
13
|
-
id: "test_rule",
|
|
14
|
-
if: { field: "amount", op: "gt", value: 100 },
|
|
15
|
-
then: { decision: "approve", weight: 0.9, reason: "Test reason" }
|
|
16
|
-
}]
|
|
17
|
-
},
|
|
18
|
-
name: "test_evaluator"
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
let(:agent) { DecisionAgent::Agent.new(evaluators: [evaluator]) }
|
|
22
|
-
let(:monitored_agent) { described_class.new(agent: agent, metrics_collector: collector) }
|
|
23
|
-
|
|
24
|
-
describe "#initialize" do
|
|
25
|
-
it "wraps an agent with metrics collection" do
|
|
26
|
-
expect(monitored_agent.agent).to eq(agent)
|
|
27
|
-
expect(monitored_agent.metrics_collector).to eq(collector)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
describe "#decide" do
|
|
32
|
-
let(:context) { { amount: 1000 } }
|
|
33
|
-
|
|
34
|
-
it "makes a decision and records metrics" do
|
|
35
|
-
result = monitored_agent.decide(context: context)
|
|
36
|
-
|
|
37
|
-
expect(result).to be_a(DecisionAgent::Decision)
|
|
38
|
-
expect(result.decision).to eq("approve")
|
|
39
|
-
expect(collector.metrics_count[:decisions]).to eq(1)
|
|
40
|
-
expect(collector.metrics_count[:evaluations]).to eq(1)
|
|
41
|
-
expect(collector.metrics_count[:performance]).to eq(1)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
it "records decision metrics with duration" do
|
|
45
|
-
monitored_agent.decide(context: context)
|
|
46
|
-
|
|
47
|
-
stats = collector.statistics
|
|
48
|
-
expect(stats[:decisions][:total]).to eq(1)
|
|
49
|
-
expect(stats[:decisions][:avg_duration_ms]).to be > 0
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
it "records evaluation metrics" do
|
|
53
|
-
monitored_agent.decide(context: context)
|
|
54
|
-
|
|
55
|
-
stats = collector.statistics
|
|
56
|
-
expect(stats[:evaluations][:total]).to eq(1)
|
|
57
|
-
expect(stats[:evaluations][:evaluator_distribution]["test_evaluator"]).to eq(1)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
it "records performance metrics as successful" do
|
|
61
|
-
monitored_agent.decide(context: context)
|
|
62
|
-
|
|
63
|
-
stats = collector.statistics
|
|
64
|
-
expect(stats[:performance][:total_operations]).to eq(1)
|
|
65
|
-
expect(stats[:performance][:successful]).to eq(1)
|
|
66
|
-
expect(stats[:performance][:success_rate]).to eq(1.0)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
it "includes metadata in performance metrics" do
|
|
70
|
-
monitored_agent.decide(context: context)
|
|
71
|
-
|
|
72
|
-
collector.statistics
|
|
73
|
-
perf_metric = collector.instance_variable_get(:@metrics)[:performance].first
|
|
74
|
-
|
|
75
|
-
expect(perf_metric[:metadata][:evaluators_count]).to eq(1)
|
|
76
|
-
expect(perf_metric[:metadata][:decision]).to eq("approve")
|
|
77
|
-
expect(perf_metric[:metadata][:confidence]).to be_a(Float)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
context "when decision fails" do
|
|
81
|
-
before do
|
|
82
|
-
allow(agent).to receive(:decide).and_raise(StandardError.new("Test error"))
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
it "records error metrics" do
|
|
86
|
-
expect do
|
|
87
|
-
monitored_agent.decide(context: context)
|
|
88
|
-
end.to raise_error(StandardError, "Test error")
|
|
89
|
-
|
|
90
|
-
expect(collector.metrics_count[:errors]).to eq(1)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
it "records failed performance metrics" do
|
|
94
|
-
expect do
|
|
95
|
-
monitored_agent.decide(context: context)
|
|
96
|
-
end.to raise_error(StandardError)
|
|
97
|
-
|
|
98
|
-
stats = collector.statistics
|
|
99
|
-
expect(stats[:performance][:total_operations]).to eq(1)
|
|
100
|
-
expect(stats[:performance][:failed]).to eq(1)
|
|
101
|
-
expect(stats[:performance][:success_rate]).to eq(0.0)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
it "includes error details in metrics" do
|
|
105
|
-
expect do
|
|
106
|
-
monitored_agent.decide(context: context)
|
|
107
|
-
end.to raise_error(StandardError)
|
|
108
|
-
|
|
109
|
-
error_metric = collector.instance_variable_get(:@metrics)[:errors].first
|
|
110
|
-
expect(error_metric[:error_class]).to eq("StandardError")
|
|
111
|
-
expect(error_metric[:error_message]).to eq("Test error")
|
|
112
|
-
expect(error_metric[:context]).to eq(context)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
it "re-raises the error" do
|
|
116
|
-
expect do
|
|
117
|
-
monitored_agent.decide(context: context)
|
|
118
|
-
end.to raise_error(StandardError, "Test error")
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
it "handles Context objects" do
|
|
123
|
-
ctx = DecisionAgent::Context.new(context)
|
|
124
|
-
result = monitored_agent.decide(context: ctx)
|
|
125
|
-
|
|
126
|
-
expect(result).to be_a(DecisionAgent::Decision)
|
|
127
|
-
expect(collector.metrics_count[:decisions]).to eq(1)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
it "handles hash contexts" do
|
|
131
|
-
result = monitored_agent.decide(context: context)
|
|
132
|
-
|
|
133
|
-
expect(result).to be_a(DecisionAgent::Decision)
|
|
134
|
-
expect(collector.metrics_count[:decisions]).to eq(1)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
it "measures decision duration accurately" do
|
|
138
|
-
# Mock agent to introduce delay
|
|
139
|
-
allow(agent).to receive(:decide) do |context:, **_kwargs|
|
|
140
|
-
sleep 0.01 # 10ms delay
|
|
141
|
-
evaluation = evaluator.evaluate(context)
|
|
142
|
-
DecisionAgent::Decision.new(
|
|
143
|
-
decision: "approve",
|
|
144
|
-
confidence: 0.9,
|
|
145
|
-
explanations: ["Test"],
|
|
146
|
-
evaluations: [evaluation].compact, # Remove nils in case evaluation returns nil
|
|
147
|
-
audit_payload: {}
|
|
148
|
-
)
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
monitored_agent.decide(context: context)
|
|
152
|
-
|
|
153
|
-
stats = collector.statistics
|
|
154
|
-
expect(stats[:decisions][:avg_duration_ms]).to be >= 10
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
describe "method delegation" do
|
|
159
|
-
it "delegates methods to wrapped agent" do
|
|
160
|
-
expect(monitored_agent.evaluators).to eq(agent.evaluators)
|
|
161
|
-
expect(monitored_agent.scoring_strategy).to eq(agent.scoring_strategy)
|
|
162
|
-
expect(monitored_agent.audit_adapter).to eq(agent.audit_adapter)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
it "responds to agent methods" do
|
|
166
|
-
expect(monitored_agent).to respond_to(:evaluators)
|
|
167
|
-
expect(monitored_agent).to respond_to(:scoring_strategy)
|
|
168
|
-
expect(monitored_agent).to respond_to(:audit_adapter)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
describe "thread safety" do
|
|
173
|
-
it "handles concurrent decisions safely" do
|
|
174
|
-
# Materialize let variables before creating threads
|
|
175
|
-
test_context = { amount: 1000 }
|
|
176
|
-
test_monitored_agent = monitored_agent
|
|
177
|
-
test_collector = collector
|
|
178
|
-
|
|
179
|
-
threads = 10.times.map do
|
|
180
|
-
Thread.new do
|
|
181
|
-
10.times do
|
|
182
|
-
test_monitored_agent.decide(context: test_context)
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
threads.each(&:join)
|
|
188
|
-
|
|
189
|
-
expect(test_collector.metrics_count[:decisions]).to eq(100)
|
|
190
|
-
expect(test_collector.metrics_count[:evaluations]).to eq(100)
|
|
191
|
-
expect(test_collector.metrics_count[:performance]).to eq(100)
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
describe "integration test" do
|
|
196
|
-
it "provides comprehensive metrics for multiple decisions" do
|
|
197
|
-
contexts = [
|
|
198
|
-
{ amount: 500 },
|
|
199
|
-
{ amount: 1500 },
|
|
200
|
-
{ amount: 2000 }
|
|
201
|
-
]
|
|
202
|
-
|
|
203
|
-
contexts.each do |ctx|
|
|
204
|
-
monitored_agent.decide(context: ctx)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
stats = collector.statistics
|
|
208
|
-
|
|
209
|
-
# Summary
|
|
210
|
-
expect(stats[:summary][:total_decisions]).to eq(3)
|
|
211
|
-
expect(stats[:summary][:total_evaluations]).to eq(3)
|
|
212
|
-
expect(stats[:summary][:total_errors]).to eq(0)
|
|
213
|
-
|
|
214
|
-
# Decision stats
|
|
215
|
-
expect(stats[:decisions][:total]).to eq(3)
|
|
216
|
-
expect(stats[:decisions][:avg_confidence]).to be > 0
|
|
217
|
-
expect(stats[:decisions][:decision_distribution]["approve"]).to eq(3)
|
|
218
|
-
|
|
219
|
-
# Performance stats
|
|
220
|
-
expect(stats[:performance][:total_operations]).to eq(3)
|
|
221
|
-
expect(stats[:performance][:success_rate]).to eq(1.0)
|
|
222
|
-
expect(stats[:performance][:avg_duration_ms]).to be > 0
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
require "spec_helper"
|
|
2
|
-
require "decision_agent/monitoring/metrics_collector"
|
|
3
|
-
require "decision_agent/monitoring/prometheus_exporter"
|
|
4
|
-
|
|
5
|
-
RSpec.describe DecisionAgent::Monitoring::PrometheusExporter do
|
|
6
|
-
let(:collector) { DecisionAgent::Monitoring::MetricsCollector.new(storage: :memory) }
|
|
7
|
-
let(:exporter) { described_class.new(metrics_collector: collector, namespace: "test") }
|
|
8
|
-
|
|
9
|
-
let(:decision) do
|
|
10
|
-
double(
|
|
11
|
-
"Decision",
|
|
12
|
-
decision: "approve",
|
|
13
|
-
confidence: 0.85,
|
|
14
|
-
evaluations: [double("Evaluation", evaluator_name: "test_evaluator")]
|
|
15
|
-
)
|
|
16
|
-
end
|
|
17
|
-
let(:context) { double("Context", to_h: { user: "test" }) }
|
|
18
|
-
|
|
19
|
-
describe "#initialize" do
|
|
20
|
-
it "initializes with metrics collector" do
|
|
21
|
-
expect(exporter).to be_a(described_class)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
it "uses default namespace" do
|
|
25
|
-
exporter = described_class.new(metrics_collector: collector)
|
|
26
|
-
output = exporter.export
|
|
27
|
-
expect(output).to include("decision_agent_")
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
it "uses custom namespace" do
|
|
31
|
-
output = exporter.export
|
|
32
|
-
expect(output).to include("test_")
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
describe "#export" do
|
|
37
|
-
before do
|
|
38
|
-
# Record some metrics
|
|
39
|
-
3.times { collector.record_decision(decision, context, duration_ms: 10.0) }
|
|
40
|
-
collector.record_performance(operation: "decide", duration_ms: 15.0, success: true)
|
|
41
|
-
collector.record_error(StandardError.new("Test error"))
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
it "exports in Prometheus text format" do
|
|
45
|
-
output = exporter.export
|
|
46
|
-
|
|
47
|
-
expect(output).to be_a(String)
|
|
48
|
-
expect(output).to include("# DecisionAgent Metrics Export")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
it "includes decision metrics" do
|
|
52
|
-
output = exporter.export
|
|
53
|
-
|
|
54
|
-
expect(output).to include("# HELP test_decisions_total")
|
|
55
|
-
expect(output).to include("# TYPE test_decisions_total counter")
|
|
56
|
-
expect(output).to include("test_decisions_total 3")
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
it "includes confidence metrics" do
|
|
60
|
-
output = exporter.export
|
|
61
|
-
|
|
62
|
-
expect(output).to include("# HELP test_decision_confidence_avg")
|
|
63
|
-
expect(output).to include("# TYPE test_decision_confidence_avg gauge")
|
|
64
|
-
expect(output).to include("test_decision_confidence_avg 0.85")
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
it "includes performance metrics" do
|
|
68
|
-
output = exporter.export
|
|
69
|
-
|
|
70
|
-
expect(output).to include("# HELP test_success_rate")
|
|
71
|
-
expect(output).to include("# TYPE test_success_rate gauge")
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
it "includes error metrics" do
|
|
75
|
-
output = exporter.export
|
|
76
|
-
|
|
77
|
-
expect(output).to include("# HELP test_errors_total")
|
|
78
|
-
expect(output).to include("# TYPE test_errors_total counter")
|
|
79
|
-
expect(output).to include("test_errors_total 1")
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
it "includes system info" do
|
|
83
|
-
output = exporter.export
|
|
84
|
-
|
|
85
|
-
expect(output).to include("# HELP test_info")
|
|
86
|
-
expect(output).to include("# TYPE test_info gauge")
|
|
87
|
-
expect(output).to include("version=\"#{DecisionAgent::VERSION}\"")
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
it "includes decision distribution" do
|
|
91
|
-
output = exporter.export
|
|
92
|
-
|
|
93
|
-
expect(output).to include("# HELP test_decisions_by_type")
|
|
94
|
-
expect(output).to include("test_decisions_by_type{decision=\"approve\"} 3")
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
it "includes error distribution by type" do
|
|
98
|
-
output = exporter.export
|
|
99
|
-
|
|
100
|
-
expect(output).to include("# HELP test_errors_by_type")
|
|
101
|
-
expect(output).to include("test_errors_by_type{error=\"StandardError\"} 1")
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
it "includes metrics count" do
|
|
105
|
-
output = exporter.export
|
|
106
|
-
|
|
107
|
-
expect(output).to include("# HELP test_metrics_stored")
|
|
108
|
-
expect(output).to include("test_metrics_stored{type=\"decisions\"} 3")
|
|
109
|
-
expect(output).to include("test_metrics_stored{type=\"errors\"} 1")
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
describe "#register_kpi" do
|
|
114
|
-
it "registers a custom KPI" do
|
|
115
|
-
exporter.register_kpi(
|
|
116
|
-
name: "custom_metric",
|
|
117
|
-
value: 42.5,
|
|
118
|
-
help: "A custom metric"
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
output = exporter.export
|
|
122
|
-
expect(output).to include("# HELP test_custom_metric A custom metric")
|
|
123
|
-
expect(output).to include("# TYPE test_custom_metric gauge")
|
|
124
|
-
expect(output).to include("test_custom_metric 42.5")
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
it "registers KPI with labels" do
|
|
128
|
-
exporter.register_kpi(
|
|
129
|
-
name: "requests",
|
|
130
|
-
value: 100,
|
|
131
|
-
labels: { endpoint: "/api/v1", method: "GET" }
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
output = exporter.export
|
|
135
|
-
expect(output).to include("test_requests{endpoint=\"/api/v1\",method=\"GET\"} 100")
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
it "sanitizes metric names" do
|
|
139
|
-
exporter.register_kpi(name: "my-custom.metric!", value: 10)
|
|
140
|
-
|
|
141
|
-
output = exporter.export
|
|
142
|
-
expect(output).to include("test_my_custom_metric_")
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
it "escapes label values" do
|
|
146
|
-
exporter.register_kpi(
|
|
147
|
-
name: "metric",
|
|
148
|
-
value: 1,
|
|
149
|
-
labels: { message: 'Contains "quotes"' }
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
output = exporter.export
|
|
153
|
-
expect(output).to include('message="Contains \"quotes\""')
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
describe "#metrics_hash" do
|
|
158
|
-
before do
|
|
159
|
-
collector.record_decision(decision, context, duration_ms: 10.0)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
it "returns metrics as hash" do
|
|
163
|
-
metrics = exporter.metrics_hash
|
|
164
|
-
|
|
165
|
-
expect(metrics).to be_a(Hash)
|
|
166
|
-
expect(metrics).to have_key(:decisions)
|
|
167
|
-
expect(metrics).to have_key(:performance)
|
|
168
|
-
expect(metrics).to have_key(:errors)
|
|
169
|
-
expect(metrics).to have_key(:system)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
it "includes metric types" do
|
|
173
|
-
metrics = exporter.metrics_hash
|
|
174
|
-
|
|
175
|
-
expect(metrics[:decisions][:total][:type]).to eq("counter")
|
|
176
|
-
expect(metrics[:decisions][:avg_confidence][:type]).to eq("gauge")
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
it "includes metric values" do
|
|
180
|
-
metrics = exporter.metrics_hash
|
|
181
|
-
|
|
182
|
-
expect(metrics[:decisions][:total][:value]).to eq(1)
|
|
183
|
-
expect(metrics[:decisions][:avg_confidence][:value]).to eq(0.85)
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
describe "thread safety" do
|
|
188
|
-
it "handles concurrent KPI registration" do
|
|
189
|
-
threads = 10.times.map do |i|
|
|
190
|
-
Thread.new do
|
|
191
|
-
10.times do |j|
|
|
192
|
-
exporter.register_kpi(
|
|
193
|
-
name: "metric_#{i}_#{j}",
|
|
194
|
-
value: (i * 10) + j
|
|
195
|
-
)
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
expect { threads.each(&:join) }.not_to raise_error
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
it "handles concurrent exports" do
|
|
204
|
-
threads = 5.times.map do
|
|
205
|
-
Thread.new do
|
|
206
|
-
10.times { exporter.export }
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
expect { threads.each(&:join) }.not_to raise_error
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
describe "performance metrics export" do
|
|
215
|
-
before do
|
|
216
|
-
5.times do |i|
|
|
217
|
-
collector.record_performance(
|
|
218
|
-
operation: "decide",
|
|
219
|
-
duration_ms: (i + 1) * 10.0,
|
|
220
|
-
success: true
|
|
221
|
-
)
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
it "exports summary metrics" do
|
|
226
|
-
output = exporter.export
|
|
227
|
-
|
|
228
|
-
expect(output).to include("# TYPE test_operation_duration_ms summary")
|
|
229
|
-
expect(output).to include("test_operation_duration_ms{quantile=\"0.5\"}")
|
|
230
|
-
expect(output).to include("test_operation_duration_ms{quantile=\"0.95\"}")
|
|
231
|
-
expect(output).to include("test_operation_duration_ms{quantile=\"0.99\"}")
|
|
232
|
-
expect(output).to include("test_operation_duration_ms_sum")
|
|
233
|
-
expect(output).to include("test_operation_duration_ms_count")
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
describe "content type" do
|
|
238
|
-
it "defines Prometheus content type" do
|
|
239
|
-
expect(described_class::CONTENT_TYPE).to eq("text/plain; version=0.0.4")
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
end
|