Skip to main content

vector/sinks/influxdb/
metrics.rs

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/// Configuration for the `influxdb_metrics` sink.
57#[configurable_component(sink("influxdb_metrics", "Deliver metric event data to InfluxDB."))]
58#[derive(Clone, Debug)]
59#[serde(deny_unknown_fields)]
60pub struct InfluxDbConfig {
61    /// Sets the default namespace for any metrics sent.
62    ///
63    /// This namespace is only used if a metric has no existing namespace. When a namespace is
64    /// present, it is used as a prefix to the metric name, and separated with a period (`.`).
65    #[serde(alias = "namespace")]
66    #[configurable(metadata(docs::examples = "service"))]
67    pub default_namespace: Option<String>,
68
69    /// The endpoint to send data to.
70    ///
71    /// This should be a full HTTP URI, including the scheme, host, and port.
72    #[configurable(metadata(docs::examples = "http://localhost:8086/"))]
73    pub endpoint: HttpEndpoint,
74
75    /// The InfluxDB API version to use.
76    ///
77    /// Omitting this option is deprecated and it will be required in a future release. When
78    /// unset, the version is temporarily inferred from the configured settings.
79    #[configurable(metadata(docs::examples = "2"))]
80    #[configurable(metadata(docs::examples = "1"))]
81    #[configurable(metadata(docs::minimal = true))]
82    pub version: Option<InfluxDbVersion>,
83
84    /// The name of the database to write into.
85    ///
86    /// Only relevant when using InfluxDB v0.x/v1.x.
87    #[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    /// The consistency level to use for writes.
93    ///
94    /// Only relevant when using InfluxDB v0.x/v1.x.
95    #[configurable(metadata(docs::examples = "any"))]
96    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
97    pub consistency: Option<String>,
98
99    /// The target retention policy for writes.
100    ///
101    /// Only relevant when using InfluxDB v0.x/v1.x.
102    #[configurable(metadata(docs::examples = "autogen"))]
103    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
104    pub retention_policy_name: Option<String>,
105
106    /// The username to authenticate with.
107    ///
108    /// Only relevant when using InfluxDB v0.x/v1.x.
109    #[configurable(metadata(docs::examples = "todd"))]
110    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
111    pub username: Option<String>,
112
113    /// The password to authenticate with.
114    ///
115    /// Only relevant when using InfluxDB v0.x/v1.x.
116    #[configurable(metadata(docs::examples = "${INFLUXDB_PASSWORD}"))]
117    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
118    pub password: Option<SensitiveString>,
119
120    /// The name of the organization to write into.
121    ///
122    /// Only relevant when using InfluxDB v2.x and above.
123    #[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    /// The name of the bucket to write into.
130    ///
131    /// Only relevant when using InfluxDB v2.x and above.
132    #[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    /// The [token][token_docs] to authenticate with.
139    ///
140    /// Only relevant when using InfluxDB v2.x and above.
141    ///
142    /// [token_docs]: https://v2.docs.influxdata.com/v2.0/security/tokens/
143    #[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    /// A map of additional tags, in the key/value pair format, to add to each measurement.
158    #[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    /// The list of quantiles to calculate when sending distribution metrics.
166    #[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    /// Rejects settings that belong to the version that was not selected, so that
266    /// an explicit `version` cannot silently ignore stale settings for the other version.
267    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    // Emission of Error internal event is handled upstream by the caller
356    fn poll_ready(&mut self, cx: &mut std::task::Context) -> Poll<Result<(), Self::Error>> {
357        self.inner.poll_ready(cx)
358    }
359
360    // Emission of Error internal event is handled upstream by the caller
361    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            // Counters are disaggregated. We take the previous value from the state
419            // and emit the difference between previous and current as a Counter
420            (_, MetricValue::Counter { .. }) => state.make_incremental(metric),
421            // Convert incremental gauges into absolute ones
422            (_, MetricValue::Gauge { .. }) => state.make_absolute(metric),
423            // All others are left as-is
424            _ => 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    // remove last '\n'
464    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                // Hard-coded quantiles because InfluxDB can't natively do anything useful with the
527                // actual bins.
528                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        //
1248        // {"results":[{"statement_id":0,"series":[{"columns":["key"],"values":
1249        //  [
1250        //    ["ns.counter-0,metric_type=counter,production=true,region=us-west-1"],
1251        //    ["ns.counter-1,metric_type=counter,production=true,region=us-west-1"],
1252        //    ["ns.counter-2,metric_type=counter,production=true,region=us-west-1"],
1253        //    ["ns.counter-3,metric_type=counter,production=true,region=us-west-1"],
1254        //    ["ns.counter-4,metric_type=counter,production=true,region=us-west-1"],
1255        //    ["ns.counter-5,metric_type=counter,production=true,region=us-west-1"],
1256        //    ["ns.counter-6,metric_type=counter,production=true,region=us-west-1"],
1257        //    ["ns.counter-7,metric_type=counter,production=true,region=us-west-1"],
1258        //    ["ns.counter-8,metric_type=counter,production=true,region=us-west-1"],
1259        //    ["ns.counter-9,metric_type=counter,production=true,region=us-west-1"]
1260        //  ]}]}]}\n
1261        //
1262
1263        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}