use super::names;
use crate::error::ErrorClass;
use crate::record::PartitionId;
use metrics::{
Counter, Gauge, Histogram, Key, Label, Level, Metadata, SharedString, counter, gauge,
histogram, with_recorder,
};
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Clone, Debug)]
pub struct ComponentLabels {
pub pipeline: SharedString,
pub component: SharedString,
pub component_type: SharedString,
}
impl ComponentLabels {
pub fn new(
pipeline: impl Into<SharedString>,
component: impl Into<SharedString>,
component_type: impl Into<SharedString>,
) -> Self {
ComponentLabels {
pipeline: pipeline.into(),
component: component.into(),
component_type: component_type.into(),
}
}
pub(crate) fn counter(&self, name: &'static str) -> Counter {
counter!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
)
}
pub(crate) fn counter1(
&self,
name: &'static str,
k: &'static str,
v: impl Into<SharedString>,
) -> Counter {
counter!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
k => v.into(),
)
}
pub(crate) fn counter2(
&self,
name: &'static str,
k1: &'static str,
v1: impl Into<SharedString>,
k2: &'static str,
v2: impl Into<SharedString>,
) -> Counter {
counter!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
k1 => v1.into(),
k2 => v2.into(),
)
}
pub(crate) fn gauge(&self, name: &'static str) -> Gauge {
gauge!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
)
}
pub(crate) fn gauge1(
&self,
name: &'static str,
k: &'static str,
v: impl Into<SharedString>,
) -> Gauge {
gauge!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
k => v.into(),
)
}
pub(crate) fn gauge2(
&self,
name: &'static str,
k1: &'static str,
v1: impl Into<SharedString>,
k2: &'static str,
v2: impl Into<SharedString>,
) -> Gauge {
gauge!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
k1 => v1.into(),
k2 => v2.into(),
)
}
pub(crate) fn histogram(&self, name: &'static str) -> Histogram {
histogram!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
)
}
pub(crate) fn histogram1(
&self,
name: &'static str,
k: &'static str,
v: impl Into<SharedString>,
) -> Histogram {
histogram!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
k => v.into(),
)
}
pub(crate) fn histogram2(
&self,
name: &'static str,
k1: &'static str,
v1: impl Into<SharedString>,
k2: &'static str,
v2: impl Into<SharedString>,
) -> Histogram {
histogram!(name,
names::L_PIPELINE => self.pipeline.clone(),
names::L_COMPONENT => self.component.clone(),
names::L_COMPONENT_TYPE => self.component_type.clone(),
k1 => v1.into(),
k2 => v2.into(),
)
}
fn family_key(&self, name: SharedString, extra: &[(&'static str, SharedString)]) -> Key {
validate_extra_labels(&name, extra);
let mut labels = Vec::with_capacity(3 + extra.len());
labels.push(Label::new(names::L_PIPELINE, self.pipeline.clone()));
labels.push(Label::new(names::L_COMPONENT, self.component.clone()));
labels.push(Label::new(
names::L_COMPONENT_TYPE,
self.component_type.clone(),
));
for (k, v) in extra {
labels.push(Label::new(*k, v.clone()));
}
Key::from_parts(name, labels)
}
pub(crate) fn register_counter(
&self,
name: SharedString,
extra: &[(&'static str, SharedString)],
) -> Counter {
let key = self.family_key(name, extra);
with_recorder(|recorder| recorder.register_counter(&key, &FAMILY_METADATA))
}
pub(crate) fn register_gauge(
&self,
name: SharedString,
extra: &[(&'static str, SharedString)],
) -> Gauge {
let key = self.family_key(name, extra);
with_recorder(|recorder| recorder.register_gauge(&key, &FAMILY_METADATA))
}
pub(crate) fn register_histogram(
&self,
name: SharedString,
extra: &[(&'static str, SharedString)],
) -> Histogram {
let key = self.family_key(name, extra);
with_recorder(|recorder| recorder.register_histogram(&key, &FAMILY_METADATA))
}
}
#[derive(Clone, Debug)]
pub(crate) struct OwnedGauge {
gauge: Gauge,
owned: bool,
}
impl OwnedGauge {
pub(crate) fn new(gauge: Gauge, owned: bool) -> Self {
OwnedGauge { gauge, owned }
}
#[inline]
pub(crate) fn set(&self, value: f64) {
if self.owned {
self.gauge.set(value);
}
}
#[inline]
pub(crate) fn increment(&self, value: f64) {
if self.owned {
self.gauge.increment(value);
}
}
}
const FAMILY_METADATA: Metadata<'static> =
Metadata::new(module_path!(), Level::INFO, Some(module_path!()));
fn validate_extra_labels(name: &str, extra: &[(&'static str, SharedString)]) {
for (i, (k, _)) in extra.iter().enumerate() {
assert!(
*k != names::L_PIPELINE && *k != names::L_COMPONENT && *k != names::L_COMPONENT_TYPE,
"custom label `{k}` on `{name}` shadows a standard label \
(pipeline/component/component_type are attached automatically)"
);
assert!(
!extra[..i].iter().any(|(prev, _)| prev == k),
"custom label `{k}` is repeated on `{name}`"
);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum NamespaceRejection {
Empty,
Malformed,
Reserved,
}
impl NamespaceRejection {
pub(crate) fn reason(self) -> &'static str {
match self {
NamespaceRejection::Empty => "it is empty",
NamespaceRejection::Malformed => "it is not a lowercase `[a-z][a-z0-9_]*` segment",
NamespaceRejection::Reserved => "it is a reserved framework stage root",
}
}
}
pub(crate) fn classify_namespace(namespace: &str) -> Result<(), NamespaceRejection> {
if namespace.is_empty() {
return Err(NamespaceRejection::Empty);
}
let well_formed = namespace
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
&& namespace.as_bytes()[0].is_ascii_lowercase();
if !well_formed {
return Err(NamespaceRejection::Malformed);
}
if names::RESERVED_ROOTS.contains(&namespace) {
return Err(NamespaceRejection::Reserved);
}
Ok(())
}
pub(crate) fn validate_namespace(namespace: &str) {
match classify_namespace(namespace) {
Ok(()) => {}
Err(NamespaceRejection::Empty) => panic!(
"Meter namespace must not be empty (it becomes the `spate_<namespace>_` \
segment on every metric); use `\"custom\"` or your connector's name"
),
Err(NamespaceRejection::Malformed) => panic!(
"Meter namespace `{namespace}` must be a lowercase `[a-z][a-z0-9_]*` \
segment (it becomes part of the `spate_<namespace>_` metric prefix)"
),
Err(NamespaceRejection::Reserved) => panic!(
"Meter namespace `{namespace}` is a reserved framework root; custom \
families would collide with `spate_{namespace}_*`. Use `\"custom\"` or \
a connector segment like `\"kafka\"`."
),
}
}
impl ErrorClass {
pub(crate) fn label(self) -> &'static str {
match self {
ErrorClass::Retryable => "retryable",
ErrorClass::RecordLevel => "record_level",
ErrorClass::Fatal => "fatal",
}
}
}
#[derive(Debug)]
pub(crate) struct PartitionGauges {
pub(crate) name: &'static str,
pub(crate) labels: ComponentLabels,
pub(crate) gauges: Mutex<HashMap<u32, Gauge>>,
pub(crate) owned: bool,
}
impl PartitionGauges {
pub(crate) fn set(&self, partition: PartitionId, value: f64) {
if !self.owned {
return;
}
let mut gauges = self.gauges.lock().expect("partition gauge lock");
gauges
.entry(partition.0)
.or_insert_with(|| {
self.labels
.gauge1(self.name, names::L_PARTITION, partition.0.to_string())
})
.set(value);
}
pub(crate) fn retain(&self, keep: &[PartitionId]) {
let mut gauges = self.gauges.lock().expect("partition gauge lock");
gauges.retain(|p, gauge| {
let kept = keep.iter().any(|k| k.0 == *p);
if !kept {
gauge.set(0.0);
}
kept
});
}
}