1use super::names;
13use crate::error::ErrorClass;
14use crate::record::PartitionId;
15use metrics::{
16 Counter, Gauge, Histogram, Key, Label, Level, Metadata, SharedString, counter, gauge,
17 histogram, with_recorder,
18};
19use std::collections::HashMap;
20use std::sync::Mutex;
21
22#[derive(Clone, Debug)]
24pub struct ComponentLabels {
25 pub pipeline: SharedString,
27 pub component: SharedString,
29 pub component_type: SharedString,
31}
32
33impl ComponentLabels {
34 pub fn new(
36 pipeline: impl Into<SharedString>,
37 component: impl Into<SharedString>,
38 component_type: impl Into<SharedString>,
39 ) -> Self {
40 ComponentLabels {
41 pipeline: pipeline.into(),
42 component: component.into(),
43 component_type: component_type.into(),
44 }
45 }
46
47 pub(crate) fn counter(&self, name: &'static str) -> Counter {
48 counter!(name,
49 names::L_PIPELINE => self.pipeline.clone(),
50 names::L_COMPONENT => self.component.clone(),
51 names::L_COMPONENT_TYPE => self.component_type.clone(),
52 )
53 }
54
55 pub(crate) fn counter1(
56 &self,
57 name: &'static str,
58 k: &'static str,
59 v: impl Into<SharedString>,
60 ) -> Counter {
61 counter!(name,
62 names::L_PIPELINE => self.pipeline.clone(),
63 names::L_COMPONENT => self.component.clone(),
64 names::L_COMPONENT_TYPE => self.component_type.clone(),
65 k => v.into(),
66 )
67 }
68
69 pub(crate) fn counter2(
70 &self,
71 name: &'static str,
72 k1: &'static str,
73 v1: impl Into<SharedString>,
74 k2: &'static str,
75 v2: impl Into<SharedString>,
76 ) -> Counter {
77 counter!(name,
78 names::L_PIPELINE => self.pipeline.clone(),
79 names::L_COMPONENT => self.component.clone(),
80 names::L_COMPONENT_TYPE => self.component_type.clone(),
81 k1 => v1.into(),
82 k2 => v2.into(),
83 )
84 }
85
86 pub(crate) fn gauge(&self, name: &'static str) -> Gauge {
87 gauge!(name,
88 names::L_PIPELINE => self.pipeline.clone(),
89 names::L_COMPONENT => self.component.clone(),
90 names::L_COMPONENT_TYPE => self.component_type.clone(),
91 )
92 }
93
94 pub(crate) fn gauge1(
95 &self,
96 name: &'static str,
97 k: &'static str,
98 v: impl Into<SharedString>,
99 ) -> Gauge {
100 gauge!(name,
101 names::L_PIPELINE => self.pipeline.clone(),
102 names::L_COMPONENT => self.component.clone(),
103 names::L_COMPONENT_TYPE => self.component_type.clone(),
104 k => v.into(),
105 )
106 }
107
108 pub(crate) fn gauge2(
109 &self,
110 name: &'static str,
111 k1: &'static str,
112 v1: impl Into<SharedString>,
113 k2: &'static str,
114 v2: impl Into<SharedString>,
115 ) -> Gauge {
116 gauge!(name,
117 names::L_PIPELINE => self.pipeline.clone(),
118 names::L_COMPONENT => self.component.clone(),
119 names::L_COMPONENT_TYPE => self.component_type.clone(),
120 k1 => v1.into(),
121 k2 => v2.into(),
122 )
123 }
124
125 pub(crate) fn histogram(&self, name: &'static str) -> Histogram {
126 histogram!(name,
127 names::L_PIPELINE => self.pipeline.clone(),
128 names::L_COMPONENT => self.component.clone(),
129 names::L_COMPONENT_TYPE => self.component_type.clone(),
130 )
131 }
132
133 pub(crate) fn histogram1(
134 &self,
135 name: &'static str,
136 k: &'static str,
137 v: impl Into<SharedString>,
138 ) -> Histogram {
139 histogram!(name,
140 names::L_PIPELINE => self.pipeline.clone(),
141 names::L_COMPONENT => self.component.clone(),
142 names::L_COMPONENT_TYPE => self.component_type.clone(),
143 k => v.into(),
144 )
145 }
146
147 pub(crate) fn histogram2(
148 &self,
149 name: &'static str,
150 k1: &'static str,
151 v1: impl Into<SharedString>,
152 k2: &'static str,
153 v2: impl Into<SharedString>,
154 ) -> Histogram {
155 histogram!(name,
156 names::L_PIPELINE => self.pipeline.clone(),
157 names::L_COMPONENT => self.component.clone(),
158 names::L_COMPONENT_TYPE => self.component_type.clone(),
159 k1 => v1.into(),
160 k2 => v2.into(),
161 )
162 }
163
164 fn family_key(&self, name: SharedString, extra: &[(&'static str, SharedString)]) -> Key {
174 validate_extra_labels(&name, extra);
175 let mut labels = Vec::with_capacity(3 + extra.len());
176 labels.push(Label::new(names::L_PIPELINE, self.pipeline.clone()));
177 labels.push(Label::new(names::L_COMPONENT, self.component.clone()));
178 labels.push(Label::new(
179 names::L_COMPONENT_TYPE,
180 self.component_type.clone(),
181 ));
182 for (k, v) in extra {
183 labels.push(Label::new(*k, v.clone()));
184 }
185 Key::from_parts(name, labels)
186 }
187
188 pub(crate) fn register_counter(
192 &self,
193 name: SharedString,
194 extra: &[(&'static str, SharedString)],
195 ) -> Counter {
196 let key = self.family_key(name, extra);
197 with_recorder(|recorder| recorder.register_counter(&key, &FAMILY_METADATA))
198 }
199
200 pub(crate) fn register_gauge(
202 &self,
203 name: SharedString,
204 extra: &[(&'static str, SharedString)],
205 ) -> Gauge {
206 let key = self.family_key(name, extra);
207 with_recorder(|recorder| recorder.register_gauge(&key, &FAMILY_METADATA))
208 }
209
210 pub(crate) fn register_histogram(
212 &self,
213 name: SharedString,
214 extra: &[(&'static str, SharedString)],
215 ) -> Histogram {
216 let key = self.family_key(name, extra);
217 with_recorder(|recorder| recorder.register_histogram(&key, &FAMILY_METADATA))
218 }
219}
220
221#[derive(Clone, Debug)]
232pub(crate) struct OwnedGauge {
233 gauge: Gauge,
234 owned: bool,
235}
236
237impl OwnedGauge {
238 pub(crate) fn new(gauge: Gauge, owned: bool) -> Self {
239 OwnedGauge { gauge, owned }
240 }
241
242 #[inline]
243 pub(crate) fn set(&self, value: f64) {
244 if self.owned {
245 self.gauge.set(value);
246 }
247 }
248
249 #[inline]
250 pub(crate) fn increment(&self, value: f64) {
251 if self.owned {
252 self.gauge.increment(value);
253 }
254 }
255}
256
257const FAMILY_METADATA: Metadata<'static> =
263 Metadata::new(module_path!(), Level::INFO, Some(module_path!()));
264
265fn validate_extra_labels(name: &str, extra: &[(&'static str, SharedString)]) {
271 for (i, (k, _)) in extra.iter().enumerate() {
272 assert!(
273 *k != names::L_PIPELINE && *k != names::L_COMPONENT && *k != names::L_COMPONENT_TYPE,
274 "custom label `{k}` on `{name}` shadows a standard label \
275 (pipeline/component/component_type are attached automatically)"
276 );
277 assert!(
278 !extra[..i].iter().any(|(prev, _)| prev == k),
279 "custom label `{k}` is repeated on `{name}`"
280 );
281 }
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290pub(crate) enum NamespaceRejection {
291 Empty,
293 Malformed,
297 Reserved,
300}
301
302impl NamespaceRejection {
303 pub(crate) fn reason(self) -> &'static str {
305 match self {
306 NamespaceRejection::Empty => "it is empty",
307 NamespaceRejection::Malformed => "it is not a lowercase `[a-z][a-z0-9_]*` segment",
308 NamespaceRejection::Reserved => "it is a reserved framework stage root",
309 }
310 }
311}
312
313pub(crate) fn classify_namespace(namespace: &str) -> Result<(), NamespaceRejection> {
320 if namespace.is_empty() {
321 return Err(NamespaceRejection::Empty);
322 }
323 let well_formed = namespace
324 .bytes()
325 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
326 && namespace.as_bytes()[0].is_ascii_lowercase();
327 if !well_formed {
328 return Err(NamespaceRejection::Malformed);
329 }
330 if names::RESERVED_ROOTS.contains(&namespace) {
331 return Err(NamespaceRejection::Reserved);
332 }
333 Ok(())
334}
335
336pub(crate) fn validate_namespace(namespace: &str) {
340 match classify_namespace(namespace) {
341 Ok(()) => {}
342 Err(NamespaceRejection::Empty) => panic!(
343 "Meter namespace must not be empty (it becomes the `spate_<namespace>_` \
344 segment on every metric); use `\"custom\"` or your connector's name"
345 ),
346 Err(NamespaceRejection::Malformed) => panic!(
347 "Meter namespace `{namespace}` must be a lowercase `[a-z][a-z0-9_]*` \
348 segment (it becomes part of the `spate_<namespace>_` metric prefix)"
349 ),
350 Err(NamespaceRejection::Reserved) => panic!(
351 "Meter namespace `{namespace}` is a reserved framework root; custom \
352 families would collide with `spate_{namespace}_*`. Use `\"custom\"` or \
353 a connector segment like `\"kafka\"`."
354 ),
355 }
356}
357
358impl ErrorClass {
359 pub(crate) fn label(self) -> &'static str {
360 match self {
361 ErrorClass::Retryable => "retryable",
362 ErrorClass::RecordLevel => "record_level",
363 ErrorClass::Fatal => "fatal",
364 }
365 }
366}
367
368#[derive(Debug)]
372pub(crate) struct PartitionGauges {
373 pub(crate) name: &'static str,
374 pub(crate) labels: ComponentLabels,
375 pub(crate) gauges: Mutex<HashMap<u32, Gauge>>,
376 pub(crate) owned: bool,
379}
380
381impl PartitionGauges {
382 pub(crate) fn set(&self, partition: PartitionId, value: f64) {
383 if !self.owned {
384 return;
385 }
386 let mut gauges = self.gauges.lock().expect("partition gauge lock");
387 gauges
388 .entry(partition.0)
389 .or_insert_with(|| {
390 self.labels
391 .gauge1(self.name, names::L_PARTITION, partition.0.to_string())
392 })
393 .set(value);
394 }
395
396 pub(crate) fn retain(&self, keep: &[PartitionId]) {
409 let mut gauges = self.gauges.lock().expect("partition gauge lock");
410 gauges.retain(|p, gauge| {
411 let kept = keep.iter().any(|k| k.0 == *p);
412 if !kept {
413 gauge.set(0.0);
414 }
415 kept
416 });
417 }
418}