Skip to main content

metrics_exporter_tracing/
lib.rs

1//! A `metrics` recorder that emits metric updates as `tracing` events.
2//!
3//! This crate is intended for developer-focused debugging and lightweight
4//! visibility during development. Metrics are emitted synchronously on the
5//! metrics call path as structured `tracing` events.
6
7#[cfg(feature = "prometheus")]
8mod prometheus;
9#[cfg(feature = "prometheus")]
10use prometheus_client::encoding::prometheus_protobuf::prometheus_data_model::MetricFamily;
11
12use std::fmt;
13use std::sync::atomic::{AtomicU8, Ordering};
14use std::sync::{Arc, OnceLock};
15
16use dashmap::{DashMap, DashSet};
17use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
18use tracing::callsite::{self, Callsite};
19use tracing::field::{self, FieldSet, Value};
20use tracing::metadata;
21use tracing::subscriber::Interest;
22use tracing::{Event, Level as TracingLevel, Metadata as TracingMetadata};
23
24const DEFAULT_TARGET: &str = "metrics";
25const EVENT_NAME: &str = "metric";
26
27/// A `metrics` recorder that emits `tracing` events.
28#[derive(Clone)]
29pub struct TracingRecorder {
30    inner: Arc<Inner>,
31}
32
33impl TracingRecorder {
34    /// Creates a recorder with defaults: level TRACE and target "metrics".
35    pub fn new() -> Self {
36        Self::builder().build()
37    }
38
39    /// Emits counter/gauge values and histogram aggregates; other types are ignored.
40    /// Uses the configured default target and level. Aggregates emit
41    /// `event = "snapshot"` instead of observation events.
42    #[cfg(feature = "prometheus")]
43    pub fn emit_snapshot(&self, metrics: &[MetricFamily]) {
44        prometheus::emit(self, metrics);
45    }
46
47    #[cfg(feature = "prometheus")]
48    fn emit_statistic(&self, key: &Key, kind: &str, statistic: &str, value: &dyn Value) {
49        let event = "snapshot";
50        let name = key.name();
51        let labels = field::debug(LabelsDebug::from_key(key));
52        let values: [Option<&dyn Value>; 6] = [
53            Some(&event),
54            Some(&name),
55            Some(&kind),
56            Some(&labels),
57            Some(&statistic),
58            Some(value),
59        ];
60        self.inner.dispatch_event(
61            Schema::Snapshot,
62            &self.inner.default_target,
63            self.inner.default_level,
64            &values,
65        );
66    }
67
68    /// Returns a builder to configure a [`TracingRecorder`].
69    pub fn builder() -> Builder {
70        Builder::default()
71    }
72}
73
74impl Default for TracingRecorder {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80/// Builder for [`TracingRecorder`].
81#[derive(Clone, Debug)]
82pub struct Builder {
83    default_level: TracingLevel,
84    default_target: Arc<str>,
85}
86
87impl Default for Builder {
88    fn default() -> Self {
89        Self {
90            default_level: TracingLevel::TRACE,
91            default_target: Arc::from(DEFAULT_TARGET),
92        }
93    }
94}
95
96impl Builder {
97    /// Sets the default tracing level for emitted events.
98    pub fn default_level(mut self, level: TracingLevel) -> Self {
99        self.default_level = level;
100        self
101    }
102
103    /// Sets the default target for emitted events.
104    pub fn default_target<T: Into<String>>(mut self, target: T) -> Self {
105        self.default_target = Arc::from(target.into());
106        self
107    }
108
109    /// Builds a [`TracingRecorder`].
110    pub fn build(self) -> TracingRecorder {
111        TracingRecorder {
112            inner: Arc::new(Inner {
113                default_level: self.default_level,
114                default_target: self.default_target,
115                described: DashSet::new(),
116                callsites: DashMap::new(),
117            }),
118        }
119    }
120}
121
122impl Recorder for TracingRecorder {
123    fn describe_counter(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
124        self.inner
125            .describe(MetricKind::Counter, key, unit, description);
126    }
127
128    fn describe_gauge(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
129        self.inner
130            .describe(MetricKind::Gauge, key, unit, description);
131    }
132
133    fn describe_histogram(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
134        self.inner
135            .describe(MetricKind::Histogram, key, unit, description);
136    }
137
138    fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
139        let target = self.inner.resolve_target(metadata.target());
140        let level = map_level(*metadata.level());
141        let handle = CounterHandle {
142            inner: Arc::clone(&self.inner),
143            key: key.clone(),
144            target,
145            level,
146        };
147        Counter::from_arc(Arc::new(handle))
148    }
149
150    fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
151        let target = self.inner.resolve_target(metadata.target());
152        let level = map_level(*metadata.level());
153        let handle = GaugeHandle {
154            inner: Arc::clone(&self.inner),
155            key: key.clone(),
156            target,
157            level,
158        };
159        Gauge::from_arc(Arc::new(handle))
160    }
161
162    fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
163        let target = self.inner.resolve_target(metadata.target());
164        let level = map_level(*metadata.level());
165        let handle = HistogramHandle {
166            inner: Arc::clone(&self.inner),
167            key: key.clone(),
168            target,
169            level,
170        };
171        Histogram::from_arc(Arc::new(handle))
172    }
173}
174
175// Shared recorder state: defaults, describe de-dup, and callsite cache.
176struct Inner {
177    default_level: TracingLevel,
178    default_target: Arc<str>,
179    described: DashSet<SeenKey>,
180    callsites: DashMap<CallsiteKey, &'static DynamicCallsite>,
181}
182
183impl Inner {
184    fn resolve_target(&self, target: &str) -> Arc<str> {
185        if target.is_empty() || target == self.default_target.as_ref() {
186            Arc::clone(&self.default_target)
187        } else {
188            Arc::from(target)
189        }
190    }
191
192    fn describe(
193        &self,
194        kind: MetricKind,
195        key: KeyName,
196        unit: Option<Unit>,
197        description: SharedString,
198    ) {
199        if !self.mark_described(&key, kind) {
200            return;
201        }
202
203        let target = Arc::clone(&self.default_target);
204        let level = self.default_level;
205
206        let event_value = EVENT_DESCRIBE;
207        let name_value = key.as_str();
208        let kind_value = kind.as_ref();
209        let description_value = description.as_ref();
210        let unit_value = unit.map(|unit| unit.as_str());
211
212        let values: [Option<&dyn Value>; 5] = [
213            Some(&event_value as &dyn Value),
214            Some(&name_value as &dyn Value),
215            Some(&kind_value as &dyn Value),
216            Some(&description_value as &dyn Value),
217            unit_value.as_ref().map(|value| value as &dyn Value),
218        ];
219
220        self.dispatch_event(Schema::Describe, &target, level, &values);
221    }
222
223    fn emit_counter(
224        &self,
225        key: &Key,
226        target: &Arc<str>,
227        level: TracingLevel,
228        op: CounterOp,
229        value: u64,
230    ) {
231        let event_value = EVENT_EMIT;
232        let name_value = key.name();
233        let kind_value = MetricKind::Counter.as_ref();
234        let op_value = op.as_ref();
235        let labels_debug = LabelsDebug::from_key(key);
236        let labels_value = field::debug(labels_debug);
237        let values: [Option<&dyn Value>; 6] = [
238            Some(&event_value as &dyn Value),
239            Some(&name_value as &dyn Value),
240            Some(&kind_value as &dyn Value),
241            Some(&labels_value as &dyn Value),
242            Some(&value as &dyn Value),
243            Some(&op_value as &dyn Value),
244        ];
245
246        self.dispatch_event(Schema::Emit, target, level, &values);
247    }
248
249    fn emit_gauge(
250        &self,
251        key: &Key,
252        target: &Arc<str>,
253        level: TracingLevel,
254        op: GaugeOp,
255        value: f64,
256    ) {
257        let event_value = EVENT_EMIT;
258        let name_value = key.name();
259        let kind_value = MetricKind::Gauge.as_ref();
260        let op_value = op.as_ref();
261        let labels_debug = LabelsDebug::from_key(key);
262        let labels_value = field::debug(labels_debug);
263        let values: [Option<&dyn Value>; 6] = [
264            Some(&event_value as &dyn Value),
265            Some(&name_value as &dyn Value),
266            Some(&kind_value as &dyn Value),
267            Some(&labels_value as &dyn Value),
268            Some(&value as &dyn Value),
269            Some(&op_value as &dyn Value),
270        ];
271
272        self.dispatch_event(Schema::Emit, target, level, &values);
273    }
274
275    fn emit_histogram(&self, key: &Key, target: &Arc<str>, level: TracingLevel, value: f64) {
276        let event_value = EVENT_EMIT;
277        let name_value = key.name();
278        let kind_value = MetricKind::Histogram.as_ref();
279        let op_value = HistogramOp::Sample.as_ref();
280        let labels_debug = LabelsDebug::from_key(key);
281        let labels_value = field::debug(labels_debug);
282        let values: [Option<&dyn Value>; 6] = [
283            Some(&event_value as &dyn Value),
284            Some(&name_value as &dyn Value),
285            Some(&kind_value as &dyn Value),
286            Some(&labels_value as &dyn Value),
287            Some(&value as &dyn Value),
288            Some(&op_value as &dyn Value),
289        ];
290
291        self.dispatch_event(Schema::Emit, target, level, &values);
292    }
293
294    fn dispatch_event(
295        &self,
296        schema: Schema,
297        target: &Arc<str>,
298        level: TracingLevel,
299        values: &[Option<&dyn Value>],
300    ) {
301        if !tracing::level_enabled!(level) {
302            return;
303        }
304
305        let callsite = self.callsite_for(schema, level, target);
306        let meta = callsite.metadata_static();
307
308        let interest = callsite.interest();
309        if interest.is_never() {
310            return;
311        }
312        if !interest.is_always() && !tracing::dispatcher::get_default(|d| d.enabled(meta)) {
313            return;
314        }
315
316        let value_set = meta.fields().value_set_all(values);
317        Event::dispatch(meta, &value_set);
318    }
319
320    fn callsite_for(
321        &self,
322        schema: Schema,
323        level: TracingLevel,
324        target: &Arc<str>,
325    ) -> &'static DynamicCallsite {
326        let key = CallsiteKey {
327            schema,
328            level: level_key(level),
329            target: Arc::clone(target),
330        };
331
332        match self.callsites.entry(key) {
333            dashmap::mapref::entry::Entry::Occupied(entry) => entry.get(),
334            dashmap::mapref::entry::Entry::Vacant(entry) => {
335                let callsite = build_callsite(schema, level, target.as_ref());
336                entry.insert(callsite);
337                callsite
338            }
339        }
340    }
341
342    fn mark_described(&self, name: &KeyName, kind: MetricKind) -> bool {
343        let key = SeenKey {
344            name: name.clone(),
345            kind,
346        };
347
348        self.described.insert(key)
349    }
350}
351
352#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, strum::AsRefStr)]
353#[strum(serialize_all = "snake_case")]
354enum MetricKind {
355    Counter,
356    Gauge,
357    Histogram,
358}
359
360#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
361enum Schema {
362    #[cfg(feature = "prometheus")]
363    Snapshot,
364    #[cfg(feature = "prometheus")]
365    HistogramSnapshot,
366    Emit,
367    Describe,
368}
369
370impl Schema {
371    fn field_names(self) -> &'static [&'static str] {
372        match self {
373            #[cfg(feature = "prometheus")]
374            Schema::Snapshot => &["event", "name", "kind", "labels", "statistic", "value"],
375            #[cfg(feature = "prometheus")]
376            Schema::HistogramSnapshot => &[
377                "event",
378                "name",
379                "kind",
380                "labels",
381                "count",
382                "sum",
383                "schema",
384                "zero_threshold",
385                "zero_count",
386                "buckets",
387                "positive_spans",
388                "positive_deltas",
389                "positive_counts",
390                "negative_spans",
391                "negative_deltas",
392                "negative_counts",
393            ],
394            Schema::Emit => &EMIT_FIELDS,
395            Schema::Describe => &DESCRIBE_FIELDS,
396        }
397    }
398}
399
400#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::AsRefStr)]
401#[strum(serialize_all = "snake_case")]
402enum CounterOp {
403    Increment,
404    Absolute,
405}
406
407#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::AsRefStr)]
408#[strum(serialize_all = "snake_case")]
409enum GaugeOp {
410    Set,
411    Increment,
412    Decrement,
413}
414
415#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::AsRefStr)]
416#[strum(serialize_all = "snake_case")]
417enum HistogramOp {
418    Sample,
419}
420
421#[derive(Clone, Debug, Eq, PartialEq, Hash)]
422struct SeenKey {
423    name: KeyName,
424    kind: MetricKind,
425}
426
427#[derive(Clone, Debug, Eq, PartialEq, Hash)]
428struct CallsiteKey {
429    schema: Schema,
430    level: u8,
431    target: Arc<str>,
432}
433
434const EVENT_EMIT: &str = "emit";
435const EVENT_DESCRIBE: &str = "describe";
436
437const EMIT_FIELDS: [&str; 6] = ["event", "name", "kind", "labels", "value", "op"];
438const DESCRIBE_FIELDS: [&str; 5] = ["event", "name", "kind", "description", "unit"];
439
440// Stores a callsite's `Interest` in a compact atomic encoding.
441struct DynamicCallsite {
442    metadata: OnceLock<TracingMetadata<'static>>,
443    interest: AtomicU8,
444}
445
446impl DynamicCallsite {
447    const INTEREST_NEVER: u8 = 0;
448    const INTEREST_SOMETIMES: u8 = 1;
449    const INTEREST_ALWAYS: u8 = 2;
450
451    fn new() -> Self {
452        Self {
453            metadata: OnceLock::new(),
454            interest: AtomicU8::new(Self::INTEREST_SOMETIMES),
455        }
456    }
457
458    fn set_metadata(&self, metadata: TracingMetadata<'static>) {
459        self.metadata
460            .set(metadata)
461            .expect("callsite metadata set once");
462    }
463
464    fn metadata_static(&'static self) -> &'static TracingMetadata<'static> {
465        self.metadata
466            .get()
467            .expect("callsite metadata set before registration")
468    }
469
470    fn interest(&self) -> Interest {
471        match self.interest.load(Ordering::Relaxed) {
472            Self::INTEREST_NEVER => Interest::never(),
473            Self::INTEREST_ALWAYS => Interest::always(),
474            _ => Interest::sometimes(),
475        }
476    }
477}
478
479impl Callsite for DynamicCallsite {
480    fn set_interest(&self, interest: Interest) {
481        let value = if interest.is_never() {
482            Self::INTEREST_NEVER
483        } else if interest.is_always() {
484            Self::INTEREST_ALWAYS
485        } else {
486            Self::INTEREST_SOMETIMES
487        };
488        self.interest.store(value, Ordering::Relaxed);
489    }
490
491    fn metadata(&self) -> &TracingMetadata<'_> {
492        self.metadata
493            .get()
494            .expect("callsite metadata set before registration")
495    }
496}
497
498fn build_callsite(schema: Schema, level: TracingLevel, target: &str) -> &'static DynamicCallsite {
499    // NOTE: `tracing` requires callsites and metadata to live for the program's
500    // lifetime, so we intentionally leak the callsite and target string.
501    let callsite = Box::leak(Box::new(DynamicCallsite::new()));
502    let target_static: &'static str = Box::leak(target.to_string().into_boxed_str());
503    let fields = FieldSet::new(schema.field_names(), callsite::Identifier(callsite));
504    let meta = TracingMetadata::new(
505        EVENT_NAME,
506        target_static,
507        level,
508        None,
509        None,
510        None,
511        fields,
512        metadata::Kind::EVENT,
513    );
514    callsite.set_metadata(meta);
515    callsite::register(callsite);
516    callsite
517}
518
519fn map_level(level: metrics::Level) -> TracingLevel {
520    match level {
521        metrics::Level::TRACE => TracingLevel::TRACE,
522        metrics::Level::DEBUG => TracingLevel::DEBUG,
523        metrics::Level::INFO => TracingLevel::INFO,
524        metrics::Level::WARN => TracingLevel::WARN,
525        metrics::Level::ERROR => TracingLevel::ERROR,
526    }
527}
528
529fn level_key(level: TracingLevel) -> u8 {
530    match level {
531        TracingLevel::TRACE => 0,
532        TracingLevel::DEBUG => 1,
533        TracingLevel::INFO => 2,
534        TracingLevel::WARN => 3,
535        TracingLevel::ERROR => 4,
536    }
537}
538
539struct LabelsDebug<'a> {
540    key: &'a Key,
541}
542
543impl<'a> LabelsDebug<'a> {
544    fn from_key(key: &'a Key) -> Self {
545        Self { key }
546    }
547}
548
549impl fmt::Debug for LabelsDebug<'_> {
550    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
551        let mut map = f.debug_map();
552        for label in self.key.labels() {
553            map.entry(&label.key(), &label.value());
554        }
555        map.finish()
556    }
557}
558
559struct CounterHandle {
560    inner: Arc<Inner>,
561    key: Key,
562    target: Arc<str>,
563    level: TracingLevel,
564}
565
566impl metrics::CounterFn for CounterHandle {
567    fn increment(&self, value: u64) {
568        self.inner.emit_counter(
569            &self.key,
570            &self.target,
571            self.level,
572            CounterOp::Increment,
573            value,
574        );
575    }
576
577    fn absolute(&self, value: u64) {
578        self.inner.emit_counter(
579            &self.key,
580            &self.target,
581            self.level,
582            CounterOp::Absolute,
583            value,
584        );
585    }
586}
587
588struct GaugeHandle {
589    inner: Arc<Inner>,
590    key: Key,
591    target: Arc<str>,
592    level: TracingLevel,
593}
594
595impl metrics::GaugeFn for GaugeHandle {
596    fn increment(&self, value: f64) {
597        self.inner.emit_gauge(
598            &self.key,
599            &self.target,
600            self.level,
601            GaugeOp::Increment,
602            value,
603        );
604    }
605
606    fn decrement(&self, value: f64) {
607        self.inner.emit_gauge(
608            &self.key,
609            &self.target,
610            self.level,
611            GaugeOp::Decrement,
612            value,
613        );
614    }
615
616    fn set(&self, value: f64) {
617        self.inner
618            .emit_gauge(&self.key, &self.target, self.level, GaugeOp::Set, value);
619    }
620}
621
622struct HistogramHandle {
623    inner: Arc<Inner>,
624    key: Key,
625    target: Arc<str>,
626    level: TracingLevel,
627}
628
629impl metrics::HistogramFn for HistogramHandle {
630    fn record(&self, value: f64) {
631        self.inner
632            .emit_histogram(&self.key, &self.target, self.level, value);
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use serde_json::Value;
640    use std::io;
641    use std::sync::{Arc, Mutex};
642
643    use tracing_subscriber::fmt::MakeWriter;
644    use tracing_subscriber::prelude::*;
645
646    #[derive(Clone, Default)]
647    struct JsonBuffer {
648        buf: Arc<Mutex<Vec<u8>>>,
649    }
650
651    impl JsonBuffer {
652        fn new(buf: Arc<Mutex<Vec<u8>>>) -> Self {
653            Self { buf }
654        }
655    }
656
657    struct JsonWriter {
658        buf: Arc<Mutex<Vec<u8>>>,
659    }
660
661    impl<'a> MakeWriter<'a> for JsonBuffer {
662        type Writer = JsonWriter;
663
664        fn make_writer(&'a self) -> Self::Writer {
665            JsonWriter {
666                buf: Arc::clone(&self.buf),
667            }
668        }
669    }
670
671    impl io::Write for JsonWriter {
672        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
673            let mut guard = self
674                .buf
675                .lock()
676                .map_err(|_| io::Error::other("log buffer poisoned"))?;
677            guard.extend_from_slice(bytes);
678            Ok(bytes.len())
679        }
680
681        fn flush(&mut self) -> io::Result<()> {
682            Ok(())
683        }
684    }
685
686    fn capture_events(f: impl FnOnce()) -> Vec<Value> {
687        let buffer = Arc::new(Mutex::new(Vec::new()));
688        let writer = JsonBuffer::new(Arc::clone(&buffer));
689        let layer = tracing_subscriber::fmt::layer()
690            .json()
691            .with_current_span(false)
692            .with_span_list(false)
693            .with_writer(writer);
694        let subscriber = tracing_subscriber::registry().with(layer);
695        tracing::subscriber::with_default(subscriber, f);
696
697        let bytes = buffer.lock().expect("log buffer").clone();
698        let text = String::from_utf8(bytes).expect("log output utf8");
699        text.lines()
700            .filter(|line| !line.is_empty())
701            .map(|line| serde_json::from_str::<Value>(line).expect("valid json"))
702            .collect()
703    }
704
705    fn event_fields(event: &Value) -> &serde_json::Map<String, Value> {
706        event
707            .get("fields")
708            .and_then(Value::as_object)
709            .expect("fields object")
710    }
711
712    #[cfg(feature = "prometheus")]
713    mod snapshots {
714        use super::*;
715        use prometheus_client::encoding::prometheus_protobuf::prometheus_data_model::{
716            Bucket, BucketSpan, Histogram, LabelPair, Metric, MetricFamily, MetricType,
717        };
718
719        fn histogram() -> MetricFamily {
720            MetricFamily {
721                name: "latency".into(),
722                r#type: MetricType::Histogram.into(),
723                metric: vec![Metric {
724                    label: vec![LabelPair {
725                        name: "session_id".into(),
726                        value: "7".into(),
727                    }],
728                    histogram: Some(Histogram {
729                        sample_count: u64::MAX,
730                        sample_sum: 1.5,
731                        ..Default::default()
732                    }),
733                    ..Default::default()
734                }],
735                ..Default::default()
736            }
737        }
738
739        #[test]
740        fn histogram_snapshot_preserves_event_fields_and_bucket_encoding() {
741            let recorder = TracingRecorder::builder()
742                .default_target("received.metrics")
743                .default_level(TracingLevel::INFO)
744                .build();
745            let mut metric = histogram();
746            let histogram = metric.metric[0].histogram.as_mut().unwrap();
747            histogram.bucket = vec![Bucket {
748                cumulative_count: 2,
749                upper_bound: 1.0,
750                ..Default::default()
751            }];
752            histogram.schema = 2;
753            histogram.zero_threshold = 0.01;
754            histogram.zero_count = 1;
755            histogram.positive_span = vec![BucketSpan {
756                offset: 3,
757                length: 2,
758            }];
759            histogram.positive_delta = vec![2, -1];
760            histogram.negative_span = vec![BucketSpan {
761                offset: -2,
762                length: 1,
763            }];
764            histogram.negative_delta = vec![3];
765            let expected = [
766                ("buckets", format!("{:?}", histogram.bucket)),
767                ("positive_spans", format!("{:?}", histogram.positive_span)),
768                ("positive_deltas", format!("{:?}", histogram.positive_delta)),
769                ("negative_spans", format!("{:?}", histogram.negative_span)),
770                ("negative_deltas", format!("{:?}", histogram.negative_delta)),
771            ];
772            let events = capture_events(|| recorder.emit_snapshot(&[metric]));
773            assert_eq!(events.len(), 1);
774            assert_eq!(events[0]["target"], "received.metrics");
775            assert_eq!(events[0]["level"], "INFO");
776            let fields = event_fields(&events[0]);
777            assert_eq!(fields["event"], "snapshot");
778            assert_eq!(fields["name"], "latency");
779            assert_eq!(fields["kind"], "histogram");
780            assert_eq!(fields["labels"], r#"{"session_id": "7"}"#);
781            assert_eq!(fields["count"], u64::MAX);
782            assert_eq!(fields["sum"], 1.5);
783            assert_eq!(fields["schema"], 2);
784            assert_eq!(fields["zero_threshold"], 0.01);
785            assert_eq!(fields["zero_count"], 1);
786            for (name, value) in expected {
787                assert_eq!(fields[name], value);
788            }
789        }
790
791        #[test]
792        fn histogram_snapshot_prefers_float_counts_over_integer_counts() {
793            let recorder = TracingRecorder::new();
794            let mut metric = histogram();
795            let histogram = metric.metric[0].histogram.as_mut().unwrap();
796            histogram.sample_count_float = 2.5;
797            histogram.zero_count = u64::MAX;
798            histogram.zero_count_float = 0.5;
799            histogram.positive_count = vec![1.25];
800            histogram.negative_count = vec![0.75];
801            let events = capture_events(|| recorder.emit_snapshot(&[metric]));
802            let fields = event_fields(&events[0]);
803            assert_eq!(fields["count"], 2.5);
804            assert_eq!(fields["zero_count"], 0.5);
805            assert_eq!(fields["positive_counts"], "[1.25]");
806            assert_eq!(fields["negative_counts"], "[0.75]");
807        }
808    }
809
810    #[test]
811    fn emits_counter_event_fields() {
812        let recorder = TracingRecorder::new();
813        let key = Key::from_parts("my_counter", &[("label", "value")]);
814        let metadata = Metadata::new("my.target", metrics::Level::INFO, None);
815        let counter = recorder.register_counter(&key, &metadata);
816
817        let events = capture_events(|| {
818            counter.increment(3);
819        });
820
821        assert_eq!(events.len(), 1);
822        let event = &events[0];
823        let fields = event_fields(event);
824
825        assert_eq!(event.get("level").and_then(Value::as_str), Some("INFO"));
826        assert_eq!(
827            event.get("target").and_then(Value::as_str),
828            Some("my.target")
829        );
830        assert_eq!(fields.get("event").and_then(Value::as_str), Some("emit"));
831        assert_eq!(
832            fields.get("name").and_then(Value::as_str),
833            Some("my_counter")
834        );
835        assert_eq!(fields.get("kind").and_then(Value::as_str), Some("counter"));
836        assert_eq!(fields.get("op").and_then(Value::as_str), Some("increment"));
837        assert_eq!(fields.get("value"), Some(&Value::from(3)));
838
839        let expected_labels = format!("{:?}", LabelsDebug::from_key(&key));
840        assert_eq!(
841            fields.get("labels").and_then(Value::as_str),
842            Some(expected_labels.as_str())
843        );
844    }
845
846    #[test]
847    fn emits_gauge_set_with_level() {
848        let recorder = TracingRecorder::new();
849        let key = Key::from_name("my_gauge");
850        let metadata = Metadata::new("gauge.target", metrics::Level::WARN, None);
851        let gauge = recorder.register_gauge(&key, &metadata);
852
853        let events = capture_events(|| {
854            gauge.set(2.5);
855        });
856
857        assert_eq!(events.len(), 1);
858        let event = &events[0];
859        let fields = event_fields(event);
860
861        assert_eq!(event.get("level").and_then(Value::as_str), Some("WARN"));
862        assert_eq!(
863            event.get("target").and_then(Value::as_str),
864            Some("gauge.target")
865        );
866        assert_eq!(fields.get("event").and_then(Value::as_str), Some("emit"));
867        assert_eq!(fields.get("kind").and_then(Value::as_str), Some("gauge"));
868        assert_eq!(fields.get("op").and_then(Value::as_str), Some("set"));
869        assert_eq!(fields.get("value"), Some(&Value::from(2.5)));
870    }
871
872    #[test]
873    fn emits_histogram_sample_op() {
874        let recorder = TracingRecorder::new();
875        let key = Key::from_name("my_histogram");
876        let metadata = Metadata::new("hist.target", metrics::Level::DEBUG, None);
877        let histogram = recorder.register_histogram(&key, &metadata);
878
879        let events = capture_events(|| {
880            histogram.record(7.25);
881        });
882
883        assert_eq!(events.len(), 1);
884        let event = &events[0];
885        let fields = event_fields(event);
886
887        assert_eq!(event.get("level").and_then(Value::as_str), Some("DEBUG"));
888        assert_eq!(
889            event.get("target").and_then(Value::as_str),
890            Some("hist.target")
891        );
892        assert_eq!(fields.get("event").and_then(Value::as_str), Some("emit"));
893        assert_eq!(
894            fields.get("kind").and_then(Value::as_str),
895            Some("histogram")
896        );
897        assert_eq!(fields.get("op").and_then(Value::as_str), Some("sample"));
898        assert_eq!(fields.get("value"), Some(&Value::from(7.25)));
899    }
900
901    #[test]
902    fn describe_is_emitted_once() {
903        let recorder = TracingRecorder::new();
904
905        let events = capture_events(|| {
906            recorder.describe_counter(
907                KeyName::from("described_counter"),
908                Some(Unit::Seconds),
909                "desc".into(),
910            );
911            recorder.describe_counter(
912                KeyName::from("described_counter"),
913                Some(Unit::Seconds),
914                "desc".into(),
915            );
916        });
917
918        assert_eq!(events.len(), 1);
919        let event = &events[0];
920        let fields = event_fields(event);
921        assert_eq!(
922            fields.get("event").and_then(Value::as_str),
923            Some("describe")
924        );
925        assert_eq!(
926            fields.get("description").and_then(Value::as_str),
927            Some("desc")
928        );
929        assert_eq!(fields.get("unit").and_then(Value::as_str), Some("seconds"));
930    }
931
932    #[test]
933    fn describe_is_emitted_per_kind() {
934        let recorder = TracingRecorder::new();
935
936        let events = capture_events(|| {
937            recorder.describe_counter(
938                KeyName::from("shared_name"),
939                Some(Unit::Seconds),
940                "counter".into(),
941            );
942            recorder.describe_gauge(KeyName::from("shared_name"), None, "gauge".into());
943        });
944
945        assert_eq!(events.len(), 2);
946        let mut kinds: Vec<_> = events
947            .iter()
948            .map(|event| {
949                event_fields(event)
950                    .get("kind")
951                    .and_then(Value::as_str)
952                    .unwrap_or_default()
953                    .to_string()
954            })
955            .collect();
956        kinds.sort();
957        assert_eq!(kinds, vec!["counter".to_string(), "gauge".to_string()]);
958    }
959
960    #[test]
961    fn describe_uses_default_level_and_target() {
962        let recorder = TracingRecorder::new();
963
964        let events = capture_events(|| {
965            recorder.describe_gauge(KeyName::from("gauge"), None, "desc".into());
966        });
967
968        assert_eq!(events.len(), 1);
969        let event = &events[0];
970        assert_eq!(event.get("level").and_then(Value::as_str), Some("TRACE"));
971        assert_eq!(
972            event.get("target").and_then(Value::as_str),
973            Some(DEFAULT_TARGET)
974        );
975    }
976
977    #[test]
978    fn emit_uses_default_target_when_metadata_empty() {
979        let recorder = TracingRecorder::new();
980        let key = Key::from_name("empty_target");
981        let metadata = Metadata::new("", metrics::Level::INFO, None);
982        let counter = recorder.register_counter(&key, &metadata);
983
984        let events = capture_events(|| {
985            counter.increment(1);
986        });
987
988        assert_eq!(events.len(), 1);
989        let event = &events[0];
990        assert_eq!(
991            event.get("target").and_then(Value::as_str),
992            Some(DEFAULT_TARGET)
993        );
994    }
995
996    #[test]
997    fn emit_does_not_include_unit() {
998        let recorder = TracingRecorder::new();
999        let key = Key::from_name("my_histogram");
1000        let metadata = Metadata::new("my.target", metrics::Level::INFO, None);
1001        let histogram = recorder.register_histogram(&key, &metadata);
1002
1003        let events = capture_events(|| {
1004            histogram.record(1.5);
1005        });
1006
1007        assert_eq!(events.len(), 1);
1008        let event = &events[0];
1009        let fields = event_fields(event);
1010        assert_eq!(fields.get("event").and_then(Value::as_str), Some("emit"));
1011        assert!(fields.get("unit").is_none());
1012    }
1013}