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#[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 #[serde(alias = "namespace")]
73 pub default_namespace: Option<String>,
74
75 #[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 #[serde(default = "super::default_histogram_buckets")]
92 pub buckets: Vec<f64>,
93
94 #[serde(default = "super::default_summary_quantiles")]
98 pub quantiles: Vec<f64>,
99
100 #[serde(default = "default_distributions_as_summaries")]
110 pub distributions_as_summaries: bool,
111
112 #[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 #[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#[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 pub fn refresh(&mut self) {
256 self.expires_at = Instant::now() + self.expiration_window;
257 }
258
259 pub fn has_expired(&self, now: Instant) -> bool {
263 !self.expiration_window.is_zero() && now >= self.expires_at
264 }
265}
266
267#[derive(Clone, Debug)]
276struct MetricRef {
277 series: MetricSeries,
278 value: Discriminant<MetricValue>,
279 bounds: Option<Vec<f64>>,
280}
281
282impl MetricRef {
283 pub fn from_metric(metric: &Metric) -> Self {
285 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 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 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 return Some(new_metric.with_value(current).into_absolute());
538 }
539 }
540
541 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 !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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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)] #![allow(clippy::print_stderr)] #![allow(clippy::dbg_macro)] 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 time::sleep(time::Duration::from_secs(2)).await;
1628 delayed_event
1629 })),
1630 &SINK_TAGS,
1631 )
1632 .await;
1633
1634 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 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 time::sleep(time::Duration::from_secs(2)).await;
1675
1676 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 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 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 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 time::sleep(time::Duration::from_secs(1)).await;
1733
1734 let body = fetch_exporter_body().await;
1736 assert!(body.contains(&name1));
1737 assert!(body.contains(&name2));
1738
1739 for _ in 0..7 {
1741 let (_, event) = tests::create_metric_set(Some(name1.clone()), vec!["43"]);
1743 tx.send(event).expect("Failed to send.");
1744
1745 time::sleep(time::Duration::from_secs(1)).await;
1747 }
1748
1749 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 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 let (name, event) = tests::create_metric_set(None, vec!["42"]);
1777 tx.send(event).expect("Failed to send.");
1778
1779 time::sleep(time::Duration::from_secs(1)).await;
1781
1782 let body = fetch_exporter_body().await;
1783 assert!(body.contains(&name));
1784
1785 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}