1use std::{collections::HashMap, future::ready, task::Poll};
2
3use bytes::{Bytes, BytesMut};
4use futures::{SinkExt, future::BoxFuture, stream};
5use indoc::indoc;
6use tower::Service;
7use vector_lib::{
8 ByteSizeOf, EstimatedJsonEncodedSizeOf,
9 configurable::configurable_component,
10 event::metric::{MetricSketch, MetricTags, Quantile},
11 sensitive_string::SensitiveString,
12};
13
14use crate::{
15 config::{AcknowledgementsConfig, GenerateConfig, Input, SinkConfig, SinkContext},
16 event::{
17 Event, KeyString,
18 metric::{Metric, MetricValue, Sample, StatisticKind},
19 },
20 http::HttpClient,
21 internal_events::InfluxdbEncodingError,
22 sinks::{
23 Healthcheck, VectorSink,
24 influxdb::{
25 Field, InfluxDb1Settings, InfluxDb2Settings, InfluxDbSettings, InfluxDbVersion,
26 ProtocolVersion, encode_timestamp, healthcheck, influx_line_protocol,
27 influxdb_settings,
28 },
29 util::{
30 BatchConfig, EncodedEvent, HttpEndpoint, SinkBatchSettings, TowerRequestConfig,
31 buffer::metrics::{MetricNormalize, MetricNormalizer, MetricSet, MetricsBuffer},
32 encode_namespace,
33 http::{HttpBatchService, HttpRetryLogic},
34 statistic::{DistributionStatistic, validate_quantiles},
35 },
36 },
37 tls::{TlsConfig, TlsSettings},
38};
39
40#[derive(Clone)]
41struct InfluxDbSvc {
42 config: InfluxDbConfig,
43 protocol_version: ProtocolVersion,
44 inner: HttpBatchService<BoxFuture<'static, crate::Result<hyper::Request<Bytes>>>>,
45}
46
47#[derive(Clone, Copy, Debug, Default)]
48pub struct InfluxDbDefaultBatchSettings;
49
50impl SinkBatchSettings for InfluxDbDefaultBatchSettings {
51 const MAX_EVENTS: Option<usize> = Some(20);
52 const MAX_BYTES: Option<usize> = None;
53 const TIMEOUT_SECS: f64 = 1.0;
54}
55
56#[configurable_component(sink("influxdb_metrics", "Deliver metric event data to InfluxDB."))]
58#[derive(Clone, Debug)]
59#[serde(deny_unknown_fields)]
60pub struct InfluxDbConfig {
61 #[serde(alias = "namespace")]
66 #[configurable(metadata(docs::examples = "service"))]
67 pub default_namespace: Option<String>,
68
69 #[configurable(metadata(docs::examples = "http://localhost:8086/"))]
73 pub endpoint: HttpEndpoint,
74
75 #[configurable(metadata(docs::examples = "2"))]
80 #[configurable(metadata(docs::examples = "1"))]
81 #[configurable(metadata(docs::minimal = true))]
82 pub version: Option<InfluxDbVersion>,
83
84 #[configurable(metadata(docs::examples = "vector-database"))]
88 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
89 #[configurable(metadata(docs::required_when = "version = \"1\""))]
90 pub database: Option<String>,
91
92 #[configurable(metadata(docs::examples = "any"))]
96 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
97 pub consistency: Option<String>,
98
99 #[configurable(metadata(docs::examples = "autogen"))]
103 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
104 pub retention_policy_name: Option<String>,
105
106 #[configurable(metadata(docs::examples = "todd"))]
110 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
111 pub username: Option<String>,
112
113 #[configurable(metadata(docs::examples = "${INFLUXDB_PASSWORD}"))]
117 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
118 pub password: Option<SensitiveString>,
119
120 #[configurable(metadata(docs::examples = "my-org"))]
124 #[configurable(metadata(docs::relevant_when = "version = \"2\""))]
125 #[configurable(metadata(docs::required_when = "version = \"2\""))]
126 #[configurable(metadata(docs::minimal = true))]
127 pub org: Option<String>,
128
129 #[configurable(metadata(docs::examples = "vector-bucket"))]
133 #[configurable(metadata(docs::relevant_when = "version = \"2\""))]
134 #[configurable(metadata(docs::required_when = "version = \"2\""))]
135 #[configurable(metadata(docs::minimal = true))]
136 pub bucket: Option<String>,
137
138 #[configurable(metadata(docs::examples = "${INFLUXDB_TOKEN}"))]
144 #[configurable(metadata(docs::relevant_when = "version = \"2\""))]
145 #[configurable(metadata(docs::required_when = "version = \"2\""))]
146 #[configurable(metadata(docs::minimal = true))]
147 pub token: Option<SensitiveString>,
148
149 #[configurable(derived)]
150 #[serde(default)]
151 pub batch: BatchConfig<InfluxDbDefaultBatchSettings>,
152
153 #[configurable(derived)]
154 #[serde(default)]
155 pub request: TowerRequestConfig,
156
157 #[configurable(metadata(docs::additional_props_description = "A tag key/value pair."))]
159 #[configurable(metadata(docs::examples = "example_tags()"))]
160 pub tags: Option<HashMap<String, String>>,
161
162 #[configurable(derived)]
163 pub tls: Option<TlsConfig>,
164
165 #[serde(default = "default_summary_quantiles")]
167 pub quantiles: Vec<f64>,
168
169 #[configurable(derived)]
170 #[serde(
171 default,
172 deserialize_with = "crate::serde::bool_or_struct",
173 skip_serializing_if = "crate::serde::is_default"
174 )]
175 acknowledgements: AcknowledgementsConfig,
176}
177
178pub fn default_summary_quantiles() -> Vec<f64> {
179 vec![0.5, 0.75, 0.9, 0.95, 0.99]
180}
181
182pub fn example_tags() -> HashMap<String, String> {
183 HashMap::from([("region".to_string(), "us-west-1".to_string())])
184}
185
186impl GenerateConfig for InfluxDbConfig {
187 fn generate_config() -> serde_json::Value {
188 toml::from_str(indoc! {r#"
189 endpoint = "http://localhost:8086/"
190 version = "2"
191 org = "my-org"
192 bucket = "my-bucket"
193 token = "${INFLUXDB_TOKEN}"
194 "#})
195 .unwrap()
196 }
197}
198
199impl InfluxDbConfig {
200 fn settings(&self) -> crate::Result<InfluxDbSettings> {
201 let version = match self.version {
202 Some(version) => {
203 self.validate_version(version)?;
204 version
205 }
206 None => {
207 warn!(
208 "The `version` option is currently optional but will be required in a future release. \
209 Please set it to `1` or `2` to match your InfluxDB settings."
210 );
211 self.infer_version()?
212 }
213 };
214 match version {
215 InfluxDbVersion::V1 => Ok(InfluxDbSettings::V1(InfluxDb1Settings {
216 database: self
217 .database
218 .clone()
219 .ok_or("the `database` option is required when using InfluxDB v1")?,
220 consistency: self.consistency.clone(),
221 retention_policy_name: self.retention_policy_name.clone(),
222 username: self.username.clone(),
223 password: self.password.clone(),
224 })),
225 InfluxDbVersion::V2 => Ok(InfluxDbSettings::V2(InfluxDb2Settings {
226 org: self
227 .org
228 .clone()
229 .ok_or("the `org` option is required when using InfluxDB v2")?,
230 bucket: self
231 .bucket
232 .clone()
233 .ok_or("the `bucket` option is required when using InfluxDB v2")?,
234 token: self
235 .token
236 .clone()
237 .ok_or("the `token` option is required when using InfluxDB v2")?,
238 })),
239 }
240 }
241
242 const fn settings_present(&self) -> (bool, bool) {
243 let has_v1 = self.database.is_some()
244 || self.consistency.is_some()
245 || self.retention_policy_name.is_some()
246 || self.username.is_some()
247 || self.password.is_some();
248 let has_v2 = self.org.is_some() || self.bucket.is_some() || self.token.is_some();
249 (has_v1, has_v2)
250 }
251
252 fn infer_version(&self) -> crate::Result<InfluxDbVersion> {
253 let (has_v1, has_v2) = self.settings_present();
254 match (has_v1, has_v2) {
255 (true, true) => Err(
256 "Unclear settings. Both InfluxDB v1 and v2 settings are configured; configure only one version."
257 .into(),
258 ),
259 (false, false) => Err("InfluxDB v1 or v2 should be configured as endpoint.".into()),
260 (true, false) => Ok(InfluxDbVersion::V1),
261 (false, true) => Ok(InfluxDbVersion::V2),
262 }
263 }
264
265 fn validate_version(&self, version: InfluxDbVersion) -> crate::Result<()> {
268 let (has_v1, has_v2) = self.settings_present();
269 match version {
270 InfluxDbVersion::V1 if has_v2 => Err(
271 "InfluxDB v1 settings are configured, but v2 settings were also provided; configure only one version."
272 .into(),
273 ),
274 InfluxDbVersion::V2 if has_v1 => Err(
275 "InfluxDB v2 settings are configured, but v1 settings were also provided; configure only one version."
276 .into(),
277 ),
278 _ => Ok(()),
279 }
280 }
281}
282
283#[async_trait::async_trait]
284#[typetag::serde(name = "influxdb_metrics")]
285impl SinkConfig for InfluxDbConfig {
286 async fn build(&self, cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
287 let tls_settings = TlsSettings::from_options(self.tls.as_ref())?;
288 let client = HttpClient::new(tls_settings, cx.proxy())?;
289 let healthcheck = healthcheck(self.clone().endpoint, self.settings()?, client.clone())?;
290 validate_quantiles(&self.quantiles)?;
291 let sink = InfluxDbSvc::new(self.clone(), client)?;
292 Ok((sink, healthcheck))
293 }
294
295 fn input(&self) -> Input {
296 Input::metric()
297 }
298
299 fn acknowledgements(&self) -> &AcknowledgementsConfig {
300 &self.acknowledgements
301 }
302}
303
304impl InfluxDbSvc {
305 pub fn new(config: InfluxDbConfig, client: HttpClient) -> crate::Result<VectorSink> {
306 let settings = influxdb_settings(config.settings()?);
307
308 let endpoint = config.endpoint.clone();
309 let token = settings.token();
310 let protocol_version = settings.protocol_version();
311
312 let batch = config.batch.into_batch_settings()?;
313 let request = config.request.into_settings();
314
315 let uri = settings.write_uri(endpoint)?;
316
317 let http_service = HttpBatchService::new(client, create_build_request(uri, token.inner()));
318
319 let influxdb_http_service = InfluxDbSvc {
320 config,
321 protocol_version,
322 inner: http_service,
323 };
324 let mut normalizer = MetricNormalizer::<InfluxMetricNormalize>::default();
325
326 let sink = request
327 .batch_sink(
328 HttpRetryLogic::default(),
329 influxdb_http_service,
330 MetricsBuffer::new(batch.size),
331 batch.timeout,
332 )
333 .with_flat_map(move |event: Event| {
334 stream::iter({
335 let byte_size = event.size_of();
336 let json_size = event.estimated_json_encoded_size_of();
337
338 normalizer
339 .normalize(event.into_metric())
340 .map(|metric| Ok(EncodedEvent::new(metric, byte_size, json_size)))
341 })
342 })
343 .sink_map_err(|error| error!(message = "Fatal influxdb sink error.", %error, internal_log_rate_limit = false));
344
345 #[allow(deprecated)]
346 Ok(VectorSink::from_event_sink(sink))
347 }
348}
349
350impl Service<Vec<Metric>> for InfluxDbSvc {
351 type Response = http::Response<Bytes>;
352 type Error = crate::Error;
353 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
354
355 fn poll_ready(&mut self, cx: &mut std::task::Context) -> Poll<Result<(), Self::Error>> {
357 self.inner.poll_ready(cx)
358 }
359
360 fn call(&mut self, items: Vec<Metric>) -> Self::Future {
362 let input = encode_events(
363 self.protocol_version,
364 items,
365 self.config.default_namespace.as_deref(),
366 self.config.tags.as_ref(),
367 &self.config.quantiles,
368 );
369 let body = input.freeze();
370
371 self.inner.call(body)
372 }
373}
374
375fn create_build_request(
376 uri: http::Uri,
377 token: &str,
378) -> impl Fn(Bytes) -> BoxFuture<'static, crate::Result<hyper::Request<Bytes>>>
379+ Sync
380+ Send
381+ 'static
382+ use<> {
383 let auth = format!("Token {token}");
384 move |body| {
385 Box::pin(ready(
386 hyper::Request::post(uri.clone())
387 .header("Content-Type", "text/plain")
388 .header("Authorization", auth.clone())
389 .body(body)
390 .map_err(Into::into),
391 ))
392 }
393}
394
395fn merge_tags(event: &Metric, tags: Option<&HashMap<String, String>>) -> Option<MetricTags> {
396 match (event.tags().cloned(), tags) {
397 (Some(mut event_tags), Some(config_tags)) => {
398 event_tags.extend(config_tags.iter().map(|(k, v)| (k.clone(), v.clone())));
399 Some(event_tags)
400 }
401 (Some(event_tags), None) => Some(event_tags),
402 (None, Some(config_tags)) => Some(
403 config_tags
404 .iter()
405 .map(|(k, v)| (k.clone(), v.clone()))
406 .collect(),
407 ),
408 (None, None) => None,
409 }
410}
411
412#[derive(Default)]
413pub struct InfluxMetricNormalize;
414
415impl MetricNormalize for InfluxMetricNormalize {
416 fn normalize(&mut self, state: &mut MetricSet, metric: Metric) -> Option<Metric> {
417 match (metric.kind(), &metric.value()) {
418 (_, MetricValue::Counter { .. }) => state.make_incremental(metric),
421 (_, MetricValue::Gauge { .. }) => state.make_absolute(metric),
423 _ => Some(metric),
425 }
426 }
427}
428
429fn encode_events(
430 protocol_version: ProtocolVersion,
431 events: Vec<Metric>,
432 default_namespace: Option<&str>,
433 tags: Option<&HashMap<String, String>>,
434 quantiles: &[f64],
435) -> BytesMut {
436 let mut output = BytesMut::new();
437 let count = events.len();
438
439 for event in events.into_iter() {
440 let fullname = encode_namespace(event.namespace().or(default_namespace), '.', event.name());
441 let ts = encode_timestamp(event.timestamp());
442 let tags = merge_tags(&event, tags);
443 let (metric_type, fields) = get_type_and_fields(event.value(), quantiles);
444
445 let mut unwrapped_tags = tags.unwrap_or_default();
446 unwrapped_tags.replace("metric_type".to_owned(), metric_type.to_owned());
447
448 if let Err(error_message) = influx_line_protocol(
449 protocol_version,
450 &fullname,
451 Some(unwrapped_tags),
452 fields,
453 ts,
454 &mut output,
455 ) {
456 emit!(InfluxdbEncodingError {
457 error_message,
458 count,
459 });
460 };
461 }
462
463 if !output.is_empty() {
465 output.truncate(output.len() - 1);
466 }
467 output
468}
469
470fn get_type_and_fields(
471 value: &MetricValue,
472 quantiles: &[f64],
473) -> (&'static str, Option<HashMap<KeyString, Field>>) {
474 match value {
475 MetricValue::Counter { value } => ("counter", Some(to_fields(*value))),
476 MetricValue::Gauge { value } => ("gauge", Some(to_fields(*value))),
477 MetricValue::Set { values } => ("set", Some(to_fields(values.len() as f64))),
478 MetricValue::AggregatedHistogram {
479 buckets,
480 count,
481 sum,
482 } => {
483 let mut fields: HashMap<KeyString, Field> = buckets
484 .iter()
485 .map(|sample| {
486 (
487 format!("bucket_{}", sample.upper_limit).into(),
488 Field::UnsignedInt(sample.count),
489 )
490 })
491 .collect();
492 fields.insert("count".into(), Field::UnsignedInt(*count));
493 fields.insert("sum".into(), Field::Float(*sum));
494
495 ("histogram", Some(fields))
496 }
497 MetricValue::AggregatedSummary {
498 quantiles,
499 count,
500 sum,
501 } => {
502 let mut fields: HashMap<KeyString, Field> = quantiles
503 .iter()
504 .map(|quantile| {
505 (
506 format!("quantile_{}", quantile.quantile).into(),
507 Field::Float(quantile.value),
508 )
509 })
510 .collect();
511 fields.insert("count".into(), Field::UnsignedInt(*count));
512 fields.insert("sum".into(), Field::Float(*sum));
513
514 ("summary", Some(fields))
515 }
516 MetricValue::Distribution { samples, statistic } => {
517 let quantiles = match statistic {
518 StatisticKind::Histogram => &[0.95] as &[_],
519 StatisticKind::Summary => quantiles,
520 };
521 let fields = encode_distribution(samples, quantiles);
522 ("distribution", fields)
523 }
524 MetricValue::Sketch { sketch } => match sketch {
525 MetricSketch::AgentDDSketch(ddsketch) => {
526 let mut fields = [0.5, 0.75, 0.9, 0.99]
529 .iter()
530 .map(|q| {
531 let quantile = Quantile {
532 quantile: *q,
533 value: ddsketch.quantile(*q).unwrap_or(0.0),
534 };
535 (
536 quantile.to_percentile_string().into(),
537 Field::Float(quantile.value),
538 )
539 })
540 .collect::<HashMap<KeyString, _>>();
541 fields.insert(
542 "count".into(),
543 Field::UnsignedInt(u64::from(ddsketch.count())),
544 );
545 fields.insert(
546 "min".into(),
547 Field::Float(ddsketch.min().unwrap_or(f64::MAX)),
548 );
549 fields.insert(
550 "max".into(),
551 Field::Float(ddsketch.max().unwrap_or(f64::MIN)),
552 );
553 fields.insert("sum".into(), Field::Float(ddsketch.sum().unwrap_or(0.0)));
554 fields.insert("avg".into(), Field::Float(ddsketch.avg().unwrap_or(0.0)));
555
556 ("sketch", Some(fields))
557 }
558 },
559 }
560}
561
562fn encode_distribution(samples: &[Sample], quantiles: &[f64]) -> Option<HashMap<KeyString, Field>> {
563 let statistic = DistributionStatistic::from_samples(samples, quantiles)?;
564
565 Some(
566 [
567 ("min".into(), Field::Float(statistic.min)),
568 ("max".into(), Field::Float(statistic.max)),
569 ("median".into(), Field::Float(statistic.median)),
570 ("avg".into(), Field::Float(statistic.avg)),
571 ("sum".into(), Field::Float(statistic.sum)),
572 ("count".into(), Field::Float(statistic.count as f64)),
573 ]
574 .into_iter()
575 .chain(
576 statistic
577 .quantiles
578 .iter()
579 .map(|&(p, val)| (format!("quantile_{p:.2}").into(), Field::Float(val))),
580 )
581 .collect(),
582 )
583}
584
585fn to_fields(value: f64) -> HashMap<KeyString, Field> {
586 [("value".into(), Field::Float(value))]
587 .into_iter()
588 .collect()
589}
590
591#[cfg(test)]
592mod tests {
593 use indoc::indoc;
594 use similar_asserts::assert_eq;
595
596 use super::*;
597 use crate::{
598 event::metric::{Metric, MetricKind, MetricValue, StatisticKind},
599 sinks::influxdb::test_util::{assert_fields, split_line_protocol, tags, ts},
600 };
601
602 #[test]
603 fn generate_config() {
604 crate::test_util::test_generate_config::<InfluxDbConfig>();
605 }
606
607 #[test]
608 fn test_config_with_tags() {
609 let config = indoc! {r#"
610 namespace: "vector"
611 endpoint: "http://localhost:9999"
612 version: "2"
613 bucket: "my-bucket"
614 org: "my-org"
615 token: "my-token"
616 tags:
617 region: "us-west-1"
618 "#};
619
620 serde_yaml::from_str::<InfluxDbConfig>(config).unwrap();
621 }
622
623 #[test]
624 fn test_settings_explicit_v2_rejects_v1_settings() {
625 let config = indoc! {r#"
626 endpoint: "http://localhost:9999"
627 version: "2"
628 bucket: "my-bucket"
629 org: "my-org"
630 token: "my-token"
631 database: "stale-v1-database"
632 username: "stale-v1-user"
633 "#};
634 let config: InfluxDbConfig = serde_yaml::from_str(config).unwrap();
635 assert!(config.settings().is_err());
636 }
637
638 #[test]
639 fn test_settings_explicit_v1_rejects_v2_settings() {
640 let config = indoc! {r#"
641 endpoint: "http://localhost:9999"
642 version: "1"
643 database: "my-database"
644 org: "stale-v2-org"
645 bucket: "stale-v2-bucket"
646 token: "stale-v2-token"
647 "#};
648 let config: InfluxDbConfig = serde_yaml::from_str(config).unwrap();
649 assert!(config.settings().is_err());
650 }
651
652 #[test]
653 fn test_settings_explicit_v2_accepts_only_v2_settings() {
654 let config = indoc! {r#"
655 endpoint: "http://localhost:9999"
656 version: "2"
657 bucket: "my-bucket"
658 org: "my-org"
659 token: "my-token"
660 "#};
661 let config: InfluxDbConfig = serde_yaml::from_str(config).unwrap();
662 assert!(config.settings().is_ok());
663 }
664
665 #[test]
666 fn test_settings_explicit_v1_accepts_only_v1_settings() {
667 let config = indoc! {r#"
668 endpoint: "http://localhost:9999"
669 version: "1"
670 database: "my-database"
671 "#};
672 let config: InfluxDbConfig = serde_yaml::from_str(config).unwrap();
673 assert!(config.settings().is_ok());
674 }
675
676 #[test]
677 fn test_encode_counter() {
678 let events = vec![
679 Metric::new(
680 "total",
681 MetricKind::Incremental,
682 MetricValue::Counter { value: 1.5 },
683 )
684 .with_namespace(Some("ns"))
685 .with_timestamp(Some(ts())),
686 Metric::new(
687 "check",
688 MetricKind::Incremental,
689 MetricValue::Counter { value: 1.0 },
690 )
691 .with_namespace(Some("ns"))
692 .with_tags(Some(tags()))
693 .with_timestamp(Some(ts())),
694 ];
695
696 let line_protocols = encode_events(ProtocolVersion::V2, events, Some("vector"), None, &[]);
697 assert_eq!(
698 line_protocols,
699 "ns.total,metric_type=counter value=1.5 1542182950000000011\n\
700 ns.check,metric_type=counter,normal_tag=value,true_tag=true value=1 1542182950000000011"
701 );
702 }
703
704 #[test]
705 fn test_encode_gauge() {
706 let events = vec![
707 Metric::new(
708 "meter",
709 MetricKind::Incremental,
710 MetricValue::Gauge { value: -1.5 },
711 )
712 .with_namespace(Some("ns"))
713 .with_tags(Some(tags()))
714 .with_timestamp(Some(ts())),
715 ];
716
717 let line_protocols = encode_events(ProtocolVersion::V2, events, None, None, &[]);
718 assert_eq!(
719 line_protocols,
720 "ns.meter,metric_type=gauge,normal_tag=value,true_tag=true value=-1.5 1542182950000000011"
721 );
722 }
723
724 #[test]
725 fn test_encode_set() {
726 let events = vec![
727 Metric::new(
728 "users",
729 MetricKind::Incremental,
730 MetricValue::Set {
731 values: vec!["alice".into(), "bob".into()].into_iter().collect(),
732 },
733 )
734 .with_namespace(Some("ns"))
735 .with_tags(Some(tags()))
736 .with_timestamp(Some(ts())),
737 ];
738
739 let line_protocols = encode_events(ProtocolVersion::V2, events, None, None, &[]);
740 assert_eq!(
741 line_protocols,
742 "ns.users,metric_type=set,normal_tag=value,true_tag=true value=2 1542182950000000011"
743 );
744 }
745
746 #[test]
747 fn test_encode_histogram_v1() {
748 let events = vec![
749 Metric::new(
750 "requests",
751 MetricKind::Absolute,
752 MetricValue::AggregatedHistogram {
753 buckets: vector_lib::buckets![1.0 => 1, 2.1 => 2, 3.0 => 3],
754 count: 6,
755 sum: 12.5,
756 },
757 )
758 .with_namespace(Some("ns"))
759 .with_tags(Some(tags()))
760 .with_timestamp(Some(ts())),
761 ];
762
763 let line_protocols = encode_events(ProtocolVersion::V1, events, None, None, &[]);
764 let line_protocols =
765 String::from_utf8(line_protocols.freeze().as_ref().to_owned()).unwrap();
766 let line_protocols: Vec<&str> = line_protocols.split('\n').collect();
767 assert_eq!(line_protocols.len(), 1);
768
769 let line_protocol1 = split_line_protocol(line_protocols[0]);
770 assert_eq!("ns.requests", line_protocol1.0);
771 assert_eq!(
772 "metric_type=histogram,normal_tag=value,true_tag=true",
773 line_protocol1.1
774 );
775 assert_fields(
776 line_protocol1.2.to_string(),
777 [
778 "bucket_1=1i",
779 "bucket_2.1=2i",
780 "bucket_3=3i",
781 "count=6i",
782 "sum=12.5",
783 ]
784 .to_vec(),
785 );
786 assert_eq!("1542182950000000011", line_protocol1.3);
787 }
788
789 #[test]
790 fn test_encode_histogram() {
791 let events = vec![
792 Metric::new(
793 "requests",
794 MetricKind::Absolute,
795 MetricValue::AggregatedHistogram {
796 buckets: vector_lib::buckets![1.0 => 1, 2.1 => 2, 3.0 => 3],
797 count: 6,
798 sum: 12.5,
799 },
800 )
801 .with_namespace(Some("ns"))
802 .with_tags(Some(tags()))
803 .with_timestamp(Some(ts())),
804 ];
805
806 let line_protocols = encode_events(ProtocolVersion::V2, events, None, None, &[]);
807 let line_protocols =
808 String::from_utf8(line_protocols.freeze().as_ref().to_owned()).unwrap();
809 let line_protocols: Vec<&str> = line_protocols.split('\n').collect();
810 assert_eq!(line_protocols.len(), 1);
811
812 let line_protocol1 = split_line_protocol(line_protocols[0]);
813 assert_eq!("ns.requests", line_protocol1.0);
814 assert_eq!(
815 "metric_type=histogram,normal_tag=value,true_tag=true",
816 line_protocol1.1
817 );
818 assert_fields(
819 line_protocol1.2.to_string(),
820 [
821 "bucket_1=1u",
822 "bucket_2.1=2u",
823 "bucket_3=3u",
824 "count=6u",
825 "sum=12.5",
826 ]
827 .to_vec(),
828 );
829 assert_eq!("1542182950000000011", line_protocol1.3);
830 }
831
832 #[test]
833 fn test_encode_summary_v1() {
834 let events = vec![
835 Metric::new(
836 "requests_sum",
837 MetricKind::Absolute,
838 MetricValue::AggregatedSummary {
839 quantiles: vector_lib::quantiles![0.01 => 1.5, 0.5 => 2.0, 0.99 => 3.0],
840 count: 6,
841 sum: 12.0,
842 },
843 )
844 .with_namespace(Some("ns"))
845 .with_tags(Some(tags()))
846 .with_timestamp(Some(ts())),
847 ];
848
849 let line_protocols = encode_events(ProtocolVersion::V1, events, None, None, &[]);
850 let line_protocols =
851 String::from_utf8(line_protocols.freeze().as_ref().to_owned()).unwrap();
852 let line_protocols: Vec<&str> = line_protocols.split('\n').collect();
853 assert_eq!(line_protocols.len(), 1);
854
855 let line_protocol1 = split_line_protocol(line_protocols[0]);
856 assert_eq!("ns.requests_sum", line_protocol1.0);
857 assert_eq!(
858 "metric_type=summary,normal_tag=value,true_tag=true",
859 line_protocol1.1
860 );
861 assert_fields(
862 line_protocol1.2.to_string(),
863 [
864 "count=6i",
865 "quantile_0.01=1.5",
866 "quantile_0.5=2",
867 "quantile_0.99=3",
868 "sum=12",
869 ]
870 .to_vec(),
871 );
872 assert_eq!("1542182950000000011", line_protocol1.3);
873 }
874
875 #[test]
876 fn test_encode_summary() {
877 let events = vec![
878 Metric::new(
879 "requests_sum",
880 MetricKind::Absolute,
881 MetricValue::AggregatedSummary {
882 quantiles: vector_lib::quantiles![0.01 => 1.5, 0.5 => 2.0, 0.99 => 3.0],
883 count: 6,
884 sum: 12.0,
885 },
886 )
887 .with_namespace(Some("ns"))
888 .with_tags(Some(tags()))
889 .with_timestamp(Some(ts())),
890 ];
891
892 let line_protocols = encode_events(ProtocolVersion::V2, events, None, None, &[]);
893 let line_protocols =
894 String::from_utf8(line_protocols.freeze().as_ref().to_owned()).unwrap();
895 let line_protocols: Vec<&str> = line_protocols.split('\n').collect();
896 assert_eq!(line_protocols.len(), 1);
897
898 let line_protocol1 = split_line_protocol(line_protocols[0]);
899 assert_eq!("ns.requests_sum", line_protocol1.0);
900 assert_eq!(
901 "metric_type=summary,normal_tag=value,true_tag=true",
902 line_protocol1.1
903 );
904 assert_fields(
905 line_protocol1.2.to_string(),
906 [
907 "count=6u",
908 "quantile_0.01=1.5",
909 "quantile_0.5=2",
910 "quantile_0.99=3",
911 "sum=12",
912 ]
913 .to_vec(),
914 );
915 assert_eq!("1542182950000000011", line_protocol1.3);
916 }
917
918 #[test]
919 fn test_encode_distribution() {
920 let events = vec![
921 Metric::new(
922 "requests",
923 MetricKind::Incremental,
924 MetricValue::Distribution {
925 samples: vector_lib::samples![1.0 => 3, 2.0 => 3, 3.0 => 2],
926 statistic: StatisticKind::Histogram,
927 },
928 )
929 .with_namespace(Some("ns"))
930 .with_tags(Some(tags()))
931 .with_timestamp(Some(ts())),
932 Metric::new(
933 "dense_stats",
934 MetricKind::Incremental,
935 MetricValue::Distribution {
936 samples: (0..20)
937 .map(|v| Sample {
938 value: f64::from(v),
939 rate: 1,
940 })
941 .collect(),
942 statistic: StatisticKind::Histogram,
943 },
944 )
945 .with_namespace(Some("ns"))
946 .with_timestamp(Some(ts())),
947 Metric::new(
948 "sparse_stats",
949 MetricKind::Incremental,
950 MetricValue::Distribution {
951 samples: (1..5)
952 .map(|v| Sample {
953 value: f64::from(v),
954 rate: v,
955 })
956 .collect(),
957 statistic: StatisticKind::Histogram,
958 },
959 )
960 .with_namespace(Some("ns"))
961 .with_timestamp(Some(ts())),
962 ];
963
964 let line_protocols = encode_events(ProtocolVersion::V2, events, None, None, &[]);
965 let line_protocols =
966 String::from_utf8(line_protocols.freeze().as_ref().to_owned()).unwrap();
967 let line_protocols: Vec<&str> = line_protocols.split('\n').collect();
968 assert_eq!(line_protocols.len(), 3);
969
970 let line_protocol1 = split_line_protocol(line_protocols[0]);
971 assert_eq!("ns.requests", line_protocol1.0);
972 assert_eq!(
973 "metric_type=distribution,normal_tag=value,true_tag=true",
974 line_protocol1.1
975 );
976 assert_fields(
977 line_protocol1.2.to_string(),
978 [
979 "avg=1.875",
980 "count=8",
981 "max=3",
982 "median=2",
983 "min=1",
984 "quantile_0.95=3",
985 "sum=15",
986 ]
987 .to_vec(),
988 );
989 assert_eq!("1542182950000000011", line_protocol1.3);
990
991 let line_protocol2 = split_line_protocol(line_protocols[1]);
992 assert_eq!("ns.dense_stats", line_protocol2.0);
993 assert_eq!("metric_type=distribution", line_protocol2.1);
994 assert_fields(
995 line_protocol2.2.to_string(),
996 [
997 "avg=9.5",
998 "count=20",
999 "max=19",
1000 "median=9",
1001 "min=0",
1002 "quantile_0.95=18",
1003 "sum=190",
1004 ]
1005 .to_vec(),
1006 );
1007 assert_eq!("1542182950000000011", line_protocol2.3);
1008
1009 let line_protocol3 = split_line_protocol(line_protocols[2]);
1010 assert_eq!("ns.sparse_stats", line_protocol3.0);
1011 assert_eq!("metric_type=distribution", line_protocol3.1);
1012 assert_fields(
1013 line_protocol3.2.to_string(),
1014 [
1015 "avg=3",
1016 "count=10",
1017 "max=4",
1018 "median=3",
1019 "min=1",
1020 "quantile_0.95=4",
1021 "sum=30",
1022 ]
1023 .to_vec(),
1024 );
1025 assert_eq!("1542182950000000011", line_protocol3.3);
1026 }
1027
1028 #[test]
1029 fn test_encode_distribution_empty_stats() {
1030 let events = vec![
1031 Metric::new(
1032 "requests",
1033 MetricKind::Incremental,
1034 MetricValue::Distribution {
1035 samples: vec![],
1036 statistic: StatisticKind::Histogram,
1037 },
1038 )
1039 .with_namespace(Some("ns"))
1040 .with_tags(Some(tags()))
1041 .with_timestamp(Some(ts())),
1042 ];
1043
1044 let line_protocols = encode_events(ProtocolVersion::V2, events, None, None, &[]);
1045 assert_eq!(line_protocols.len(), 0);
1046 }
1047
1048 #[test]
1049 fn test_encode_distribution_zero_counts_stats() {
1050 let events = vec![
1051 Metric::new(
1052 "requests",
1053 MetricKind::Incremental,
1054 MetricValue::Distribution {
1055 samples: vector_lib::samples![1.0 => 0, 2.0 => 0],
1056 statistic: StatisticKind::Histogram,
1057 },
1058 )
1059 .with_namespace(Some("ns"))
1060 .with_tags(Some(tags()))
1061 .with_timestamp(Some(ts())),
1062 ];
1063
1064 let line_protocols = encode_events(ProtocolVersion::V2, events, None, None, &[]);
1065 assert_eq!(line_protocols.len(), 0);
1066 }
1067
1068 #[test]
1069 fn test_encode_distribution_summary() {
1070 let events = vec![
1071 Metric::new(
1072 "requests",
1073 MetricKind::Incremental,
1074 MetricValue::Distribution {
1075 samples: vector_lib::samples![1.0 => 3, 2.0 => 3, 3.0 => 2],
1076 statistic: StatisticKind::Summary,
1077 },
1078 )
1079 .with_namespace(Some("ns"))
1080 .with_tags(Some(tags()))
1081 .with_timestamp(Some(ts())),
1082 ];
1083
1084 let line_protocols = encode_events(
1085 ProtocolVersion::V2,
1086 events,
1087 None,
1088 None,
1089 &default_summary_quantiles(),
1090 );
1091 let line_protocols =
1092 String::from_utf8(line_protocols.freeze().as_ref().to_owned()).unwrap();
1093 let line_protocols: Vec<&str> = line_protocols.split('\n').collect();
1094 assert_eq!(line_protocols.len(), 1);
1095
1096 let line_protocol = split_line_protocol(line_protocols[0]);
1097 assert_eq!("ns.requests", line_protocol.0);
1098 assert_eq!(
1099 "metric_type=distribution,normal_tag=value,true_tag=true",
1100 line_protocol.1
1101 );
1102 assert_fields(
1103 line_protocol.2.to_string(),
1104 [
1105 "avg=1.875",
1106 "count=8",
1107 "max=3",
1108 "median=2",
1109 "min=1",
1110 "sum=15",
1111 "quantile_0.50=2",
1112 "quantile_0.75=2",
1113 "quantile_0.90=3",
1114 "quantile_0.95=3",
1115 "quantile_0.99=3",
1116 ]
1117 .to_vec(),
1118 );
1119 assert_eq!("1542182950000000011", line_protocol.3);
1120 }
1121
1122 #[test]
1123 fn test_encode_with_some_tags() {
1124 crate::test_util::trace_init();
1125
1126 let events = vec![
1127 Metric::new(
1128 "cpu",
1129 MetricKind::Absolute,
1130 MetricValue::Gauge { value: 2.5 },
1131 )
1132 .with_namespace(Some("vector"))
1133 .with_timestamp(Some(ts())),
1134 Metric::new(
1135 "mem",
1136 MetricKind::Absolute,
1137 MetricValue::Gauge { value: 1000.0 },
1138 )
1139 .with_namespace(Some("vector"))
1140 .with_tags(Some(tags()))
1141 .with_timestamp(Some(ts())),
1142 ];
1143
1144 let mut tags = HashMap::new();
1145 tags.insert("host".to_owned(), "local".to_owned());
1146 tags.insert("datacenter".to_owned(), "us-east".to_owned());
1147
1148 let line_protocols = encode_events(
1149 ProtocolVersion::V1,
1150 events,
1151 Some("ns"),
1152 Some(tags).as_ref(),
1153 &[],
1154 );
1155 let line_protocols =
1156 String::from_utf8(line_protocols.freeze().as_ref().to_owned()).unwrap();
1157 let line_protocols: Vec<&str> = line_protocols.split('\n').collect();
1158 assert_eq!(line_protocols.len(), 2);
1159 assert_eq!(
1160 line_protocols[0],
1161 "vector.cpu,datacenter=us-east,host=local,metric_type=gauge value=2.5 1542182950000000011"
1162 );
1163 assert_eq!(
1164 line_protocols[1],
1165 "vector.mem,datacenter=us-east,host=local,metric_type=gauge,normal_tag=value,true_tag=true value=1000 1542182950000000011"
1166 );
1167 }
1168}
1169
1170#[cfg(feature = "influxdb-integration-tests")]
1171#[cfg(test)]
1172mod integration_tests {
1173 use chrono::{SecondsFormat, Utc};
1174 use futures::stream;
1175 use similar_asserts::assert_eq;
1176 use vector_lib::metric_tags;
1177
1178 use crate::{
1179 config::{SinkConfig, SinkContext},
1180 event::{
1181 Event,
1182 metric::{Metric, MetricKind, MetricValue},
1183 },
1184 http::HttpClient,
1185 sinks::influxdb::{
1186 InfluxDbVersion,
1187 metrics::{InfluxDbConfig, InfluxDbSvc, default_summary_quantiles},
1188 test_util::{
1189 BUCKET, ORG, TOKEN, address_v1, address_v2, cleanup_v1, format_timestamp,
1190 onboarding_v1, onboarding_v2, query_v1,
1191 },
1192 },
1193 sinks::util::HttpEndpoint,
1194 test_util::components::{HTTP_SINK_TAGS, run_and_assert_sink_compliance},
1195 tls::{self, TlsConfig},
1196 };
1197
1198 #[tokio::test]
1199 async fn inserts_metrics_v1_over_https() {
1200 insert_metrics_v1(
1201 address_v1(true).as_str(),
1202 Some(TlsConfig {
1203 ca_file: Some(tls::TEST_PEM_CA_PATH.into()),
1204 ..Default::default()
1205 }),
1206 )
1207 .await
1208 }
1209
1210 #[tokio::test]
1211 async fn inserts_metrics_v1_over_http() {
1212 insert_metrics_v1(address_v1(false).as_str(), None).await
1213 }
1214
1215 async fn insert_metrics_v1(url: &str, tls: Option<TlsConfig>) {
1216 crate::test_util::trace_init();
1217 let database = onboarding_v1(url).await;
1218
1219 let cx = SinkContext::default();
1220
1221 let config = InfluxDbConfig {
1222 endpoint: HttpEndpoint::parse(url).unwrap(),
1223 version: Some(InfluxDbVersion::V1),
1224 database: Some(database.clone()),
1225 consistency: None,
1226 retention_policy_name: Some("autogen".to_string()),
1227 username: None,
1228 password: None,
1229 org: None,
1230 bucket: None,
1231 token: None,
1232 batch: Default::default(),
1233 request: Default::default(),
1234 tls,
1235 quantiles: default_summary_quantiles(),
1236 tags: None,
1237 default_namespace: None,
1238 acknowledgements: Default::default(),
1239 };
1240
1241 let events: Vec<_> = (0..10).map(create_event).collect();
1242 let (sink, _) = config.build(cx).await.expect("error when building config");
1243 run_and_assert_sink_compliance(sink, stream::iter(events.clone()), &HTTP_SINK_TAGS).await;
1244
1245 let res = query_v1_json(url, &format!("show series on {database}")).await;
1246
1247 assert_eq!(
1264 res["results"][0]["series"][0]["values"]
1265 .as_array()
1266 .unwrap()
1267 .len(),
1268 events.len()
1269 );
1270
1271 for event in events {
1272 let metric = event.into_metric();
1273 let name = format!("{}.{}", metric.namespace().unwrap(), metric.name());
1274 let value = match metric.value() {
1275 MetricValue::Counter { value } => *value,
1276 _ => unreachable!(),
1277 };
1278 let timestamp = format_timestamp(metric.timestamp().unwrap(), SecondsFormat::Nanos);
1279 let res = query_v1_json(url, &format!("select * from {database}..\"{name}\"")).await;
1280
1281 assert_eq!(
1282 res,
1283 serde_json::json! {
1284 {"results": [{
1285 "statement_id": 0,
1286 "series": [{
1287 "name": name,
1288 "columns": ["time", "metric_type", "production", "region", "value"],
1289 "values": [[timestamp, "counter", "true", "us-west-1", value as isize]]
1290 }]
1291 }]}
1292 }
1293 );
1294 }
1295
1296 cleanup_v1(url, &database).await;
1297 }
1298
1299 async fn query_v1_json(url: &str, query: &str) -> serde_json::Value {
1300 let string = query_v1(url, query)
1301 .await
1302 .text()
1303 .await
1304 .expect("Fetching text from InfluxDB query failed");
1305 serde_json::from_str(&string).expect("Error when parsing InfluxDB response JSON")
1306 }
1307
1308 #[tokio::test]
1309 async fn influxdb2_metrics_put_data() {
1310 crate::test_util::trace_init();
1311 let endpoint = address_v2();
1312 onboarding_v2(&endpoint).await;
1313
1314 let cx = SinkContext::default();
1315
1316 let config = InfluxDbConfig {
1317 endpoint: HttpEndpoint::parse(&endpoint).unwrap(),
1318 version: Some(InfluxDbVersion::V2),
1319 database: None,
1320 consistency: None,
1321 retention_policy_name: None,
1322 username: None,
1323 password: None,
1324 org: Some(ORG.to_string()),
1325 bucket: Some(BUCKET.to_string()),
1326 token: Some(TOKEN.to_string().into()),
1327 quantiles: default_summary_quantiles(),
1328 batch: Default::default(),
1329 request: Default::default(),
1330 tags: None,
1331 tls: None,
1332 default_namespace: None,
1333 acknowledgements: Default::default(),
1334 };
1335
1336 let metric = format!(
1337 "counter-{}",
1338 Utc::now()
1339 .timestamp_nanos_opt()
1340 .expect("Timestamp out of range")
1341 );
1342 let mut events = Vec::new();
1343 for i in 0..10 {
1344 let event = Event::Metric(
1345 Metric::new(
1346 metric.clone(),
1347 MetricKind::Incremental,
1348 MetricValue::Counter { value: i as f64 },
1349 )
1350 .with_namespace(Some("ns"))
1351 .with_tags(Some(metric_tags!(
1352 "region" => "us-west-1",
1353 "production" => "true",
1354 ))),
1355 );
1356 events.push(event);
1357 }
1358
1359 let client = HttpClient::new(None, cx.proxy()).unwrap();
1360 let sink = InfluxDbSvc::new(config, client).unwrap();
1361 run_and_assert_sink_compliance(sink, stream::iter(events), &HTTP_SINK_TAGS).await;
1362
1363 let mut body = std::collections::HashMap::new();
1364 body.insert("query", format!("from(bucket:\"my-bucket\") |> range(start: 0) |> filter(fn: (r) => r._measurement == \"ns.{metric}\")"));
1365 body.insert("type", "flux".to_owned());
1366
1367 let client = reqwest::Client::builder()
1368 .danger_accept_invalid_certs(true)
1369 .build()
1370 .unwrap();
1371
1372 let res = client
1373 .post(format!("{}/api/v2/query?org=my-org", address_v2()))
1374 .json(&body)
1375 .header("accept", "application/json")
1376 .header("Authorization", "Token my-token")
1377 .send()
1378 .await
1379 .unwrap();
1380 let string = res.text().await.unwrap();
1381
1382 let lines = string.split('\n').collect::<Vec<&str>>();
1383 let header = lines[0].split(',').collect::<Vec<&str>>();
1384 let record = lines[1].split(',').collect::<Vec<&str>>();
1385
1386 assert_eq!(
1387 record[header
1388 .iter()
1389 .position(|&r| r.trim() == "metric_type")
1390 .unwrap()]
1391 .trim(),
1392 "counter"
1393 );
1394 assert_eq!(
1395 record[header
1396 .iter()
1397 .position(|&r| r.trim() == "production")
1398 .unwrap()]
1399 .trim(),
1400 "true"
1401 );
1402 assert_eq!(
1403 record[header.iter().position(|&r| r.trim() == "region").unwrap()].trim(),
1404 "us-west-1"
1405 );
1406 assert_eq!(
1407 record[header
1408 .iter()
1409 .position(|&r| r.trim() == "_measurement")
1410 .unwrap()]
1411 .trim(),
1412 format!("ns.{}", metric)
1413 );
1414 assert_eq!(
1415 record[header.iter().position(|&r| r.trim() == "_field").unwrap()].trim(),
1416 "value"
1417 );
1418 assert_eq!(
1419 record[header.iter().position(|&r| r.trim() == "_value").unwrap()].trim(),
1420 "45"
1421 );
1422 }
1423
1424 fn create_event(i: i32) -> Event {
1425 Event::Metric(
1426 Metric::new(
1427 format!("counter-{i}"),
1428 MetricKind::Incremental,
1429 MetricValue::Counter { value: i as f64 },
1430 )
1431 .with_namespace(Some("ns"))
1432 .with_tags(Some(metric_tags!(
1433 "region" => "us-west-1",
1434 "production" => "true",
1435 )))
1436 .with_timestamp(Some(Utc::now())),
1437 )
1438 }
1439}