1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::{error::PoolsimError, types::WorkloadConfig};
11
12pub const DEFAULT_RPS_METRIC: &str = "poolsim.rps";
14pub const DEFAULT_P50_METRIC: &str = "poolsim.latency.p50_ms";
16pub const DEFAULT_P95_METRIC: &str = "poolsim.latency.p95_ms";
18pub const DEFAULT_P99_METRIC: &str = "poolsim.latency.p99_ms";
20
21fn default_rps_metric() -> String {
22 DEFAULT_RPS_METRIC.to_string()
23}
24
25fn default_p50_metric() -> String {
26 DEFAULT_P50_METRIC.to_string()
27}
28
29fn default_p95_metric() -> String {
30 DEFAULT_P95_METRIC.to_string()
31}
32
33fn default_p99_metric() -> String {
34 DEFAULT_P99_METRIC.to_string()
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub struct OtlpMetricNames {
40 #[serde(default = "default_rps_metric")]
42 pub rps_metric: String,
43 #[serde(default = "default_p50_metric")]
45 pub p50_metric: String,
46 #[serde(default = "default_p95_metric")]
48 pub p95_metric: String,
49 #[serde(default = "default_p99_metric")]
51 pub p99_metric: String,
52}
53
54impl Default for OtlpMetricNames {
55 fn default() -> Self {
56 Self {
57 rps_metric: default_rps_metric(),
58 p50_metric: default_p50_metric(),
59 p95_metric: default_p95_metric(),
60 p99_metric: default_p99_metric(),
61 }
62 }
63}
64
65pub fn workload_from_otlp_json(
72 root: &Value,
73 names: &OtlpMetricNames,
74) -> Result<WorkloadConfig, PoolsimError> {
75 Ok(WorkloadConfig {
76 requests_per_second: metric_value(root, &names.rps_metric)?,
77 latency_p50_ms: metric_value(root, &names.p50_metric)?,
78 latency_p95_ms: metric_value(root, &names.p95_metric)?,
79 latency_p99_ms: metric_value(root, &names.p99_metric)?,
80 raw_samples_ms: None,
81 step_load_profile: None,
82 })
83}
84
85pub fn metric_value(root: &Value, name: &str) -> Result<f64, PoolsimError> {
92 find_metric(root, name)
93 .and_then(first_numeric)
94 .ok_or_else(|| {
95 PoolsimError::invalid_input(
96 "OTLP_METRIC_NOT_FOUND",
97 format!("OTLP metric {name:?} was not found or had no numeric datapoint"),
98 Some(serde_json::json!({ "metric": name })),
99 )
100 })
101}
102
103fn find_metric<'a>(value: &'a Value, name: &str) -> Option<&'a Value> {
104 match value {
105 Value::Object(map) => {
106 if map.get("name").and_then(Value::as_str) == Some(name) {
107 return Some(value);
108 }
109 map.values().find_map(|child| find_metric(child, name))
110 }
111 Value::Array(items) => items.iter().find_map(|child| find_metric(child, name)),
112 _ => None,
113 }
114}
115
116fn first_numeric(value: &Value) -> Option<f64> {
117 match value {
118 Value::Number(number) => number.as_f64(),
119 Value::String(text) => text.parse().ok(),
120 Value::Object(map) => {
121 for key in ["asDouble", "asInt", "doubleValue", "intValue", "value"] {
122 if let Some(number) = map.get(key).and_then(first_numeric) {
123 return Some(number);
124 }
125 }
126 for key in ["sum", "gauge", "histogram", "dataPoints"] {
127 if let Some(number) = map.get(key).and_then(first_numeric) {
128 return Some(number);
129 }
130 }
131 map.values().find_map(first_numeric)
132 }
133 Value::Array(items) => items.iter().find_map(first_numeric),
134 _ => None,
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn fixture() -> Value {
143 serde_json::json!({
144 "resourceMetrics": [{
145 "scopeMetrics": [{
146 "metrics": [
147 {"name": "poolsim.rps", "sum": {"dataPoints": [{"asDouble": 180.0}]}},
148 {"name": "poolsim.latency.p50_ms", "gauge": {"dataPoints": [{"asDouble": 8.0}]}},
149 {"name": "poolsim.latency.p95_ms", "gauge": {"dataPoints": [{"asInt": "30"}]}},
150 {"name": "poolsim.latency.p99_ms", "gauge": {"dataPoints": [{"value": 70.0}]}}
151 ]
152 }]
153 }]
154 })
155 }
156
157 #[test]
158 fn extracts_workload_from_otlp_json() {
159 let workload = workload_from_otlp_json(&fixture(), &OtlpMetricNames::default())
160 .expect("OTLP workload should resolve");
161 assert_eq!(workload.requests_per_second, 180.0);
162 assert_eq!(workload.latency_p95_ms, 30.0);
163 assert_eq!(workload.latency_p99_ms, 70.0);
164 }
165
166 #[test]
167 fn reports_missing_metric_with_stable_error_code() {
168 let err = metric_value(&serde_json::json!({ "metrics": [] }), "missing")
169 .expect_err("missing metric should fail");
170 assert_eq!(err.code(), "OTLP_METRIC_NOT_FOUND");
171 }
172}