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