Skip to main content

vector/sinks/prometheus/
exporter.rs

1use std::{
2    convert::Infallible,
3    hash::Hash,
4    mem::{Discriminant, discriminant},
5    net::{IpAddr, Ipv4Addr, SocketAddr},
6    sync::{Arc, RwLock},
7    time::{Duration, Instant},
8};
9
10use async_trait::async_trait;
11use base64::prelude::{BASE64_STANDARD, Engine as _};
12use futures::{FutureExt, StreamExt, future, stream::BoxStream};
13use hyper::{
14    Body, Method, Request, Response, Server, StatusCode,
15    body::HttpBody,
16    header::HeaderValue,
17    service::{make_service_fn, service_fn},
18};
19use indexmap::{IndexMap, map::Entry};
20use serde_with::serde_as;
21use stream_cancel::{Trigger, Tripwire};
22use tower::ServiceBuilder;
23use tower_http::compression::CompressionLayer;
24use tracing::{Instrument, Span};
25use vector_lib::{
26    ByteSizeOf, EstimatedJsonEncodedSizeOf,
27    configurable::configurable_component,
28    internal_event::{
29        ByteSize, BytesSent, CountByteSize, EventsSent, InternalEventHandle as _, Output, Protocol,
30        Registered,
31    },
32};
33
34use super::collector::{MetricCollector, StringCollector};
35use crate::{
36    config::{
37        AcknowledgementsConfig, DynValidatedSink, GenerateConfig, Input, Resource, SinkConfig,
38        SinkContext, ValidatedSink,
39    },
40    event::{
41        Event, EventStatus, Finalizable,
42        metric::{Metric, MetricData, MetricKind, MetricSeries, MetricValue},
43    },
44    http::{Auth, build_http_trace_layer},
45    internal_events::PrometheusNormalizationError,
46    sinks::{
47        Healthcheck, VectorSink,
48        util::{StreamSink, statistic::validate_quantiles},
49    },
50    tls::{MaybeTlsSettings, TlsEnableableConfig},
51};
52
53const LOCK_FAILED: &str = "Prometheus exporter data lock is poisoned";
54
55/// Configuration for the `prometheus_exporter` sink.
56#[serde_as]
57#[configurable_component(sink(
58    "prometheus_exporter",
59    "Expose metric events on a Prometheus compatible endpoint."
60))]
61#[derive(Clone, Debug)]
62#[serde(deny_unknown_fields)]
63pub struct PrometheusExporterConfig {
64    /// The default namespace for any metrics sent.
65    ///
66    /// This namespace is only used if a metric has no existing namespace. When a namespace is
67    /// present, it is used as a prefix to the metric name, and separated with an underscore (`_`).
68    ///
69    /// It should follow the Prometheus [naming conventions][prom_naming_docs].
70    ///
71    /// [prom_naming_docs]: https://prometheus.io/docs/practices/naming/#metric-names
72    #[serde(alias = "namespace")]
73    pub default_namespace: Option<String>,
74
75    /// The address to expose for scraping.
76    ///
77    /// The metrics are exposed at the typical Prometheus exporter path, `/metrics`.
78    #[serde(default = "default_address")]
79    #[configurable(metadata(docs::examples = "192.160.0.10:9598"))]
80    pub address: SocketAddr,
81
82    #[configurable(derived)]
83    pub auth: Option<Auth>,
84
85    #[configurable(derived)]
86    pub tls: Option<TlsEnableableConfig>,
87
88    /// Default buckets to use for aggregating [distribution][dist_metric_docs] metrics into histograms.
89    ///
90    /// [dist_metric_docs]: https://vector.dev/docs/architecture/data-model/metric/#distribution
91    #[serde(default = "super::default_histogram_buckets")]
92    pub buckets: Vec<f64>,
93
94    /// Quantiles to use for aggregating [distribution][dist_metric_docs] metrics into a summary.
95    ///
96    /// [dist_metric_docs]: https://vector.dev/docs/architecture/data-model/metric/#distribution
97    #[serde(default = "super::default_summary_quantiles")]
98    pub quantiles: Vec<f64>,
99
100    /// Whether or not to render [distributions][dist_metric_docs] as an [aggregated histogram][prom_agg_hist_docs] or  [aggregated summary][prom_agg_summ_docs].
101    ///
102    /// While distributions as a lossless way to represent a set of samples for a
103    /// metric is supported, Prometheus clients (the application being scraped, which is this sink) must
104    /// aggregate locally into either an aggregated histogram or aggregated summary.
105    ///
106    /// [dist_metric_docs]: https://vector.dev/docs/architecture/data-model/metric/#distribution
107    /// [prom_agg_hist_docs]: https://prometheus.io/docs/concepts/metric_types/#histogram
108    /// [prom_agg_summ_docs]: https://prometheus.io/docs/concepts/metric_types/#summary
109    #[serde(default = "default_distributions_as_summaries")]
110    pub distributions_as_summaries: bool,
111
112    /// The interval, in seconds, on which metrics are flushed.
113    ///
114    /// On the flush interval, if a metric has not been seen since the last flush interval, it is
115    /// considered expired and is removed.
116    ///
117    /// Be sure to configure this value higher than your client’s scrape interval.
118    ///
119    /// Set to `0` to disable expiration entirely. Metrics will then accumulate for as long as the
120    /// sink runs, which can result in unbounded memory growth if metric series cardinality is
121    /// unbounded.
122    #[serde(default = "default_flush_period_secs")]
123    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
124    #[configurable(metadata(docs::human_name = "Flush Interval"))]
125    pub flush_period_secs: Duration,
126
127    /// Suppresses timestamps on the Prometheus output.
128    ///
129    /// This can sometimes be useful when the source of metrics leads to their timestamps being too
130    /// far in the past for Prometheus to allow them, such as when aggregating metrics over long
131    /// time periods, or when replaying old metrics from a disk buffer.
132    #[serde(default)]
133    pub suppress_timestamp: bool,
134
135    #[configurable(derived)]
136    #[serde(
137        default,
138        deserialize_with = "crate::serde::bool_or_struct",
139        skip_serializing_if = "crate::serde::is_default"
140    )]
141    pub acknowledgements: AcknowledgementsConfig,
142}
143
144impl Default for PrometheusExporterConfig {
145    fn default() -> Self {
146        Self {
147            default_namespace: None,
148            address: default_address(),
149            auth: None,
150            tls: None,
151            buckets: super::default_histogram_buckets(),
152            quantiles: super::default_summary_quantiles(),
153            distributions_as_summaries: default_distributions_as_summaries(),
154            flush_period_secs: default_flush_period_secs(),
155            suppress_timestamp: default_suppress_timestamp(),
156            acknowledgements: Default::default(),
157        }
158    }
159}
160
161const fn default_address() -> SocketAddr {
162    SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 9598)
163}
164
165const fn default_distributions_as_summaries() -> bool {
166    false
167}
168
169const fn default_flush_period_secs() -> Duration {
170    Duration::from_secs(60)
171}
172
173const fn default_suppress_timestamp() -> bool {
174    false
175}
176
177impl GenerateConfig for PrometheusExporterConfig {
178    fn generate_config() -> serde_json::Value {
179        serde_json::to_value(Self::default()).unwrap()
180    }
181}
182
183#[async_trait::async_trait]
184#[typetag::serde(name = "prometheus_exporter")]
185impl SinkConfig for PrometheusExporterConfig {
186    fn input(&self) -> Input {
187        Input::metric()
188    }
189
190    fn resources(&self) -> Vec<Resource> {
191        vec![Resource::tcp(self.address)]
192    }
193
194    fn acknowledgements(&self) -> &AcknowledgementsConfig {
195        &self.acknowledgements
196    }
197
198    fn as_dyn_validated(&self) -> Option<&dyn DynValidatedSink> {
199        Some(self)
200    }
201}
202
203#[async_trait::async_trait]
204impl ValidatedSink for PrometheusExporterConfig {
205    type Validated = ();
206
207    fn validate(&self) -> crate::Result<()> {
208        validate_quantiles(&self.quantiles)?;
209
210        Ok(())
211    }
212
213    async fn build(
214        &self,
215        _validated: &(),
216        _cx: SinkContext,
217    ) -> crate::Result<(VectorSink, Healthcheck)> {
218        if self.flush_period_secs.is_zero() {
219            warn!(
220                "Disabling `flush_period_secs` (setting it to `0`) disables metric expiration, \
221                 which can result in unbounded memory growth if metric series cardinality is \
222                 unbounded."
223            );
224        }
225
226        let sink = PrometheusExporter::new(self.clone());
227        let healthcheck = future::ok(()).boxed();
228
229        Ok((VectorSink::from_event_streamsink(sink), healthcheck))
230    }
231}
232
233struct PrometheusExporter {
234    server_shutdown_trigger: Option<Trigger>,
235    config: PrometheusExporterConfig,
236    metrics: Arc<RwLock<IndexMap<MetricRef, (Metric, MetricMetadata)>>>,
237}
238
239/// Expiration metadata for a metric.
240#[derive(Clone, Copy, Debug)]
241struct MetricMetadata {
242    expiration_window: Duration,
243    expires_at: Instant,
244}
245
246impl MetricMetadata {
247    pub fn new(expiration_window: Duration) -> Self {
248        Self {
249            expiration_window,
250            expires_at: Instant::now() + expiration_window,
251        }
252    }
253
254    /// Resets the expiration deadline.
255    pub fn refresh(&mut self) {
256        self.expires_at = Instant::now() + self.expiration_window;
257    }
258
259    /// Whether or not the referenced metric has expired yet.
260    ///
261    /// A zero expiration window means expiration is disabled, so the metric never expires.
262    pub fn has_expired(&self, now: Instant) -> bool {
263        !self.expiration_window.is_zero() && now >= self.expires_at
264    }
265}
266
267// Composite identifier that uniquely represents a metric.
268//
269// Instead of simply working off of the name (series) alone, we include the metric kind as well as
270// the type (counter, gauge, etc) and any subtype information like histogram buckets.
271//
272// Specifically, though, we do _not_ include the actual metric value.  This type is used
273// specifically to look up the entry in a map for a metric in the sense of "get the metric whose
274// name is X and type is Y and has these tags".
275#[derive(Clone, Debug)]
276struct MetricRef {
277    series: MetricSeries,
278    value: Discriminant<MetricValue>,
279    bounds: Option<Vec<f64>>,
280}
281
282impl MetricRef {
283    /// Creates a `MetricRef` based on the given `Metric`.
284    pub fn from_metric(metric: &Metric) -> Self {
285        // Either the buckets for an aggregated histogram, or the quantiles for an aggregated summary.
286        let bounds = match metric.value() {
287            MetricValue::AggregatedHistogram { buckets, .. } => {
288                Some(buckets.iter().map(|b| b.upper_limit).collect())
289            }
290            MetricValue::AggregatedSummary { quantiles, .. } => {
291                Some(quantiles.iter().map(|q| q.quantile).collect())
292            }
293            _ => None,
294        };
295
296        Self {
297            series: metric.series().clone(),
298            value: discriminant(metric.value()),
299            bounds,
300        }
301    }
302}
303
304impl PartialEq for MetricRef {
305    fn eq(&self, other: &Self) -> bool {
306        self.series == other.series && self.value == other.value && self.bounds == other.bounds
307    }
308}
309
310impl Eq for MetricRef {}
311
312impl Hash for MetricRef {
313    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
314        self.series.hash(state);
315        self.value.hash(state);
316        if let Some(bounds) = &self.bounds {
317            for bound in bounds {
318                bound.to_bits().hash(state);
319            }
320        }
321    }
322}
323
324fn authorized<T: HttpBody>(req: &Request<T>, auth: &Option<Auth>) -> bool {
325    if let Some(auth) = auth {
326        let headers = req.headers();
327        if let Some(auth_header) = headers.get(hyper::header::AUTHORIZATION) {
328            let encoded_credentials = match auth {
329                Auth::Basic { user, password } => Some(HeaderValue::from_str(
330                    format!(
331                        "Basic {}",
332                        BASE64_STANDARD.encode(format!("{}:{}", user, password.inner()))
333                    )
334                    .as_str(),
335                )),
336                Auth::Bearer { token } => Some(HeaderValue::from_str(
337                    format!("Bearer {}", token.inner()).as_str(),
338                )),
339                Auth::Custom { value } => Some(HeaderValue::from_str(value)),
340                #[cfg(feature = "aws-core")]
341                _ => None,
342            };
343
344            if let Some(Ok(encoded_credentials)) = encoded_credentials
345                && auth_header == encoded_credentials
346            {
347                return true;
348            }
349        }
350    } else {
351        return true;
352    }
353
354    false
355}
356
357#[derive(Clone)]
358struct Handler {
359    auth: Option<Auth>,
360    default_namespace: Option<String>,
361    buckets: Box<[f64]>,
362    quantiles: Box<[f64]>,
363    bytes_sent: Registered<BytesSent>,
364    events_sent: Registered<EventsSent>,
365}
366
367impl Handler {
368    fn handle<T: HttpBody>(
369        &self,
370        req: Request<T>,
371        metrics: &RwLock<IndexMap<MetricRef, (Metric, MetricMetadata)>>,
372    ) -> Response<Body> {
373        let mut response = Response::new(Body::empty());
374
375        match (authorized(&req, &self.auth), req.method(), req.uri().path()) {
376            (false, _, _) => {
377                *response.status_mut() = StatusCode::UNAUTHORIZED;
378                response.headers_mut().insert(
379                    http::header::WWW_AUTHENTICATE,
380                    HeaderValue::from_static("Basic, Bearer"),
381                );
382            }
383
384            (true, &Method::GET, "/metrics") => {
385                let metrics = metrics.read().expect(LOCK_FAILED);
386
387                let count = metrics.len();
388                let byte_size = metrics
389                    .iter()
390                    .map(|(_, (metric, _))| metric.estimated_json_encoded_size_of())
391                    .sum();
392
393                let mut collector = StringCollector::new();
394
395                for (_, (metric, _)) in metrics.iter() {
396                    collector.encode_metric(
397                        self.default_namespace.as_deref(),
398                        &self.buckets,
399                        &self.quantiles,
400                        metric,
401                    );
402                }
403
404                drop(metrics);
405
406                let body = collector.finish();
407                let body_size = body.size_of();
408
409                *response.body_mut() = body.into();
410
411                response.headers_mut().insert(
412                    "Content-Type",
413                    HeaderValue::from_static("text/plain; version=0.0.4"),
414                );
415
416                self.events_sent.emit(CountByteSize(count, byte_size));
417                self.bytes_sent.emit(ByteSize(body_size));
418            }
419
420            (true, _, _) => {
421                *response.status_mut() = StatusCode::NOT_FOUND;
422            }
423        }
424
425        response
426    }
427}
428
429impl PrometheusExporter {
430    fn new(config: PrometheusExporterConfig) -> Self {
431        Self {
432            server_shutdown_trigger: None,
433            config,
434            metrics: Arc::new(RwLock::new(IndexMap::new())),
435        }
436    }
437
438    async fn start_server_if_needed(&mut self) -> crate::Result<()> {
439        if self.server_shutdown_trigger.is_some() {
440            return Ok(());
441        }
442
443        let handler = Handler {
444            bytes_sent: register!(BytesSent::from(Protocol::HTTP)),
445            events_sent: register!(EventsSent::from(Output(None))),
446            default_namespace: self.config.default_namespace.clone(),
447            buckets: self.config.buckets.clone().into(),
448            quantiles: self.config.quantiles.clone().into(),
449            auth: self.config.auth.clone(),
450        };
451
452        let span = Span::current();
453        let metrics = Arc::clone(&self.metrics);
454
455        let new_service = make_service_fn(move |_| {
456            let span = Span::current();
457            let metrics = Arc::clone(&metrics);
458            let handler = handler.clone();
459
460            let inner = service_fn(move |req| {
461                let response = handler.handle(req, &metrics);
462
463                future::ok::<_, Infallible>(response)
464            });
465
466            let service = ServiceBuilder::new()
467                .layer(build_http_trace_layer(span.clone()))
468                .layer(CompressionLayer::new())
469                .service(inner);
470
471            async move { Ok::<_, Infallible>(service) }
472        });
473
474        let (trigger, tripwire) = Tripwire::new();
475
476        let tls = self.config.tls.clone();
477        let address = self.config.address;
478
479        let tls = MaybeTlsSettings::from_config(tls.as_ref(), true)?;
480        let listener = tls.bind(&address).await?;
481
482        crate::spawn_in_current_span(async move {
483            info!(message = "Building HTTP server.", address = %address);
484
485            Server::builder(hyper::server::accept::from_stream(listener.accept_stream()))
486                .serve(new_service)
487                .with_graceful_shutdown(tripwire.then(crate::shutdown::tripwire_handler))
488                .instrument(span)
489                .await
490                .map_err(|error| error!("Server error: {}.", error))?;
491
492            Ok::<(), ()>(())
493        });
494
495        self.server_shutdown_trigger = Some(trigger);
496        Ok(())
497    }
498
499    fn normalize(&mut self, metric: Metric) -> Option<Metric> {
500        let new_metric = match metric.value() {
501            MetricValue::Distribution { .. } => {
502                // Convert the distribution as-is, and then absolute-ify it.
503                let (series, data, metadata) = metric.into_parts();
504                let (time, kind, value) = data.into_parts();
505
506                let new_value = if self.config.distributions_as_summaries {
507                    // We use a sketch when in summary mode because they're actually able to be
508                    // merged and provide correct output, unlike the aggregated summaries that
509                    // we handle from _sources_ like Prometheus.  The collector code itself
510                    // will render sketches as aggregated summaries, so we have continuity there.
511                    value
512                        .distribution_to_sketch()
513                        .expect("value should be distribution already")
514                } else {
515                    value
516                        .distribution_to_agg_histogram(&self.config.buckets)
517                        .expect("value should be distribution already")
518                };
519
520                let data = MetricData::from_parts(time, kind, new_value);
521                Metric::from_parts(series, data, metadata)
522            }
523            _ => metric,
524        };
525
526        match new_metric.kind() {
527            MetricKind::Absolute => Some(new_metric),
528            MetricKind::Incremental => {
529                let metrics = self.metrics.read().expect(LOCK_FAILED);
530                let metric_ref = MetricRef::from_metric(&new_metric);
531
532                if let Some(existing) = metrics.get(&metric_ref) {
533                    let mut current = existing.0.value().clone();
534                    if current.add(new_metric.value()) {
535                        // If we were able to add to the existing value (i.e. they were compatible),
536                        // return the result as an absolute metric.
537                        return Some(new_metric.with_value(current).into_absolute());
538                    }
539                }
540
541                // Otherwise, if we didn't have an existing value or we did and it was not
542                // compatible with the new value, simply return the new value as absolute.
543                Some(new_metric.into_absolute())
544            }
545        }
546    }
547}
548
549#[async_trait]
550impl StreamSink<Event> for PrometheusExporter {
551    async fn run(mut self: Box<Self>, mut input: BoxStream<'_, Event>) -> Result<(), ()> {
552        self.start_server_if_needed()
553            .await
554            .map_err(|error| error!("Failed to start Prometheus exporter: {}.", error))?;
555
556        let mut last_flush = Instant::now();
557        let flush_period = self.config.flush_period_secs;
558
559        while let Some(event) = input.next().await {
560            // If we've exceed our flush interval, go through all of the metrics we're currently
561            // tracking and remove any which have exceeded the flush interval in terms of not
562            // having been updated within that long of a time.
563            //
564            // TODO: Can we be smarter about this? As is, we might wait up to 2x the flush period to
565            // remove an expired metric depending on how things line up.  It'd be cool to _check_
566            // for expired metrics more often, but we also don't want to check _way_ too often, like
567            // every second, since then we're constantly iterating through every metric, etc etc.
568            //
569            // A zero flush period means expiration is disabled, so we skip sweeping entirely.
570            if !flush_period.is_zero() && last_flush.elapsed() > flush_period {
571                last_flush = Instant::now();
572
573                let mut metrics = self.metrics.write().expect(LOCK_FAILED);
574
575                metrics.retain(|_metric_ref, (_, metadata)| !metadata.has_expired(last_flush));
576            }
577
578            // Now process the metric we got.
579            let mut metric = event.into_metric();
580            let finalizers = metric.take_finalizers();
581
582            match self.normalize(metric) {
583                Some(normalized) => {
584                    let normalized = if self.config.suppress_timestamp {
585                        normalized.with_timestamp(None)
586                    } else {
587                        normalized
588                    };
589
590                    // We have a normalized metric, in absolute form.  If we're already aware of this
591                    // metric, update its expiration deadline, otherwise, start tracking it.
592                    let mut metrics = self.metrics.write().expect(LOCK_FAILED);
593
594                    match metrics.entry(MetricRef::from_metric(&normalized)) {
595                        Entry::Occupied(mut entry) => {
596                            let (data, metadata) = entry.get_mut();
597                            *data = normalized;
598                            metadata.refresh();
599                        }
600                        Entry::Vacant(entry) => {
601                            entry.insert((normalized, MetricMetadata::new(flush_period)));
602                        }
603                    }
604                    finalizers.update_status(EventStatus::Delivered);
605                }
606                _ => {
607                    emit!(PrometheusNormalizationError {});
608                    finalizers.update_status(EventStatus::Errored);
609                }
610            }
611        }
612
613        Ok(())
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use chrono::{Duration, Utc};
620    use futures::stream;
621    use indoc::indoc;
622    use similar_asserts::assert_eq;
623    use tokio::{sync::oneshot::error::TryRecvError, time};
624    use vector_common::decompression::CappedDecoder;
625    use vector_lib::{
626        event::{MetricTags, StatisticKind},
627        finalization::{BatchNotifier, BatchStatus},
628        metric_tags, samples,
629        sensitive_string::SensitiveString,
630    };
631
632    use super::*;
633    use crate::{
634        config::ProxyConfig,
635        event::metric::{Metric, MetricValue},
636        http::HttpClient,
637        sinks::prometheus::{distribution_to_agg_histogram, distribution_to_ddsketch},
638        test_util::{
639            addr::next_addr,
640            components::{SINK_TAGS, run_and_assert_sink_compliance},
641            random_string, trace_init,
642        },
643        tls::MaybeTlsSettings,
644    };
645
646    #[test]
647    fn generate_config() {
648        crate::test_util::test_generate_config::<PrometheusExporterConfig>();
649    }
650
651    #[test]
652    fn validate_accepts_zero_flush_period() {
653        // A zero flush period disables metric expiration and is valid.
654        let config = PrometheusExporterConfig {
655            flush_period_secs: std::time::Duration::from_secs(0),
656            ..Default::default()
657        };
658        config
659            .validate()
660            .expect("zero flush period should validate");
661
662        // The default config validates cleanly.
663        PrometheusExporterConfig::default()
664            .validate()
665            .expect("default config should validate");
666    }
667
668    #[tokio::test]
669    async fn prometheus_notls() {
670        export_and_fetch_simple(None).await;
671    }
672
673    #[tokio::test]
674    async fn prometheus_tls() {
675        let mut tls_config = TlsEnableableConfig::test_config();
676        tls_config.options.verify_hostname = Some(false);
677        export_and_fetch_simple(Some(tls_config)).await;
678    }
679
680    #[tokio::test]
681    async fn prometheus_noauth() {
682        let (name1, event1) = create_metric_gauge(None, 123.4);
683        let (name2, event2) = tests::create_metric_set(None, vec!["0", "1", "2"]);
684        let events = vec![event1, event2];
685
686        let response_result = export_and_fetch_with_auth(None, None, events, false).await;
687
688        assert!(response_result.is_ok());
689
690        let body = response_result.expect("Cannot extract body from the response");
691
692        assert!(body.contains(&format!(
693            indoc! {r#"
694               # HELP {name} {name}
695               # TYPE {name} gauge
696               {name}{{some_tag="some_value"}} 123.4
697            "#},
698            name = name1
699        )));
700        assert!(body.contains(&format!(
701            indoc! {r#"
702               # HELP {name} {name}
703               # TYPE {name} gauge
704               {name}{{some_tag="some_value"}} 3
705            "#},
706            name = name2
707        )));
708    }
709
710    #[tokio::test]
711    async fn prometheus_successful_basic_auth() {
712        let (name1, event1) = create_metric_gauge(None, 123.4);
713        let (name2, event2) = tests::create_metric_set(None, vec!["0", "1", "2"]);
714        let events = vec![event1, event2];
715
716        let auth_config = Auth::Basic {
717            user: "user".to_string(),
718            password: SensitiveString::from("password".to_string()),
719        };
720
721        let response_result =
722            export_and_fetch_with_auth(Some(auth_config.clone()), Some(auth_config), events, false)
723                .await;
724
725        assert!(response_result.is_ok());
726
727        let body = response_result.expect("Cannot extract body from the response");
728
729        assert!(body.contains(&format!(
730            indoc! {r#"
731               # HELP {name} {name}
732               # TYPE {name} gauge
733               {name}{{some_tag="some_value"}} 123.4
734            "#},
735            name = name1
736        )));
737        assert!(body.contains(&format!(
738            indoc! {r#"
739               # HELP {name} {name}
740               # TYPE {name} gauge
741               {name}{{some_tag="some_value"}} 3
742            "#},
743            name = name2
744        )));
745    }
746
747    #[tokio::test]
748    async fn prometheus_successful_token_auth() {
749        let (name1, event1) = create_metric_gauge(None, 123.4);
750        let (name2, event2) = tests::create_metric_set(None, vec!["0", "1", "2"]);
751        let events = vec![event1, event2];
752
753        let auth_config = Auth::Bearer {
754            token: SensitiveString::from("token".to_string()),
755        };
756
757        let response_result =
758            export_and_fetch_with_auth(Some(auth_config.clone()), Some(auth_config), events, false)
759                .await;
760
761        assert!(response_result.is_ok());
762
763        let body = response_result.expect("Cannot extract body from the response");
764
765        assert!(body.contains(&format!(
766            indoc! {r#"
767               # HELP {name} {name}
768               # TYPE {name} gauge
769               {name}{{some_tag="some_value"}} 123.4
770            "#},
771            name = name1
772        )));
773        assert!(body.contains(&format!(
774            indoc! {r#"
775               # HELP {name} {name}
776               # TYPE {name} gauge
777               {name}{{some_tag="some_value"}} 3
778            "#},
779            name = name2
780        )));
781    }
782
783    #[tokio::test]
784    async fn prometheus_missing_auth() {
785        let (_, event1) = create_metric_gauge(None, 123.4);
786        let (_, event2) = tests::create_metric_set(None, vec!["0", "1", "2"]);
787        let events = vec![event1, event2];
788
789        let server_auth_config = Auth::Bearer {
790            token: SensitiveString::from("token".to_string()),
791        };
792
793        let response_result =
794            export_and_fetch_with_auth(Some(server_auth_config), None, events, false).await;
795
796        assert!(response_result.is_err());
797        assert_eq!(response_result.unwrap_err(), StatusCode::UNAUTHORIZED);
798    }
799
800    #[tokio::test]
801    async fn prometheus_wrong_auth() {
802        let (_, event1) = create_metric_gauge(None, 123.4);
803        let (_, event2) = tests::create_metric_set(None, vec!["0", "1", "2"]);
804        let events = vec![event1, event2];
805
806        let server_auth_config = Auth::Bearer {
807            token: SensitiveString::from("token".to_string()),
808        };
809
810        let client_auth_config = Auth::Basic {
811            user: "user".to_string(),
812            password: SensitiveString::from("password".to_string()),
813        };
814
815        let response_result = export_and_fetch_with_auth(
816            Some(server_auth_config),
817            Some(client_auth_config),
818            events,
819            false,
820        )
821        .await;
822
823        assert!(response_result.is_err());
824        assert_eq!(response_result.unwrap_err(), StatusCode::UNAUTHORIZED);
825    }
826
827    #[tokio::test]
828    async fn encoding_gzip() {
829        let (name1, event1) = create_metric_gauge(None, 123.4);
830        let events = vec![event1];
831
832        let body_raw = export_and_fetch_raw(None, events, false, Some(String::from("gzip"))).await;
833        let expected = format!(
834            indoc! {r#"
835                # HELP {name} {name}
836                # TYPE {name} gauge
837                {name}{{some_tag="some_value"}} 123.4
838            "#},
839            name = name1,
840        );
841
842        let body_decoded =
843            String::from_utf8(CappedDecoder::gzip(&body_raw[..]).decompress().unwrap()).unwrap();
844
845        assert!(body_raw.len() < expected.len());
846        assert_eq!(body_decoded, expected);
847    }
848
849    #[tokio::test]
850    async fn updates_timestamps() {
851        let timestamp1 = Utc::now();
852        let (name, event1) = create_metric_gauge(None, 123.4);
853        let event1 = Event::from(event1.into_metric().with_timestamp(Some(timestamp1)));
854        let (_, event2) = create_metric_gauge(Some(name.clone()), 12.0);
855        let timestamp2 = timestamp1 + Duration::seconds(1);
856        let event2 = Event::from(event2.into_metric().with_timestamp(Some(timestamp2)));
857        let events = vec![event1, event2];
858
859        let body = export_and_fetch(None, events, false).await;
860        let timestamp = timestamp2.timestamp_millis();
861        assert_eq!(
862            body,
863            format!(
864                indoc! {r#"
865                    # HELP {name} {name}
866                    # TYPE {name} gauge
867                    {name}{{some_tag="some_value"}} 135.4 {timestamp}
868                "#},
869                name = name,
870                timestamp = timestamp
871            )
872        );
873    }
874
875    #[tokio::test]
876    async fn suppress_timestamp() {
877        let timestamp = Utc::now();
878        let (name, event) = create_metric_gauge(None, 123.4);
879        let event = Event::from(event.into_metric().with_timestamp(Some(timestamp)));
880        let events = vec![event];
881
882        let body = export_and_fetch(None, events, true).await;
883        assert_eq!(
884            body,
885            format!(
886                indoc! {r#"
887                    # HELP {name} {name}
888                    # TYPE {name} gauge
889                    {name}{{some_tag="some_value"}} 123.4
890                "#},
891                name = name,
892            )
893        );
894    }
895
896    /// According to the [spec](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md?plain=1#L115)
897    /// > Label names MUST be unique within a LabelSet.
898    /// Prometheus itself will reject the metric with an error. Largely to remain backward compatible with older versions of Vector,
899    /// we only publish the last tag in the list.
900    #[tokio::test]
901    async fn prometheus_duplicate_labels() {
902        let (name, event) = create_metric_with_tags(
903            None,
904            MetricValue::Gauge { value: 123.4 },
905            Some(metric_tags!("code" => "200", "code" => "success")),
906        );
907        let events = vec![event];
908
909        let response_result = export_and_fetch_with_auth(None, None, events, false).await;
910
911        assert!(response_result.is_ok());
912
913        let body = response_result.expect("Cannot extract body from the response");
914
915        assert!(body.contains(&format!(
916            indoc! {r#"
917               # HELP {name} {name}
918               # TYPE {name} gauge
919               {name}{{code="success"}} 123.4
920            "# },
921            name = name
922        )));
923    }
924
925    async fn export_and_fetch_raw(
926        tls_config: Option<TlsEnableableConfig>,
927        mut events: Vec<Event>,
928        suppress_timestamp: bool,
929        encoding: Option<String>,
930    ) -> hyper::body::Bytes {
931        trace_init();
932
933        let client_settings = MaybeTlsSettings::from_config(tls_config.as_ref(), false).unwrap();
934        let proto = client_settings.http_protocol_name();
935
936        let (_guard, address) = next_addr();
937        let config = PrometheusExporterConfig {
938            address,
939            tls: tls_config,
940            suppress_timestamp,
941            ..Default::default()
942        };
943
944        // Set up acknowledgement notification
945        let mut receiver = BatchNotifier::apply_to(&mut events[..]);
946        assert_eq!(receiver.try_recv(), Err(TryRecvError::Empty));
947
948        let (sink, _) = SinkConfig::build(&config, SinkContext::default())
949            .await
950            .unwrap();
951        let (_, delayed_event) = create_metric_gauge(Some("delayed".to_string()), 123.4);
952        let sink_handle = tokio::spawn(run_and_assert_sink_compliance(
953            sink,
954            stream::iter(events).chain(stream::once(async move {
955                // Wait a bit to have time to scrape metrics
956                time::sleep(time::Duration::from_millis(500)).await;
957                delayed_event
958            })),
959            &SINK_TAGS,
960        ));
961
962        time::sleep(time::Duration::from_millis(100)).await;
963
964        // Events are marked as delivered as soon as they are aggregated.
965        assert_eq!(receiver.try_recv(), Ok(BatchStatus::Delivered));
966
967        let mut request = Request::get(format!("{proto}://{address}/metrics"))
968            .body(Body::empty())
969            .expect("Error creating request.");
970
971        if let Some(ref encoding) = encoding {
972            request.headers_mut().insert(
973                http::header::ACCEPT_ENCODING,
974                HeaderValue::from_str(encoding.as_str()).unwrap(),
975            );
976        }
977
978        let proxy = ProxyConfig::default();
979        let result = HttpClient::new(client_settings, &proxy)
980            .unwrap()
981            .send(request)
982            .await
983            .expect("Could not fetch query");
984
985        assert!(result.status().is_success());
986
987        if encoding.is_some() {
988            assert!(
989                result
990                    .headers()
991                    .contains_key(http::header::CONTENT_ENCODING)
992            );
993        }
994
995        let body = result.into_body();
996        let bytes = http_body::Body::collect(body)
997            .await
998            .expect("Reading body failed")
999            .to_bytes();
1000
1001        sink_handle.await.unwrap();
1002
1003        bytes
1004    }
1005
1006    async fn export_and_fetch(
1007        tls_config: Option<TlsEnableableConfig>,
1008        events: Vec<Event>,
1009        suppress_timestamp: bool,
1010    ) -> String {
1011        let bytes = export_and_fetch_raw(tls_config, events, suppress_timestamp, None);
1012        String::from_utf8(bytes.await.to_vec()).unwrap()
1013    }
1014
1015    async fn export_and_fetch_with_auth(
1016        server_auth_config: Option<Auth>,
1017        client_auth_config: Option<Auth>,
1018        mut events: Vec<Event>,
1019        suppress_timestamp: bool,
1020    ) -> Result<String, http::status::StatusCode> {
1021        trace_init();
1022
1023        let client_settings = MaybeTlsSettings::from_config(None, false).unwrap();
1024        let proto = client_settings.http_protocol_name();
1025
1026        let (_guard, address) = next_addr();
1027        let config = PrometheusExporterConfig {
1028            address,
1029            auth: server_auth_config,
1030            tls: None,
1031            suppress_timestamp,
1032            ..Default::default()
1033        };
1034
1035        // Set up acknowledgement notification
1036        let mut receiver = BatchNotifier::apply_to(&mut events[..]);
1037        assert_eq!(receiver.try_recv(), Err(TryRecvError::Empty));
1038
1039        let (sink, _) = SinkConfig::build(&config, SinkContext::default())
1040            .await
1041            .unwrap();
1042        let (_, delayed_event) = create_metric_gauge(Some("delayed".to_string()), 123.4);
1043        let sink_handle = tokio::spawn(run_and_assert_sink_compliance(
1044            sink,
1045            stream::iter(events).chain(stream::once(async move {
1046                // Wait a bit to have time to scrape metrics
1047                time::sleep(time::Duration::from_millis(500)).await;
1048                delayed_event
1049            })),
1050            &SINK_TAGS,
1051        ));
1052
1053        time::sleep(time::Duration::from_millis(100)).await;
1054
1055        // Events are marked as delivered as soon as they are aggregated.
1056        assert_eq!(receiver.try_recv(), Ok(BatchStatus::Delivered));
1057
1058        let mut request = Request::get(format!("{proto}://{address}/metrics"))
1059            .body(Body::empty())
1060            .expect("Error creating request.");
1061
1062        if let Some(client_auth_config) = client_auth_config {
1063            client_auth_config.apply(&mut request);
1064        }
1065
1066        let proxy = ProxyConfig::default();
1067        let result = HttpClient::new(client_settings, &proxy)
1068            .unwrap()
1069            .send(request)
1070            .await
1071            .expect("Could not fetch query");
1072
1073        if !result.status().is_success() {
1074            return Err(result.status());
1075        }
1076
1077        let body = result.into_body();
1078        let bytes = http_body::Body::collect(body)
1079            .await
1080            .expect("Reading body failed")
1081            .to_bytes();
1082        let result = String::from_utf8(bytes.to_vec()).unwrap();
1083
1084        sink_handle.await.unwrap();
1085
1086        Ok(result)
1087    }
1088
1089    async fn export_and_fetch_simple(tls_config: Option<TlsEnableableConfig>) {
1090        let (name1, event1) = create_metric_gauge(None, 123.4);
1091        let (name2, event2) = tests::create_metric_set(None, vec!["0", "1", "2"]);
1092        let events = vec![event1, event2];
1093
1094        let body = export_and_fetch(tls_config, events, false).await;
1095
1096        assert!(body.contains(&format!(
1097            indoc! {r#"
1098               # HELP {name} {name}
1099               # TYPE {name} gauge
1100               {name}{{some_tag="some_value"}} 123.4
1101            "#},
1102            name = name1
1103        )));
1104        assert!(body.contains(&format!(
1105            indoc! {r#"
1106               # HELP {name} {name}
1107               # TYPE {name} gauge
1108               {name}{{some_tag="some_value"}} 3
1109            "#},
1110            name = name2
1111        )));
1112    }
1113
1114    pub fn create_metric_gauge(name: Option<String>, value: f64) -> (String, Event) {
1115        create_metric(name, MetricValue::Gauge { value })
1116    }
1117
1118    pub fn create_metric_set(name: Option<String>, values: Vec<&'static str>) -> (String, Event) {
1119        create_metric(
1120            name,
1121            MetricValue::Set {
1122                values: values.into_iter().map(Into::into).collect(),
1123            },
1124        )
1125    }
1126
1127    fn create_metric(name: Option<String>, value: MetricValue) -> (String, Event) {
1128        create_metric_with_tags(name, value, Some(metric_tags!("some_tag" => "some_value")))
1129    }
1130
1131    fn create_metric_with_tags(
1132        name: Option<String>,
1133        value: MetricValue,
1134        tags: Option<MetricTags>,
1135    ) -> (String, Event) {
1136        let name = name.unwrap_or_else(|| format!("vector_set_{}", random_string(16)));
1137        let event = Metric::new(name.clone(), MetricKind::Incremental, value)
1138            .with_tags(tags)
1139            .into();
1140        (name, event)
1141    }
1142
1143    #[tokio::test]
1144    async fn sink_absolute() {
1145        let (_guard, address) = next_addr();
1146        let config = PrometheusExporterConfig {
1147            address,
1148            tls: None,
1149            ..Default::default()
1150        };
1151
1152        let sink = PrometheusExporter::new(config);
1153
1154        let m1 = Metric::new(
1155            "absolute",
1156            MetricKind::Absolute,
1157            MetricValue::Counter { value: 32. },
1158        )
1159        .with_tags(Some(metric_tags!("tag1" => "value1")));
1160
1161        let m2 = m1.clone().with_tags(Some(metric_tags!("tag1" => "value2")));
1162
1163        let events = vec![
1164            Event::Metric(m1.clone().with_value(MetricValue::Counter { value: 32. })),
1165            Event::Metric(m2.clone().with_value(MetricValue::Counter { value: 33. })),
1166            Event::Metric(m1.clone().with_value(MetricValue::Counter { value: 40. })),
1167        ];
1168
1169        let metrics_handle = Arc::clone(&sink.metrics);
1170
1171        let sink = VectorSink::from_event_streamsink(sink);
1172        let input_events = stream::iter(events).map(Into::into);
1173        sink.run(input_events).await.unwrap();
1174
1175        let metrics_after = metrics_handle.read().unwrap();
1176
1177        let expected_m1 = metrics_after
1178            .get(&MetricRef::from_metric(&m1))
1179            .expect("m1 should exist");
1180        let expected_m1_value = MetricValue::Counter { value: 40. };
1181        assert_eq!(expected_m1.0.value(), &expected_m1_value);
1182
1183        let expected_m2 = metrics_after
1184            .get(&MetricRef::from_metric(&m2))
1185            .expect("m2 should exist");
1186        let expected_m2_value = MetricValue::Counter { value: 33. };
1187        assert_eq!(expected_m2.0.value(), &expected_m2_value);
1188    }
1189
1190    #[tokio::test]
1191    async fn sink_distributions_as_histograms() {
1192        // When we get summary distributions, unless we've been configured to actually emit
1193        // summaries for distributions, we just forcefully turn them into histograms.  This is
1194        // simpler and uses less memory, as aggregated histograms are better supported by Prometheus
1195        // since they can actually be aggregated anywhere in the pipeline -- so long as the buckets
1196        // are the same -- without loss of accuracy.
1197
1198        // This expects that the default for the sink is to render distributions as aggregated histograms.
1199        let (_guard, address) = next_addr();
1200        let config = PrometheusExporterConfig {
1201            address,
1202            tls: None,
1203            ..Default::default()
1204        };
1205        let buckets = config.buckets.clone();
1206
1207        let sink = PrometheusExporter::new(config);
1208
1209        // Define a series of incremental distribution updates.
1210        let base_summary_metric = Metric::new(
1211            "distrib_summary",
1212            MetricKind::Incremental,
1213            MetricValue::Distribution {
1214                statistic: StatisticKind::Summary,
1215                samples: samples!(1.0 => 1, 3.0 => 2),
1216            },
1217        );
1218
1219        let base_histogram_metric = Metric::new(
1220            "distrib_histo",
1221            MetricKind::Incremental,
1222            MetricValue::Distribution {
1223                statistic: StatisticKind::Histogram,
1224                samples: samples!(7.0 => 1, 9.0 => 2),
1225            },
1226        );
1227
1228        let metrics = [
1229            base_summary_metric.clone(),
1230            base_summary_metric
1231                .clone()
1232                .with_value(MetricValue::Distribution {
1233                    statistic: StatisticKind::Summary,
1234                    samples: samples!(1.0 => 2, 2.9 => 1),
1235                }),
1236            base_summary_metric
1237                .clone()
1238                .with_value(MetricValue::Distribution {
1239                    statistic: StatisticKind::Summary,
1240                    samples: samples!(1.0 => 4, 3.2 => 1),
1241                }),
1242            base_histogram_metric.clone(),
1243            base_histogram_metric
1244                .clone()
1245                .with_value(MetricValue::Distribution {
1246                    statistic: StatisticKind::Histogram,
1247                    samples: samples!(7.0 => 2, 9.9 => 1),
1248                }),
1249            base_histogram_metric
1250                .clone()
1251                .with_value(MetricValue::Distribution {
1252                    statistic: StatisticKind::Histogram,
1253                    samples: samples!(7.0 => 4, 10.2 => 1),
1254                }),
1255        ];
1256
1257        // Figure out what the merged distributions should add up to.
1258        let mut merged_summary = base_summary_metric.clone();
1259        assert!(merged_summary.update(&metrics[1]));
1260        assert!(merged_summary.update(&metrics[2]));
1261        let expected_summary = distribution_to_agg_histogram(merged_summary, &buckets)
1262            .expect("input summary metric should have been distribution")
1263            .into_absolute();
1264
1265        let mut merged_histogram = base_histogram_metric.clone();
1266        assert!(merged_histogram.update(&metrics[4]));
1267        assert!(merged_histogram.update(&metrics[5]));
1268        let expected_histogram = distribution_to_agg_histogram(merged_histogram, &buckets)
1269            .expect("input histogram metric should have been distribution")
1270            .into_absolute();
1271
1272        // TODO: make a new metric based on merged_distrib_histogram, with expected_histogram_value,
1273        // so that the discriminant matches and our lookup in the indexmap can actually find it
1274
1275        // Now run the events through the sink and see what ends up in the internal metric map.
1276        let metrics_handle = Arc::clone(&sink.metrics);
1277
1278        let events = metrics
1279            .iter()
1280            .cloned()
1281            .map(Event::Metric)
1282            .collect::<Vec<_>>();
1283
1284        let sink = VectorSink::from_event_streamsink(sink);
1285        let input_events = stream::iter(events).map(Into::into);
1286        sink.run(input_events).await.unwrap();
1287
1288        let metrics_after = metrics_handle.read().unwrap();
1289
1290        // Both metrics should be present, and both should be aggregated histograms.
1291        assert_eq!(metrics_after.len(), 2);
1292
1293        let actual_summary = metrics_after
1294            .get(&MetricRef::from_metric(&expected_summary))
1295            .expect("summary metric should exist");
1296        assert_eq!(actual_summary.0.value(), expected_summary.value());
1297
1298        let actual_histogram = metrics_after
1299            .get(&MetricRef::from_metric(&expected_histogram))
1300            .expect("histogram metric should exist");
1301        assert_eq!(actual_histogram.0.value(), expected_histogram.value());
1302    }
1303
1304    #[tokio::test]
1305    async fn sink_distributions_as_summaries() {
1306        // When we get summary distributions, unless we've been configured to actually emit
1307        // summaries for distributions, we just forcefully turn them into histograms.  This is
1308        // simpler and uses less memory, as aggregated histograms are better supported by Prometheus
1309        // since they can actually be aggregated anywhere in the pipeline -- so long as the buckets
1310        // are the same -- without loss of accuracy.
1311
1312        // This assumes that when we turn on `distributions_as_summaries`, we'll get aggregated
1313        // summaries from distributions.  This is technically true, but the way this test works is
1314        // that we check the internal metric data, which, when in this mode, will actually be a
1315        // sketch (so that we can merge without loss of accuracy).
1316        //
1317        // The render code is actually what will end up rendering those sketches as aggregated
1318        // summaries in the scrape output.
1319        let (_guard, address) = next_addr();
1320        let config = PrometheusExporterConfig {
1321            address,
1322            tls: None,
1323            distributions_as_summaries: true,
1324            ..Default::default()
1325        };
1326
1327        let sink = PrometheusExporter::new(config);
1328
1329        // Define a series of incremental distribution updates.
1330        let base_summary_metric = Metric::new(
1331            "distrib_summary",
1332            MetricKind::Incremental,
1333            MetricValue::Distribution {
1334                statistic: StatisticKind::Summary,
1335                samples: samples!(1.0 => 1, 3.0 => 2),
1336            },
1337        );
1338
1339        let base_histogram_metric = Metric::new(
1340            "distrib_histo",
1341            MetricKind::Incremental,
1342            MetricValue::Distribution {
1343                statistic: StatisticKind::Histogram,
1344                samples: samples!(7.0 => 1, 9.0 => 2),
1345            },
1346        );
1347
1348        let metrics = [
1349            base_summary_metric.clone(),
1350            base_summary_metric
1351                .clone()
1352                .with_value(MetricValue::Distribution {
1353                    statistic: StatisticKind::Summary,
1354                    samples: samples!(1.0 => 2, 2.9 => 1),
1355                }),
1356            base_summary_metric
1357                .clone()
1358                .with_value(MetricValue::Distribution {
1359                    statistic: StatisticKind::Summary,
1360                    samples: samples!(1.0 => 4, 3.2 => 1),
1361                }),
1362            base_histogram_metric.clone(),
1363            base_histogram_metric
1364                .clone()
1365                .with_value(MetricValue::Distribution {
1366                    statistic: StatisticKind::Histogram,
1367                    samples: samples!(7.0 => 2, 9.9 => 1),
1368                }),
1369            base_histogram_metric
1370                .clone()
1371                .with_value(MetricValue::Distribution {
1372                    statistic: StatisticKind::Histogram,
1373                    samples: samples!(7.0 => 4, 10.2 => 1),
1374                }),
1375        ];
1376
1377        // Figure out what the merged distributions should add up to.
1378        let mut merged_summary = base_summary_metric.clone();
1379        assert!(merged_summary.update(&metrics[1]));
1380        assert!(merged_summary.update(&metrics[2]));
1381        let expected_summary = distribution_to_ddsketch(merged_summary)
1382            .expect("input summary metric should have been distribution")
1383            .into_absolute();
1384
1385        let mut merged_histogram = base_histogram_metric.clone();
1386        assert!(merged_histogram.update(&metrics[4]));
1387        assert!(merged_histogram.update(&metrics[5]));
1388        let expected_histogram = distribution_to_ddsketch(merged_histogram)
1389            .expect("input histogram metric should have been distribution")
1390            .into_absolute();
1391
1392        // Now run the events through the sink and see what ends up in the internal metric map.
1393        let metrics_handle = Arc::clone(&sink.metrics);
1394
1395        let events = metrics
1396            .iter()
1397            .cloned()
1398            .map(Event::Metric)
1399            .collect::<Vec<_>>();
1400
1401        let sink = VectorSink::from_event_streamsink(sink);
1402        let input_events = stream::iter(events).map(Into::into);
1403        sink.run(input_events).await.unwrap();
1404
1405        let metrics_after = metrics_handle.read().unwrap();
1406
1407        // Both metrics should be present, and both should be aggregated histograms.
1408        assert_eq!(metrics_after.len(), 2);
1409
1410        let actual_summary = metrics_after
1411            .get(&MetricRef::from_metric(&expected_summary))
1412            .expect("summary metric should exist");
1413        assert_eq!(actual_summary.0.value(), expected_summary.value());
1414
1415        let actual_histogram = metrics_after
1416            .get(&MetricRef::from_metric(&expected_histogram))
1417            .expect("histogram metric should exist");
1418        assert_eq!(actual_histogram.0.value(), expected_histogram.value());
1419    }
1420
1421    #[tokio::test]
1422    async fn sink_gauge_incremental_absolute_mix() {
1423        // Because Prometheus does not, itself, have the concept of an Incremental metric, the
1424        // Exporter must apply a normalization function that converts all metrics to Absolute ones
1425        // before handling them.
1426
1427        // This test ensures that this normalization works correctly when applied to a mix of both
1428        // Incremental and Absolute inputs.
1429        let (_guard, address) = next_addr();
1430        let config = PrometheusExporterConfig {
1431            address,
1432            tls: None,
1433            ..Default::default()
1434        };
1435
1436        let sink = PrometheusExporter::new(config);
1437
1438        let base_absolute_gauge_metric = Metric::new(
1439            "gauge",
1440            MetricKind::Absolute,
1441            MetricValue::Gauge { value: 100.0 },
1442        );
1443
1444        let base_incremental_gauge_metric = Metric::new(
1445            "gauge",
1446            MetricKind::Incremental,
1447            MetricValue::Gauge { value: -10.0 },
1448        );
1449
1450        let metrics = [
1451            base_absolute_gauge_metric.clone(),
1452            base_absolute_gauge_metric
1453                .clone()
1454                .with_value(MetricValue::Gauge { value: 333.0 }),
1455            base_incremental_gauge_metric.clone(),
1456            base_incremental_gauge_metric
1457                .clone()
1458                .with_value(MetricValue::Gauge { value: 4.0 }),
1459        ];
1460
1461        // Now run the events through the sink and see what ends up in the internal metric map.
1462        let metrics_handle = Arc::clone(&sink.metrics);
1463
1464        let events = metrics
1465            .iter()
1466            .cloned()
1467            .map(Event::Metric)
1468            .collect::<Vec<_>>();
1469
1470        let sink = VectorSink::from_event_streamsink(sink);
1471        let input_events = stream::iter(events).map(Into::into);
1472        sink.run(input_events).await.unwrap();
1473
1474        let metrics_after = metrics_handle.read().unwrap();
1475
1476        // The gauge metric should be present.
1477        assert_eq!(metrics_after.len(), 1);
1478
1479        let expected_gauge = Metric::new(
1480            "gauge",
1481            MetricKind::Absolute,
1482            MetricValue::Gauge { value: 327.0 },
1483        );
1484
1485        let actual_gauge = metrics_after
1486            .get(&MetricRef::from_metric(&expected_gauge))
1487            .expect("gauge metric should exist");
1488        assert_eq!(actual_gauge.0.value(), expected_gauge.value());
1489    }
1490
1491    #[test]
1492    fn metric_metadata_never_expires_when_disabled() {
1493        let metadata = MetricMetadata::new(std::time::Duration::ZERO);
1494
1495        // Even long after "now", a metric with expiration disabled should never be considered
1496        // expired.
1497        let far_future = Instant::now() + std::time::Duration::from_secs(60 * 60 * 24 * 365);
1498        assert!(!metadata.has_expired(far_future));
1499    }
1500
1501    #[test]
1502    fn metric_metadata_expires_when_enabled() {
1503        let metadata = MetricMetadata::new(std::time::Duration::from_secs(1));
1504
1505        let past_expiration = Instant::now() + std::time::Duration::from_secs(2);
1506        assert!(metadata.has_expired(past_expiration));
1507    }
1508
1509    #[tokio::test]
1510    async fn build_succeeds_with_flush_period_disabled() {
1511        let (_guard, address) = next_addr();
1512        let config = PrometheusExporterConfig {
1513            address,
1514            tls: None,
1515            flush_period_secs: std::time::Duration::ZERO,
1516            ..Default::default()
1517        };
1518
1519        assert!(
1520            SinkConfig::build(&config, SinkContext::default())
1521                .await
1522                .is_ok()
1523        );
1524    }
1525}
1526
1527#[cfg(all(test, feature = "prometheus-integration-tests"))]
1528mod integration_tests {
1529    #![allow(clippy::print_stdout)] // tests
1530    #![allow(clippy::print_stderr)] // tests
1531    #![allow(clippy::dbg_macro)] // tests
1532
1533    use chrono::Utc;
1534    use futures::{future::ready, stream};
1535    use serde_json::Value;
1536    use tokio::{sync::mpsc, time};
1537    use tokio_stream::wrappers::UnboundedReceiverStream;
1538
1539    use super::*;
1540    use crate::{
1541        config::ProxyConfig,
1542        http::HttpClient,
1543        test_util::{
1544            components::{SINK_TAGS, run_and_assert_sink_compliance},
1545            trace_init,
1546        },
1547    };
1548
1549    fn sink_exporter_address() -> String {
1550        std::env::var("SINK_EXPORTER_ADDRESS").unwrap_or_else(|_| "127.0.0.1:9101".into())
1551    }
1552
1553    fn prometheus_address() -> String {
1554        std::env::var("PROMETHEUS_ADDRESS").unwrap_or_else(|_| "localhost:9090".into())
1555    }
1556
1557    async fn fetch_exporter_body() -> String {
1558        let url = format!("http://{}/metrics", sink_exporter_address());
1559        let request = Request::get(url)
1560            .body(Body::empty())
1561            .expect("Error creating request.");
1562        let proxy = ProxyConfig::default();
1563        let result = HttpClient::new(None, &proxy)
1564            .unwrap()
1565            .send(request)
1566            .await
1567            .expect("Could not send request");
1568        let result = http_body::Body::collect(result.into_body())
1569            .await
1570            .expect("Error fetching body")
1571            .to_bytes();
1572        String::from_utf8_lossy(&result).to_string()
1573    }
1574
1575    async fn prometheus_query(query: &str) -> Value {
1576        let url = format!(
1577            "http://{}/api/v1/query?query={}",
1578            prometheus_address(),
1579            query
1580        );
1581        let request = Request::post(url)
1582            .body(Body::empty())
1583            .expect("Error creating request.");
1584        let proxy = ProxyConfig::default();
1585        let result = HttpClient::new(None, &proxy)
1586            .unwrap()
1587            .send(request)
1588            .await
1589            .expect("Could not fetch query");
1590        let result = http_body::Body::collect(result.into_body())
1591            .await
1592            .expect("Error fetching body")
1593            .to_bytes();
1594        let result = String::from_utf8_lossy(&result);
1595        serde_json::from_str(result.as_ref()).expect("Invalid JSON from prometheus")
1596    }
1597
1598    #[tokio::test]
1599    async fn prometheus_metrics() {
1600        trace_init();
1601
1602        prometheus_scrapes_metrics().await;
1603        time::sleep(time::Duration::from_millis(500)).await;
1604        reset_on_flush_period().await;
1605        expire_on_flush_period().await;
1606        never_expire_when_flush_period_disabled().await;
1607    }
1608
1609    async fn prometheus_scrapes_metrics() {
1610        let start = Utc::now().timestamp();
1611
1612        let config = PrometheusExporterConfig {
1613            address: sink_exporter_address().parse().unwrap(),
1614            flush_period_secs: Duration::from_secs(2),
1615            ..Default::default()
1616        };
1617        let (sink, _) = SinkConfig::build(&config, SinkContext::default())
1618            .await
1619            .unwrap();
1620        let (name, event) = tests::create_metric_gauge(None, 123.4);
1621        let (_, delayed_event) = tests::create_metric_gauge(Some("delayed".to_string()), 123.4);
1622
1623        run_and_assert_sink_compliance(
1624            sink,
1625            stream::once(ready(event)).chain(stream::once(async move {
1626                // Wait a bit for the prometheus server to scrape the metrics
1627                time::sleep(time::Duration::from_secs(2)).await;
1628                delayed_event
1629            })),
1630            &SINK_TAGS,
1631        )
1632        .await;
1633
1634        // Now try to download them from prometheus
1635        let result = prometheus_query(&name).await;
1636
1637        let data = &result["data"]["result"][0];
1638        assert_eq!(data["metric"]["__name__"], Value::String(name));
1639        assert_eq!(
1640            data["metric"]["instance"],
1641            Value::String(sink_exporter_address())
1642        );
1643        assert_eq!(
1644            data["metric"]["some_tag"],
1645            Value::String("some_value".into())
1646        );
1647        assert!(data["value"][0].as_f64().unwrap() >= start as f64);
1648        assert_eq!(data["value"][1], Value::String("123.4".into()));
1649    }
1650
1651    async fn reset_on_flush_period() {
1652        let config = PrometheusExporterConfig {
1653            address: sink_exporter_address().parse().unwrap(),
1654            flush_period_secs: Duration::from_secs(3),
1655            ..Default::default()
1656        };
1657        let (sink, _) = SinkConfig::build(&config, SinkContext::default())
1658            .await
1659            .unwrap();
1660        let (tx, rx) = mpsc::unbounded_channel();
1661        let input_events = UnboundedReceiverStream::new(rx);
1662
1663        let input_events = input_events.map(Into::into);
1664        let sink_handle = tokio::spawn(async move { sink.run(input_events).await.unwrap() });
1665
1666        // Create two sets with different names but the same size.
1667        let (name1, event) = tests::create_metric_set(None, vec!["0", "1", "2"]);
1668        tx.send(event).expect("Failed to send.");
1669        let (name2, event) = tests::create_metric_set(None, vec!["3", "4", "5"]);
1670        tx.send(event).expect("Failed to send.");
1671
1672        // Wait for the Prometheus server to scrape them, and then query it to ensure both metrics
1673        // have their correct set size value.
1674        time::sleep(time::Duration::from_secs(2)).await;
1675
1676        // Now query Prometheus to make sure we see them there.
1677        let result = prometheus_query(&name1).await;
1678        assert_eq!(
1679            result["data"]["result"][0]["value"][1],
1680            Value::String("3".into())
1681        );
1682        let result = prometheus_query(&name2).await;
1683        assert_eq!(
1684            result["data"]["result"][0]["value"][1],
1685            Value::String("3".into())
1686        );
1687
1688        // Wait a few more seconds to ensure that the two original sets have logically expired.
1689        // We'll update `name2` but not `name1`, which should lead to both being expired, but
1690        // `name2` being recreated with two values only, while `name1` is entirely gone.
1691        time::sleep(time::Duration::from_secs(3)).await;
1692
1693        let (name2, event) = tests::create_metric_set(Some(name2), vec!["8", "9"]);
1694        tx.send(event).expect("Failed to send.");
1695
1696        // Again, wait for the Prometheus server to scrape the metrics, and then query it again.
1697        time::sleep(time::Duration::from_secs(2)).await;
1698        let result = prometheus_query(&name1).await;
1699        assert_eq!(result["data"]["result"][0]["value"][1], Value::Null);
1700        let result = prometheus_query(&name2).await;
1701        assert_eq!(
1702            result["data"]["result"][0]["value"][1],
1703            Value::String("2".into())
1704        );
1705
1706        drop(tx);
1707        sink_handle.await.unwrap();
1708    }
1709
1710    async fn expire_on_flush_period() {
1711        let config = PrometheusExporterConfig {
1712            address: sink_exporter_address().parse().unwrap(),
1713            flush_period_secs: Duration::from_secs(3),
1714            ..Default::default()
1715        };
1716        let (sink, _) = SinkConfig::build(&config, SinkContext::default())
1717            .await
1718            .unwrap();
1719        let (tx, rx) = mpsc::unbounded_channel();
1720        let input_events = UnboundedReceiverStream::new(rx);
1721
1722        let input_events = input_events.map(Into::into);
1723        let sink_handle = tokio::spawn(async move { sink.run(input_events).await.unwrap() });
1724
1725        // metrics that will not be updated for a full flush period and therefore should expire
1726        let (name1, event) = tests::create_metric_set(None, vec!["42"]);
1727        tx.send(event).expect("Failed to send.");
1728        let (name2, event) = tests::create_metric_gauge(None, 100.0);
1729        tx.send(event).expect("Failed to send.");
1730
1731        // Wait a bit for the sink to process the events
1732        time::sleep(time::Duration::from_secs(1)).await;
1733
1734        // Exporter should present both metrics at first
1735        let body = fetch_exporter_body().await;
1736        assert!(body.contains(&name1));
1737        assert!(body.contains(&name2));
1738
1739        // Wait long enough to put us past flush_period_secs for the metric that wasn't updated
1740        for _ in 0..7 {
1741            // Update the first metric, ensuring it doesn't expire
1742            let (_, event) = tests::create_metric_set(Some(name1.clone()), vec!["43"]);
1743            tx.send(event).expect("Failed to send.");
1744
1745            // Wait a bit for time to pass
1746            time::sleep(time::Duration::from_secs(1)).await;
1747        }
1748
1749        // Exporter should present only the one that got updated
1750        let body = fetch_exporter_body().await;
1751        assert!(body.contains(&name1));
1752        assert!(!body.contains(&name2));
1753
1754        drop(tx);
1755        sink_handle.await.unwrap();
1756    }
1757
1758    async fn never_expire_when_flush_period_disabled() {
1759        let config = PrometheusExporterConfig {
1760            address: sink_exporter_address().parse().unwrap(),
1761            // A flush period of zero disables metric expiration entirely.
1762            flush_period_secs: Duration::from_secs(0),
1763            ..Default::default()
1764        };
1765        let (sink, _) = SinkConfig::build(&config, SinkContext::default())
1766            .await
1767            .unwrap();
1768        let (tx, rx) = mpsc::unbounded_channel();
1769        let input_events = UnboundedReceiverStream::new(rx);
1770
1771        let input_events = input_events.map(Into::into);
1772        let sink_handle = tokio::spawn(async move { sink.run(input_events).await.unwrap() });
1773
1774        // This metric is never updated again after this point. With expiration disabled it
1775        // should still be present long after it would normally have expired.
1776        let (name, event) = tests::create_metric_set(None, vec!["42"]);
1777        tx.send(event).expect("Failed to send.");
1778
1779        // Wait a bit for the sink to process the event.
1780        time::sleep(time::Duration::from_secs(1)).await;
1781
1782        let body = fetch_exporter_body().await;
1783        assert!(body.contains(&name));
1784
1785        // Wait well past what would be the default/any reasonable flush period, sending other
1786        // metrics along the way to trigger sweeps that would otherwise remove `name`.
1787        for _ in 0..5 {
1788            let (_, event) = tests::create_metric_gauge(None, 1.0);
1789            tx.send(event).expect("Failed to send.");
1790
1791            time::sleep(time::Duration::from_secs(1)).await;
1792        }
1793
1794        let body = fetch_exporter_body().await;
1795        assert!(body.contains(&name));
1796
1797        drop(tx);
1798        sink_handle.await.unwrap();
1799    }
1800}