feat(controller/reporting): reconcile-rspecific measurements & prometheus metric retains tracing messages

This commit is contained in:
spushk 2026-05-15 14:47:35 +05:00 • committed by spushk
commit 683869982f
Signed by: spushk
GPG key ID: 7797B761E105E8AA
3 changed files with 196 additions and 17 deletions

View file

@ -1,2 +1,24 @@
use confique::Config;
pub mod prometheus;
pub mod tracing_evt;
#[derive(Config)]
pub struct ReportingConfig {
#[config(env = "KUYASEG_REPORT_K8S")]
#[config(default = true)]
pub k8s_events: bool,
#[config(env = "KUYASEG_REPORT_PROMETHEUS")]
#[config(default = false)]
pub prometheus: bool,
#[config(env = "KUYASEG_REPORT_OTEL")]
#[config(default = false)]
pub opentelemetry: bool,
}
pub fn trace_id_str(trace_id: &opentelemetry::TraceId) -> Option<String> {
match trace_id {
&opentelemetry::TraceId::INVALID => None,
x => Some(x.to_string()),
}
}

View file

@ -1,30 +1,120 @@
use std::time::{
Instant,
SystemTime,
};
use opentelemetry::trace::TraceContextExt;
use prometheus_client::encoding::{
EncodeLabelSet,
EncodeLabelValue,
};
use prometheus_client::metrics::counter::Counter;
use prometheus_client::metrics::exemplar::{
CounterWithExemplar,
HistogramWithExemplars,
};
use prometheus_client::metrics::family::Family;
use prometheus_client::registry::Registry;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use crate::reporting::trace_id_str;
#[derive(Default, Clone)]
pub struct ProMetrics {
k8s_metrics: Family<ProK8sLabel, Counter>,
k8s_metrics: Family<ProK8sLabel, CounterWithExemplar<ProK8sExemplar>>,
reconcile_metrics: ProReconcileMetrics,
}
/// Will not register unless told to with `register`
impl ProMetrics {
pub fn new() -> Self {
Self {
k8s_metrics: Default::default(),
reconcile_metrics: Default::default(),
}
}
/// Measure labelled evt
pub fn measure(&self, label: &ProLabel) {
match label {
ProLabel::K8s(label) => {
self.k8s_metrics.get_or_create(label).inc();
ProLabel::Generic { label, exemplar } => {
self.k8s_metrics
.get_or_create(label)
.inc_by(1, exemplar.clone(), Some(SystemTime::now()));
}
}
}
/// Start measuring reconcile duration. Registers on `TimedMeasurer` drop
pub fn measure_reconcilation(&self) -> TimedMeasurer {
self.reconcile_metrics.measure()
}
pub fn register(&self, registry: &mut Registry) {
registry.register("events", "General event counters", self.k8s_metrics.clone());
registry.register(
"reconcile_duration",
"Reconcile durations",
self.reconcile_metrics.duration.clone(),
);
registry.register("reconcile_runs", "Reconcilations", self.reconcile_metrics.runs.clone());
}
}
#[derive(Clone)]
pub struct ProReconcileMetrics {
runs: Counter,
duration: HistogramWithExemplars<TraceLabel>,
}
impl ProReconcileMetrics {
pub fn measure(&self) -> TimedMeasurer {
self.runs.inc();
let trace_id = tracing::Span::current().context().span().span_context().trace_id();
let label = trace_id_str(&trace_id).map(|trace_id| TraceLabel { trace_id });
TimedMeasurer {
label,
start: Instant::now(),
metric: self.duration.clone(),
}
}
}
pub enum ProLabel {
K8s(ProK8sLabel),
impl Default for ProReconcileMetrics {
fn default() -> Self {
Self {
runs: Default::default(),
duration: HistogramWithExemplars::new([0.01, 0.1, 0.25, 0.5, 1., 5., 15., 60.].into_iter()),
}
}
}
#[derive(Clone, Hash, PartialEq, Eq, EncodeLabelSet)]
/// Reports timing on drop
pub struct TimedMeasurer {
start: Instant,
label: Option<TraceLabel>,
metric: HistogramWithExemplars<TraceLabel>,
}
impl Drop for TimedMeasurer {
fn drop(&mut self) {
let duration_f = self.start.elapsed().as_secs_f64();
let label = self.label.take();
self.metric.observe(duration_f, label, Some(SystemTime::now()));
}
}
pub enum ProLabel {
/// Generic event to be registered as labels
Generic {
label: ProK8sLabel,
exemplar: Option<ProK8sExemplar>,
},
}
/// General purpose label
#[derive(Debug, Clone, Hash, PartialEq, Eq, EncodeLabelSet)]
pub struct ProK8sLabel {
pub severity: ProK8sSeverity,
pub reason: String,
@ -33,7 +123,7 @@ pub struct ProK8sLabel {
pub namespace: Option<String>,
}
#[derive(Clone, Hash, PartialEq, Eq, EncodeLabelValue)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, EncodeLabelValue)]
#[allow(non_camel_case_types)] // verbatim label value encoding must match k8s standard
pub enum ProK8sSeverity {
critical,
@ -42,3 +132,15 @@ pub enum ProK8sSeverity {
info,
debug,
}
/// General purpose counter exemplar
#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, EncodeLabelSet)]
pub struct ProK8sExemplar {
pub description: Option<String>,
pub trace_id: Option<String>,
}
#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, EncodeLabelSet)]
pub struct TraceLabel {
pub trace_id: String,
}

View file

@ -13,16 +13,19 @@ use tracing::{
Id,
Subscriber,
};
use tracing_opentelemetry::OtelData;
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
use crate::reporting::prometheus::{
ProK8sExemplar,
ProK8sLabel,
ProK8sSeverity,
ProLabel,
ProMetrics,
};
use crate::reporting::trace_id_str;
/// - k8s events are not automatically recorded
/// - prometheus labels are recorded if metrics are set for layer
@ -36,6 +39,11 @@ pub enum TracingK8sEvent {
},
}
pub struct TracingK8sEventLayerConfig {
pub enable_k8s_events: bool,
pub enable_prometheus_metrics: bool,
}
/// Watches for tracing events, containing information necessary to construst k8s events and write prometheus metrics.
///
/// - k8s native events you must record yourself - thats what `tx` is for.
@ -43,13 +51,19 @@ pub enum TracingK8sEvent {
/// - k8s events are NOT rate-limited. K8s API rejects >10 events/object/min.
/// Channel consumer must batch/dedup to avoid API errors.
pub struct TracingK8sEventLayer {
config: TracingK8sEventLayerConfig,
tx: RwLock<mpsc::UnboundedSender<TracingK8sEvent>>,
metrics: RwLock<Option<ProMetrics>>,
}
impl TracingK8sEventLayer {
pub fn new(tx: mpsc::UnboundedSender<TracingK8sEvent>, metrics: Option<ProMetrics>) -> Self {
pub fn new(
config: TracingK8sEventLayerConfig,
tx: mpsc::UnboundedSender<TracingK8sEvent>,
metrics: Option<ProMetrics>,
) -> Self {
Self {
config,
tx: RwLock::new(tx),
metrics: RwLock::new(metrics),
}
@ -121,11 +135,18 @@ where
tracing::Level::INFO => (EventType::Normal, ProK8sSeverity::info),
_ => (EventType::Normal, ProK8sSeverity::debug),
};
// gather trace_id
let otel_trace_id = ctx
.event_span(event)
.and_then(|span| span.extensions().get::<OtelData>().and_then(|dat| dat.trace_id()));
let str_trace_id = otel_trace_id.as_ref().and_then(trace_id_str);
let tx = self.tx.read();
// write k8s event
if let Some(event) = event_fields.try_to_event(k8s_event_type) {
if self.config.enable_k8s_events
&& let Some(event) = event_fields.try_to_event(k8s_event_type)
{
_ = tx
.send(TracingK8sEvent::K8sEvent {
reference: span_fields.to_object(),
@ -135,7 +156,9 @@ where
}
// write/measure pro event
if let Some(label) = try_create_pro_label(&span_fields, &event_fields, pro_severity) {
if self.config.enable_prometheus_metrics
&& let Some(label) = try_create_pro_label(&span_fields, &event_fields, pro_severity, str_trace_id)
{
if let Some(metrics) = self.metrics.read().as_ref() {
metrics.measure(&label);
}
@ -286,12 +309,19 @@ fn try_create_pro_label(
span_fields: &SpanFields,
event_fields: &EventFields,
severity: ProK8sSeverity,
trace_id: Option<String>,
) -> Option<ProLabel> {
Some(ProLabel::K8s(ProK8sLabel {
severity,
reason: event_fields.reason.clone()?,
name: span_fields.name.clone()?,
kind: span_fields.kind.clone()?,
namespace: span_fields.ns.clone(),
}))
Some(ProLabel::Generic {
label: ProK8sLabel {
severity,
reason: event_fields.reason.clone()?,
name: span_fields.name.clone()?,
kind: span_fields.kind.clone()?,
namespace: span_fields.ns.clone(),
},
exemplar: Some(ProK8sExemplar {
description: event_fields.message.clone(),
trace_id,
}),
})
}