1use std::fmt::Write as _;
8
9use nasaga_backend::SagaStoreMetrics;
10
11#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub struct SagaOperationalMetrics {
14 pub started_total: u64,
16 pub completed_total: u64,
18 pub compensated_total: u64,
20 pub manual_intervention_total: u64,
22 pub manually_closed_total: u64,
24 pub quota_rejections_total: u64,
26 pub action_rate_rejections_total: u64,
29 pub unknown_result_total: u64,
31 pub retry_attempt_total: u64,
33 pub conflict_total: u64,
35 pub running_current: u64,
37 pub waiting_resolution_current: u64,
39 pub compensating_current: u64,
41 pub manual_intervention_current: u64,
43 pub due_timer_current: u64,
45 pub lifecycle_duration_count: u64,
47 pub lifecycle_duration_micros_sum: u64,
49 pub lifecycle_quantiles: Vec<nasaga_backend::SagaLifecycleQuantiles>,
51 pub kafka_result_processing_total: u64,
53 pub kafka_result_processing_micros_sum: u64,
55 pub kafka_result_retry_total: u64,
57 pub kafka_result_dlt_requested_total: u64,
59 pub kafka_result_ack_total: u64,
61 pub kafka_result_duplicate_total: u64,
63 pub kafka_command_processing_total: u64,
65 pub kafka_command_processing_micros_sum: u64,
67 pub kafka_command_retry_total: u64,
69 pub kafka_command_dlt_requested_total: u64,
71 pub kafka_command_ack_total: u64,
73 pub kafka_command_duplicate_total: u64,
75}
76
77impl SagaOperationalMetrics {
78 pub fn render_prometheus(&self) -> String {
84 let mut output = String::with_capacity(2_048);
85 counter(&mut output, "nasaga_started_total", self.started_total);
86 counter(&mut output, "nasaga_completed_total", self.completed_total);
87 counter(
88 &mut output,
89 "nasaga_compensated_total",
90 self.compensated_total,
91 );
92 counter(
93 &mut output,
94 "nasaga_manual_intervention_total",
95 self.manual_intervention_total,
96 );
97 counter(
98 &mut output,
99 "nasaga_manually_closed_total",
100 self.manually_closed_total,
101 );
102 counter(
103 &mut output,
104 "nasaga_quota_rejections_total",
105 self.quota_rejections_total,
106 );
107 counter(
108 &mut output,
109 "nasaga_action_rate_rejections_total",
110 self.action_rate_rejections_total,
111 );
112 counter(
113 &mut output,
114 "nasaga_unknown_result_total",
115 self.unknown_result_total,
116 );
117 counter(
118 &mut output,
119 "nasaga_retry_attempt_total",
120 self.retry_attempt_total,
121 );
122 counter(&mut output, "nasaga_conflict_total", self.conflict_total);
123 gauge(&mut output, "nasaga_running", self.running_current);
124 gauge(
125 &mut output,
126 "nasaga_waiting_resolution",
127 self.waiting_resolution_current,
128 );
129 gauge(
130 &mut output,
131 "nasaga_compensating",
132 self.compensating_current,
133 );
134 gauge(
135 &mut output,
136 "nasaga_manual_intervention",
137 self.manual_intervention_current,
138 );
139 gauge(&mut output, "nasaga_due_timer", self.due_timer_current);
140 summary(
141 &mut output,
142 "nasaga_lifecycle_duration_seconds",
143 self.lifecycle_duration_count,
144 self.lifecycle_duration_micros_sum,
145 );
146 for quantiles in &self.lifecycle_quantiles {
147 labeled_gauge(
148 &mut output,
149 "nasaga_lifecycle_duration_seconds",
150 &quantiles.workflow,
151 quantiles.definition_version,
152 "0.5",
153 quantiles.p50_micros,
154 );
155 labeled_gauge(
156 &mut output,
157 "nasaga_lifecycle_duration_seconds",
158 &quantiles.workflow,
159 quantiles.definition_version,
160 "0.95",
161 quantiles.p95_micros,
162 );
163 labeled_gauge(
164 &mut output,
165 "nasaga_lifecycle_duration_seconds",
166 &quantiles.workflow,
167 quantiles.definition_version,
168 "0.99",
169 quantiles.p99_micros,
170 );
171 }
172 summary(
173 &mut output,
174 "nasaga_kafka_result_processing_duration_seconds",
175 self.kafka_result_processing_total,
176 self.kafka_result_processing_micros_sum,
177 );
178 counter(
179 &mut output,
180 "nasaga_kafka_result_retry_total",
181 self.kafka_result_retry_total,
182 );
183 counter(
184 &mut output,
185 "nasaga_kafka_result_dlt_requested_total",
186 self.kafka_result_dlt_requested_total,
187 );
188 counter(
189 &mut output,
190 "nasaga_kafka_result_ack_total",
191 self.kafka_result_ack_total,
192 );
193 counter(
194 &mut output,
195 "nasaga_kafka_result_duplicate_total",
196 self.kafka_result_duplicate_total,
197 );
198 summary(
199 &mut output,
200 "nasaga_kafka_command_processing_duration_seconds",
201 self.kafka_command_processing_total,
202 self.kafka_command_processing_micros_sum,
203 );
204 counter(
205 &mut output,
206 "nasaga_kafka_command_retry_total",
207 self.kafka_command_retry_total,
208 );
209 counter(
210 &mut output,
211 "nasaga_kafka_command_dlt_requested_total",
212 self.kafka_command_dlt_requested_total,
213 );
214 counter(
215 &mut output,
216 "nasaga_kafka_command_ack_total",
217 self.kafka_command_ack_total,
218 );
219 counter(
220 &mut output,
221 "nasaga_kafka_command_duplicate_total",
222 self.kafka_command_duplicate_total,
223 );
224 output.push_str(&crate::render_saga_latency_metrics());
225 output
226 }
227}
228
229impl From<SagaStoreMetrics> for SagaOperationalMetrics {
230 fn from(store: SagaStoreMetrics) -> Self {
237 let process = crate::process_metrics_snapshot();
238 Self {
239 started_total: store.started_total,
240 completed_total: store.completed_total,
241 compensated_total: store.compensated_total,
242 manual_intervention_total: store.manual_intervention_total,
243 manually_closed_total: store.manually_closed_total,
244 quota_rejections_total: process.quota_rejections_total,
245 action_rate_rejections_total: process.action_rate_rejections_total,
246 unknown_result_total: store.unknown_result_total,
247 retry_attempt_total: store.retry_attempt_total,
248 conflict_total: store.conflict_total,
249 running_current: store.running_current,
250 waiting_resolution_current: store.waiting_resolution_current,
251 compensating_current: store.compensating_current,
252 manual_intervention_current: store.manual_intervention_current,
253 due_timer_current: store.due_timer_current,
254 lifecycle_duration_count: store.lifecycle_duration_count,
255 lifecycle_duration_micros_sum: store.lifecycle_duration_micros_sum,
256 lifecycle_quantiles: store.lifecycle_quantiles,
257 kafka_result_processing_total: process.kafka_result_processing_total,
258 kafka_result_processing_micros_sum: process.kafka_result_processing_micros_sum,
259 kafka_result_retry_total: process.kafka_result_retry_total,
260 kafka_result_dlt_requested_total: process.kafka_result_dlt_requested_total,
261 kafka_result_ack_total: process.kafka_result_ack_total,
262 kafka_result_duplicate_total: process.kafka_result_duplicate_total,
263 kafka_command_processing_total: process.kafka_command_processing_total,
264 kafka_command_processing_micros_sum: process.kafka_command_processing_micros_sum,
265 kafka_command_retry_total: process.kafka_command_retry_total,
266 kafka_command_dlt_requested_total: process.kafka_command_dlt_requested_total,
267 kafka_command_ack_total: process.kafka_command_ack_total,
268 kafka_command_duplicate_total: process.kafka_command_duplicate_total,
269 }
270 }
271}
272
273fn labeled_gauge(
280 output: &mut String,
281 name: &str,
282 workflow: &str,
283 version: u32,
284 quantile: &str,
285 micros: u64,
286) {
287 let workflow = workflow.replace('\\', "\\\\").replace('"', "\\\"");
288 let _ = writeln!(
289 output,
290 "{name}{{workflow=\"{workflow}\",definition_version=\"{version}\",quantile=\"{quantile}\"}} {}",
291 micros as f64 / 1_000_000.0
292 );
293}
294
295pub(crate) fn record_quota_rejection() {
301 crate::record_quota_rejection();
302}
303
304pub(crate) fn record_action_rate_rejection() {
310 crate::record_action_rate_rejection();
311}
312
313fn counter(output: &mut String, name: &str, value: u64) {
315 let _ = writeln!(output, "# TYPE {name} counter\n{name} {value}");
316}
317
318fn gauge(output: &mut String, name: &str, value: u64) {
320 let _ = writeln!(output, "# TYPE {name} gauge\n{name} {value}");
321}
322
323fn summary(output: &mut String, name: &str, count: u64, micros_sum: u64) {
325 let seconds_sum = micros_sum as f64 / 1_000_000.0;
326 let _ = writeln!(
327 output,
328 "# TYPE {name} summary\n{name}_count {count}\n{name}_sum {seconds_sum:.6}"
329 );
330}