Skip to main content

vector/sinks/influxdb/
logs.rs

1use std::collections::{HashMap, HashSet};
2
3use bytes::{Bytes, BytesMut};
4use futures::SinkExt;
5use http::{Request, Uri};
6use indoc::indoc;
7use vector_lib::{
8    config::log_schema,
9    configurable::configurable_component,
10    lookup::{PathPrefix, lookup_v2::OptionalValuePath},
11    schema,
12    sensitive_string::SensitiveString,
13};
14use vrl::{event_path, path::OwnedValuePath, value::Kind};
15
16use super::{
17    Field, InfluxDb1Settings, InfluxDb2Settings, InfluxDbSettings, InfluxDbVersion,
18    ProtocolVersion, encode_timestamp, healthcheck, influx_line_protocol, influxdb_settings,
19};
20use crate::{
21    codecs::Transformer,
22    config::{AcknowledgementsConfig, GenerateConfig, Input, SinkConfig, SinkContext},
23    event::{Event, KeyString, MetricTags, Value},
24    http::HttpClient,
25    internal_events::InfluxdbEncodingError,
26    sinks::{
27        Healthcheck, VectorSink,
28        util::{
29            BatchConfig, Buffer, Compression, HttpEndpoint, SinkBatchSettings, TowerRequestConfig,
30            http::{BatchedHttpSink, HttpEventEncoder, HttpSink},
31        },
32    },
33    tls::{TlsConfig, TlsSettings},
34};
35
36#[derive(Clone, Copy, Debug, Default)]
37pub struct InfluxDbLogsDefaultBatchSettings;
38
39impl SinkBatchSettings for InfluxDbLogsDefaultBatchSettings {
40    const MAX_EVENTS: Option<usize> = None;
41    const MAX_BYTES: Option<usize> = Some(1_000_000);
42    const TIMEOUT_SECS: f64 = 1.0;
43}
44
45/// Configuration for the `influxdb_logs` sink.
46#[configurable_component(sink("influxdb_logs", "Deliver log event data to InfluxDB."))]
47#[derive(Clone, Debug)]
48#[serde(deny_unknown_fields)]
49pub struct InfluxDbLogsConfig {
50    /// The name of the InfluxDB measurement that is written to.
51    #[configurable(metadata(docs::examples = "vector-logs"))]
52    #[configurable(metadata(docs::required = true))]
53    pub measurement: Option<String>,
54
55    /// The endpoint to send data to.
56    ///
57    /// This should be a full HTTP URI, including the scheme, host, and port.
58    #[configurable(metadata(docs::examples = "http://localhost:8086"))]
59    pub endpoint: HttpEndpoint,
60
61    /// The list of names of log fields that should be added as tags to each measurement.
62    ///
63    /// By default Vector adds `metric_type` as well as the configured `log_schema.host_key` and
64    /// `log_schema.source_type_key` options.
65    #[serde(default)]
66    #[configurable(metadata(docs::examples = "field1"))]
67    #[configurable(metadata(docs::examples = "parent.child_field"))]
68    pub tags: Vec<KeyString>,
69
70    /// The InfluxDB API version to use.
71    ///
72    /// Omitting this option is deprecated and it will be required in a future release. When
73    /// unset, the version is temporarily inferred from the configured settings.
74    #[configurable(metadata(docs::examples = "2"))]
75    #[configurable(metadata(docs::examples = "1"))]
76    #[configurable(metadata(docs::minimal = true))]
77    pub version: Option<InfluxDbVersion>,
78
79    /// The name of the database to write into.
80    ///
81    /// Only relevant when using InfluxDB v0.x/v1.x.
82    #[configurable(metadata(docs::examples = "vector-database"))]
83    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
84    #[configurable(metadata(docs::required_when = "version = \"1\""))]
85    pub database: Option<String>,
86
87    /// The consistency level to use for writes.
88    ///
89    /// Only relevant when using InfluxDB v0.x/v1.x.
90    #[configurable(metadata(docs::examples = "any"))]
91    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
92    pub consistency: Option<String>,
93
94    /// The target retention policy for writes.
95    ///
96    /// Only relevant when using InfluxDB v0.x/v1.x.
97    #[configurable(metadata(docs::examples = "autogen"))]
98    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
99    pub retention_policy_name: Option<String>,
100
101    /// The username to authenticate with.
102    ///
103    /// Only relevant when using InfluxDB v0.x/v1.x.
104    #[configurable(metadata(docs::examples = "todd"))]
105    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
106    pub username: Option<String>,
107
108    /// The password to authenticate with.
109    ///
110    /// Only relevant when using InfluxDB v0.x/v1.x.
111    #[configurable(metadata(docs::examples = "${INFLUXDB_PASSWORD}"))]
112    #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
113    pub password: Option<SensitiveString>,
114
115    /// The name of the organization to write into.
116    ///
117    /// Only relevant when using InfluxDB v2.x and above.
118    #[configurable(metadata(docs::examples = "my-org"))]
119    #[configurable(metadata(docs::relevant_when = "version = \"2\""))]
120    #[configurable(metadata(docs::required_when = "version = \"2\""))]
121    #[configurable(metadata(docs::minimal = true))]
122    pub org: Option<String>,
123
124    /// The name of the bucket to write into.
125    ///
126    /// Only relevant when using InfluxDB v2.x and above.
127    #[configurable(metadata(docs::examples = "vector-bucket"))]
128    #[configurable(metadata(docs::relevant_when = "version = \"2\""))]
129    #[configurable(metadata(docs::required_when = "version = \"2\""))]
130    #[configurable(metadata(docs::minimal = true))]
131    pub bucket: Option<String>,
132
133    /// The [token][token_docs] to authenticate with.
134    ///
135    /// Only relevant when using InfluxDB v2.x and above.
136    ///
137    /// [token_docs]: https://v2.docs.influxdata.com/v2.0/security/tokens/
138    #[configurable(metadata(docs::examples = "${INFLUXDB_TOKEN}"))]
139    #[configurable(metadata(docs::relevant_when = "version = \"2\""))]
140    #[configurable(metadata(docs::required_when = "version = \"2\""))]
141    #[configurable(metadata(docs::minimal = true))]
142    pub token: Option<SensitiveString>,
143
144    #[configurable(derived)]
145    #[serde(skip_serializing_if = "crate::serde::is_default", default)]
146    pub encoding: Transformer,
147
148    #[configurable(derived)]
149    #[serde(default)]
150    pub batch: BatchConfig<InfluxDbLogsDefaultBatchSettings>,
151
152    #[configurable(derived)]
153    #[serde(default)]
154    pub request: TowerRequestConfig,
155
156    #[configurable(derived)]
157    pub tls: Option<TlsConfig>,
158
159    #[configurable(derived)]
160    #[serde(
161        default,
162        deserialize_with = "crate::serde::bool_or_struct",
163        skip_serializing_if = "crate::serde::is_default"
164    )]
165    acknowledgements: AcknowledgementsConfig,
166
167    // `host_key`, `message_key`, and `source_type_key` are `Option` as we want `vector generate`
168    // to produce a config with these as `None`, to not accidentally override a users configured
169    // `log_schema`. Generating is constrained by build-time and can't account for changes to the
170    // default `log_schema`.
171    /// Use this option to customize the key containing the hostname.
172    ///
173    /// The setting of `log_schema.host_key`, usually `host`, is used here by default.
174    #[configurable(metadata(docs::examples = "hostname"))]
175    pub host_key: Option<OptionalValuePath>,
176
177    /// Use this option to customize the key containing the message.
178    ///
179    /// The setting of `log_schema.message_key`, usually `message`, is used here by default.
180    #[configurable(metadata(docs::examples = "text"))]
181    pub message_key: Option<OptionalValuePath>,
182
183    /// Use this option to customize the key containing the source_type.
184    ///
185    /// The setting of `log_schema.source_type_key`, usually `source_type`, is used here by default.
186    #[configurable(metadata(docs::examples = "source"))]
187    pub source_type_key: Option<OptionalValuePath>,
188}
189
190#[derive(Debug)]
191struct InfluxDbLogsSink {
192    uri: Uri,
193    token: String,
194    protocol_version: ProtocolVersion,
195    measurement: String,
196    tags: HashSet<KeyString>,
197    transformer: Transformer,
198    host_key: OwnedValuePath,
199    message_key: OwnedValuePath,
200    source_type_key: OwnedValuePath,
201}
202
203impl GenerateConfig for InfluxDbLogsConfig {
204    fn generate_config() -> serde_json::Value {
205        serde_yaml::from_str(indoc! {r#"
206            endpoint: http://localhost:8086/
207            measurement: vector-logs
208            tags: []
209            version: "2"
210            org: my-org
211            bucket: my-bucket
212            token: ${INFLUXDB_TOKEN}
213        "#})
214        .unwrap()
215    }
216}
217
218#[async_trait::async_trait]
219#[typetag::serde(name = "influxdb_logs")]
220impl SinkConfig for InfluxDbLogsConfig {
221    async fn build(&self, cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
222        let measurement = self.get_measurement()?;
223        let tags: HashSet<KeyString> = self.tags.iter().cloned().collect();
224
225        let tls_settings = TlsSettings::from_options(self.tls.as_ref())?;
226        let client = HttpClient::new(tls_settings, cx.proxy())?;
227        let healthcheck = self.healthcheck(client.clone())?;
228
229        let batch = self.batch.into_batch_settings()?;
230        let request = self.request.into_settings();
231
232        let settings = influxdb_settings(self.settings()?);
233
234        let endpoint = self.endpoint.clone();
235        let uri = settings.write_uri(endpoint).unwrap();
236
237        let token = settings.token();
238        let protocol_version = settings.protocol_version();
239
240        let host_key = self
241            .host_key
242            .as_ref()
243            .and_then(|k| k.path.clone())
244            .or_else(|| log_schema().host_key().cloned())
245            .expect("global log_schema.host_key to be valid path");
246
247        let message_key = self
248            .message_key
249            .as_ref()
250            .and_then(|k| k.path.clone())
251            .or_else(|| log_schema().message_key().cloned())
252            .expect("global log_schema.message_key to be valid path");
253
254        let source_type_key = self
255            .source_type_key
256            .as_ref()
257            .and_then(|k| k.path.clone())
258            .or_else(|| log_schema().source_type_key().cloned())
259            .expect("global log_schema.source_type_key to be valid path");
260
261        let sink = InfluxDbLogsSink {
262            uri,
263            token: token.inner().to_owned(),
264            protocol_version,
265            measurement,
266            tags,
267            transformer: self.encoding.clone(),
268            host_key,
269            message_key,
270            source_type_key,
271        };
272
273        let sink = BatchedHttpSink::new(
274            sink,
275            Buffer::new(batch.size, Compression::None),
276            request,
277            batch.timeout,
278            client,
279        )
280        .sink_map_err(|error| error!(message = "Fatal influxdb_logs sink error.", %error, internal_log_rate_limit = false));
281
282        #[allow(deprecated)]
283        Ok((VectorSink::from_event_sink(sink), healthcheck))
284    }
285
286    fn input(&self) -> Input {
287        let requirements = schema::Requirement::empty()
288            .optional_meaning("message", Kind::bytes())
289            .optional_meaning("host", Kind::bytes())
290            .optional_meaning("timestamp", Kind::timestamp());
291
292        Input::log().with_schema_requirement(requirements)
293    }
294
295    fn acknowledgements(&self) -> &AcknowledgementsConfig {
296        &self.acknowledgements
297    }
298}
299
300struct InfluxDbLogsEncoder {
301    protocol_version: ProtocolVersion,
302    measurement: String,
303    tags: HashSet<KeyString>,
304    transformer: Transformer,
305    host_key: OwnedValuePath,
306    message_key: OwnedValuePath,
307    source_type_key: OwnedValuePath,
308}
309
310impl HttpEventEncoder<BytesMut> for InfluxDbLogsEncoder {
311    fn encode_event(&mut self, event: Event) -> Option<BytesMut> {
312        let mut log = event.into_log();
313        // If the event isn't an object (`. = "foo"`), inserting or renaming will result in losing
314        // the original value that was assigned to the root. To avoid this we intentionally rename
315        // the path that points to "message" such that it has a dedicated key.
316        // TODO: add a `TargetPath::is_event_root()` to conditionally rename?
317        if let Some(message_path) = log.message_path().cloned().as_ref() {
318            log.rename_key(message_path, (PathPrefix::Event, &self.message_key));
319        }
320        // Add the `host` and `source_type` to the HashSet of tags to include
321        // Ensure those paths are on the event to be encoded, rather than metadata
322        if let Some(host_path) = log.host_path().cloned().as_ref() {
323            self.tags.replace(host_path.path.to_string().into());
324            log.rename_key(host_path, (PathPrefix::Event, &self.host_key));
325        }
326
327        if let Some(source_type_path) = log.source_type_path().cloned().as_ref() {
328            self.tags.replace(source_type_path.path.to_string().into());
329            log.rename_key(source_type_path, (PathPrefix::Event, &self.source_type_key));
330        }
331
332        self.tags.replace("metric_type".into());
333        log.insert(event_path!("metric_type"), "logs");
334
335        // Timestamp
336        let timestamp = encode_timestamp(match log.remove_timestamp() {
337            Some(Value::Timestamp(ts)) => Some(ts),
338            _ => None,
339        });
340
341        let log = {
342            let mut event = Event::from(log);
343            self.transformer.transform(&mut event);
344            event.into_log()
345        };
346
347        // Tags + Fields
348        let mut tags = MetricTags::default();
349        let mut fields: HashMap<KeyString, Field> = HashMap::new();
350        log.convert_to_fields().for_each(|(key, value)| {
351            if self.tags.contains(key.as_str()) {
352                tags.replace(key.into(), value.to_string_lossy().into_owned());
353            } else {
354                fields.insert(key, to_field(value));
355            }
356        });
357
358        let mut output = BytesMut::new();
359        if let Err(error_message) = influx_line_protocol(
360            self.protocol_version,
361            &self.measurement,
362            Some(tags),
363            Some(fields),
364            timestamp,
365            &mut output,
366        ) {
367            emit!(InfluxdbEncodingError {
368                error_message,
369                count: 1
370            });
371            return None;
372        };
373
374        Some(output)
375    }
376}
377
378impl HttpSink for InfluxDbLogsSink {
379    type Input = BytesMut;
380    type Output = BytesMut;
381    type Encoder = InfluxDbLogsEncoder;
382
383    fn build_encoder(&self) -> Self::Encoder {
384        InfluxDbLogsEncoder {
385            protocol_version: self.protocol_version,
386            measurement: self.measurement.clone(),
387            tags: self.tags.clone(),
388            transformer: self.transformer.clone(),
389            host_key: self.host_key.clone(),
390            message_key: self.message_key.clone(),
391            source_type_key: self.source_type_key.clone(),
392        }
393    }
394
395    async fn build_request(&self, events: Self::Output) -> crate::Result<Request<Bytes>> {
396        Request::post(&self.uri)
397            .header("Content-Type", "text/plain")
398            .header("Authorization", format!("Token {}", &self.token))
399            .body(events.freeze())
400            .map_err(Into::into)
401    }
402}
403
404impl InfluxDbLogsConfig {
405    fn settings(&self) -> crate::Result<InfluxDbSettings> {
406        let version = match self.version {
407            Some(version) => {
408                self.validate_version(version)?;
409                version
410            }
411            None => {
412                warn!(
413                    "The `version` option is currently optional but will be required in a future release. \
414                     Please set it to `1` or `2` to match your InfluxDB settings."
415                );
416                self.infer_version()?
417            }
418        };
419        match version {
420            InfluxDbVersion::V1 => Ok(InfluxDbSettings::V1(InfluxDb1Settings {
421                database: self
422                    .database
423                    .clone()
424                    .ok_or("the `database` option is required when using InfluxDB v1")?,
425                consistency: self.consistency.clone(),
426                retention_policy_name: self.retention_policy_name.clone(),
427                username: self.username.clone(),
428                password: self.password.clone(),
429            })),
430            InfluxDbVersion::V2 => Ok(InfluxDbSettings::V2(InfluxDb2Settings {
431                org: self
432                    .org
433                    .clone()
434                    .ok_or("the `org` option is required when using InfluxDB v2")?,
435                bucket: self
436                    .bucket
437                    .clone()
438                    .ok_or("the `bucket` option is required when using InfluxDB v2")?,
439                token: self
440                    .token
441                    .clone()
442                    .ok_or("the `token` option is required when using InfluxDB v2")?,
443            })),
444        }
445    }
446
447    const fn settings_present(&self) -> (bool, bool) {
448        let has_v1 = self.database.is_some()
449            || self.consistency.is_some()
450            || self.retention_policy_name.is_some()
451            || self.username.is_some()
452            || self.password.is_some();
453        let has_v2 = self.org.is_some() || self.bucket.is_some() || self.token.is_some();
454        (has_v1, has_v2)
455    }
456
457    fn infer_version(&self) -> crate::Result<InfluxDbVersion> {
458        let (has_v1, has_v2) = self.settings_present();
459        match (has_v1, has_v2) {
460            (true, true) => Err(
461                "Unclear settings. Both InfluxDB v1 and v2 settings are configured; configure only one version."
462                    .into(),
463            ),
464            (false, false) => Err("InfluxDB v1 or v2 should be configured as endpoint.".into()),
465            (true, false) => Ok(InfluxDbVersion::V1),
466            (false, true) => Ok(InfluxDbVersion::V2),
467        }
468    }
469
470    /// Rejects settings that belong to the version that was not selected, so that
471    /// an explicit `version` cannot silently ignore stale settings for the other version.
472    fn validate_version(&self, version: InfluxDbVersion) -> crate::Result<()> {
473        let (has_v1, has_v2) = self.settings_present();
474        match version {
475            InfluxDbVersion::V1 if has_v2 => Err(
476                "InfluxDB v1 settings are configured, but v2 settings were also provided; configure only one version."
477                    .into(),
478            ),
479            InfluxDbVersion::V2 if has_v1 => Err(
480                "InfluxDB v2 settings are configured, but v1 settings were also provided; configure only one version."
481                    .into(),
482            ),
483            _ => Ok(()),
484        }
485    }
486
487    fn get_measurement(&self) -> Result<String, &'static str> {
488        self.measurement
489            .clone()
490            .ok_or("The `measurement` option is required.")
491    }
492
493    fn healthcheck(&self, client: HttpClient) -> crate::Result<Healthcheck> {
494        let config = self.clone();
495
496        let healthcheck = healthcheck(config.endpoint.clone(), config.settings()?, client)?;
497
498        Ok(healthcheck)
499    }
500}
501
502fn to_field(value: &Value) -> Field {
503    match value {
504        Value::Integer(num) => Field::Int(*num),
505        Value::Float(num) => Field::Float(num.into_inner()),
506        Value::Boolean(b) => Field::Bool(*b),
507        _ => Field::String(value.to_string_lossy().into_owned()),
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use chrono::{Utc, offset::TimeZone};
514    use futures::{StreamExt, channel::mpsc, stream};
515    use http::{StatusCode, request::Parts};
516    use indoc::indoc;
517    use vector_lib::{
518        event::{BatchNotifier, BatchStatus, Event, LogEvent},
519        lookup::owned_value_path,
520    };
521
522    use super::*;
523    use crate::{
524        sinks::{
525            influxdb::test_util::{assert_fields, split_line_protocol, ts},
526            util::test::{build_test_server_status, load_sink},
527        },
528        test_util::{
529            addr::next_addr,
530            components::{
531                COMPONENT_ERROR_TAGS, HTTP_SINK_TAGS, run_and_assert_sink_compliance,
532                run_and_assert_sink_error,
533            },
534        },
535    };
536
537    type Receiver = mpsc::Receiver<(Parts, bytes::Bytes)>;
538
539    #[test]
540    fn generate_config() {
541        crate::test_util::test_generate_config::<InfluxDbLogsConfig>();
542    }
543
544    #[test]
545    fn test_config_without_tags() {
546        let config = indoc! {r#"
547            measurement: "vector-logs"
548            endpoint: "http://localhost:9999"
549            version: "2"
550            bucket: "my-bucket"
551            org: "my-org"
552            token: "my-token"
553        "#};
554
555        serde_yaml::from_str::<InfluxDbLogsConfig>(config).unwrap();
556    }
557
558    #[test]
559    fn test_config_measurement_required() {
560        let config = indoc! {r#"
561            endpoint: "http://localhost:9999"
562            version: "2"
563            bucket: "my-bucket"
564            org: "my-org"
565            token: "my-token"
566        "#};
567
568        let sink_config = serde_yaml::from_str::<InfluxDbLogsConfig>(config).unwrap();
569        assert_eq!(
570            Err("The `measurement` option is required."),
571            sink_config.get_measurement()
572        );
573    }
574
575    #[test]
576    fn test_infer_version_v2() {
577        let config = indoc! {r#"
578            endpoint: "http://localhost:9999"
579            bucket: "my-bucket"
580            org: "my-org"
581            token: "my-token"
582        "#};
583        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
584        assert_eq!(config.infer_version().unwrap(), InfluxDbVersion::V2);
585    }
586
587    #[test]
588    fn test_infer_version_v1() {
589        let config = indoc! {r#"
590            endpoint: "http://localhost:9999"
591            database: "my-database"
592        "#};
593        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
594        assert_eq!(config.infer_version().unwrap(), InfluxDbVersion::V1);
595    }
596
597    #[test]
598    fn test_infer_version_missing() {
599        let config = indoc! {r#"
600            endpoint: "http://localhost:9999"
601        "#};
602        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
603        assert!(config.infer_version().is_err());
604    }
605
606    #[test]
607    fn test_infer_version_both() {
608        let config = indoc! {r#"
609            endpoint: "http://localhost:9999"
610            database: "my-database"
611            bucket: "my-bucket"
612            org: "my-org"
613            token: "my-token"
614        "#};
615        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
616        assert!(config.infer_version().is_err());
617    }
618
619    #[test]
620    fn test_settings_explicit_v2_rejects_v1_settings() {
621        let config = indoc! {r#"
622            endpoint: "http://localhost:9999"
623            version: "2"
624            bucket: "my-bucket"
625            org: "my-org"
626            token: "my-token"
627            database: "stale-v1-database"
628            username: "stale-v1-user"
629        "#};
630        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
631        assert!(config.settings().is_err());
632    }
633
634    #[test]
635    fn test_settings_explicit_v1_rejects_v2_settings() {
636        let config = indoc! {r#"
637            endpoint: "http://localhost:9999"
638            version: "1"
639            database: "my-database"
640            org: "stale-v2-org"
641            bucket: "stale-v2-bucket"
642            token: "stale-v2-token"
643        "#};
644        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
645        assert!(config.settings().is_err());
646    }
647
648    #[test]
649    fn test_settings_explicit_v2_accepts_only_v2_settings() {
650        let config = indoc! {r#"
651            endpoint: "http://localhost:9999"
652            version: "2"
653            bucket: "my-bucket"
654            org: "my-org"
655            token: "my-token"
656        "#};
657        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
658        assert!(config.settings().is_ok());
659    }
660
661    #[test]
662    fn test_settings_explicit_v1_accepts_only_v1_settings() {
663        let config = indoc! {r#"
664            endpoint: "http://localhost:9999"
665            version: "1"
666            database: "my-database"
667        "#};
668        let config: InfluxDbLogsConfig = serde_yaml::from_str(config).unwrap();
669        assert!(config.settings().is_ok());
670    }
671
672    #[test]
673    fn test_encode_event_apply_rules() {
674        let mut event = Event::Log(LogEvent::from("hello"));
675        event
676            .as_mut_log()
677            .insert(event_path!("host"), "aws.cloud.eur");
678        event.as_mut_log().insert(event_path!("timestamp"), ts());
679
680        let mut sink = create_sink(
681            "http://localhost:9999",
682            "my-token",
683            ProtocolVersion::V1,
684            "vector",
685            ["metric_type", "host"].to_vec(),
686        );
687        sink.transformer
688            .set_except_fields(Some(vec!["host".into()]))
689            .unwrap();
690        let mut encoder = sink.build_encoder();
691
692        let bytes = encoder.encode_event(event.clone()).unwrap();
693        let string = std::str::from_utf8(&bytes).unwrap();
694
695        let line_protocol = split_line_protocol(string);
696        assert_eq!("vector", line_protocol.0);
697        assert_eq!("metric_type=logs", line_protocol.1);
698        assert_fields(line_protocol.2.to_string(), ["message=\"hello\""].to_vec());
699        assert_eq!("1542182950000000011\n", line_protocol.3);
700
701        sink.transformer
702            .set_except_fields(Some(vec!["metric_type".into()]))
703            .unwrap();
704        let mut encoder = sink.build_encoder();
705        let bytes = encoder.encode_event(event.clone()).unwrap();
706        let string = std::str::from_utf8(&bytes).unwrap();
707        let line_protocol = split_line_protocol(string);
708        assert_eq!(
709            "host=aws.cloud.eur", line_protocol.1,
710            "metric_type tag should be excluded"
711        );
712        assert_fields(line_protocol.2, ["message=\"hello\""].to_vec());
713    }
714
715    #[test]
716    fn test_encode_event_v1() {
717        let mut event = Event::Log(LogEvent::from("hello"));
718        event
719            .as_mut_log()
720            .insert(event_path!("host"), "aws.cloud.eur");
721        event
722            .as_mut_log()
723            .insert(event_path!("source_type"), "file");
724
725        event.as_mut_log().insert(event_path!("int"), 4i32);
726        event.as_mut_log().insert(event_path!("float"), 5.5);
727        event.as_mut_log().insert(event_path!("bool"), true);
728        event
729            .as_mut_log()
730            .insert(event_path!("string"), "thisisastring");
731        event.as_mut_log().insert(event_path!("timestamp"), ts());
732
733        let sink = create_sink(
734            "http://localhost:9999",
735            "my-token",
736            ProtocolVersion::V1,
737            "vector",
738            ["source_type", "host", "metric_type"].to_vec(),
739        );
740        let mut encoder = sink.build_encoder();
741
742        let bytes = encoder.encode_event(event).unwrap();
743        let string = std::str::from_utf8(&bytes).unwrap();
744
745        let line_protocol = split_line_protocol(string);
746        assert_eq!("vector", line_protocol.0);
747        assert_eq!(
748            "host=aws.cloud.eur,metric_type=logs,source_type=file",
749            line_protocol.1
750        );
751        assert_fields(
752            line_protocol.2.to_string(),
753            [
754                "int=4i",
755                "float=5.5",
756                "bool=true",
757                "string=\"thisisastring\"",
758                "message=\"hello\"",
759            ]
760            .to_vec(),
761        );
762
763        assert_eq!("1542182950000000011\n", line_protocol.3);
764    }
765
766    #[test]
767    fn test_encode_event() {
768        let mut event = Event::Log(LogEvent::from("hello"));
769        event
770            .as_mut_log()
771            .insert(event_path!("host"), "aws.cloud.eur");
772        event
773            .as_mut_log()
774            .insert(event_path!("source_type"), "file");
775
776        event.as_mut_log().insert(event_path!("int"), 4i32);
777        event.as_mut_log().insert(event_path!("float"), 5.5);
778        event.as_mut_log().insert(event_path!("bool"), true);
779        event
780            .as_mut_log()
781            .insert(event_path!("string"), "thisisastring");
782        event.as_mut_log().insert(event_path!("timestamp"), ts());
783
784        let sink = create_sink(
785            "http://localhost:9999",
786            "my-token",
787            ProtocolVersion::V2,
788            "vector",
789            ["source_type", "host", "metric_type"].to_vec(),
790        );
791        let mut encoder = sink.build_encoder();
792
793        let bytes = encoder.encode_event(event).unwrap();
794        let string = std::str::from_utf8(&bytes).unwrap();
795
796        let line_protocol = split_line_protocol(string);
797        assert_eq!("vector", line_protocol.0);
798        assert_eq!(
799            "host=aws.cloud.eur,metric_type=logs,source_type=file",
800            line_protocol.1
801        );
802        assert_fields(
803            line_protocol.2.to_string(),
804            [
805                "int=4i",
806                "float=5.5",
807                "bool=true",
808                "string=\"thisisastring\"",
809                "message=\"hello\"",
810            ]
811            .to_vec(),
812        );
813
814        assert_eq!("1542182950000000011\n", line_protocol.3);
815    }
816
817    #[test]
818    fn test_encode_event_without_tags() {
819        let mut event = Event::Log(LogEvent::from("hello"));
820
821        event.as_mut_log().insert(event_path!("value"), 100);
822        event.as_mut_log().insert(event_path!("timestamp"), ts());
823
824        let mut sink = create_sink(
825            "http://localhost:9999",
826            "my-token",
827            ProtocolVersion::V2,
828            "vector",
829            [].to_vec(),
830        );
831        // exclude default metric_type tag so to emit empty tags
832        sink.transformer
833            .set_except_fields(Some(vec!["metric_type".into()]))
834            .unwrap();
835        let mut encoder = sink.build_encoder();
836
837        let bytes = encoder.encode_event(event).unwrap();
838        let line = std::str::from_utf8(&bytes).unwrap();
839        assert!(
840            line.starts_with("vector "),
841            "measurement (without tags) should ends with space ' '"
842        );
843
844        let line_protocol = split_line_protocol(line);
845        assert_eq!("vector", line_protocol.0);
846        assert_eq!("", line_protocol.1, "tags should be empty");
847        assert_fields(
848            line_protocol.2,
849            ["value=100i", "message=\"hello\""].to_vec(),
850        );
851
852        assert_eq!("1542182950000000011\n", line_protocol.3);
853    }
854
855    #[test]
856    fn test_encode_nested_fields() {
857        let mut event = LogEvent::default();
858
859        event.insert(event_path!("a"), 1);
860        event.insert(event_path!("nested", "field"), "2");
861        event.insert(event_path!("nested", "bool"), true);
862        event.insert(event_path!("nested", "array", 0isize), "example-value");
863        event.insert(event_path!("nested", "array", 2isize), "another-value");
864        event.insert(event_path!("nested", "array", 3isize), 15);
865
866        let sink = create_sink(
867            "http://localhost:9999",
868            "my-token",
869            ProtocolVersion::V2,
870            "vector",
871            ["metric_type"].to_vec(),
872        );
873        let mut encoder = sink.build_encoder();
874
875        let bytes = encoder.encode_event(event.into()).unwrap();
876        let string = std::str::from_utf8(&bytes).unwrap();
877
878        let line_protocol = split_line_protocol(string);
879        assert_eq!("vector", line_protocol.0);
880        assert_eq!("metric_type=logs", line_protocol.1);
881        assert_fields(
882            line_protocol.2,
883            [
884                "a=1i",
885                "nested.array[0]=\"example-value\"",
886                "nested.array[1]=\"<null>\"",
887                "nested.array[2]=\"another-value\"",
888                "nested.array[3]=15i",
889                "nested.bool=true",
890                "nested.field=\"2\"",
891            ]
892            .to_vec(),
893        );
894    }
895
896    #[test]
897    fn test_add_tag() {
898        let mut event = Event::Log(LogEvent::from("hello"));
899        event
900            .as_mut_log()
901            .insert(event_path!("source_type"), "file");
902
903        event.as_mut_log().insert(event_path!("as_a_tag"), 10);
904        event.as_mut_log().insert(event_path!("timestamp"), ts());
905
906        let sink = create_sink(
907            "http://localhost:9999",
908            "my-token",
909            ProtocolVersion::V2,
910            "vector",
911            ["as_a_tag", "not_exists_field", "source_type", "metric_type"].to_vec(),
912        );
913        let mut encoder = sink.build_encoder();
914
915        let bytes = encoder.encode_event(event).unwrap();
916        let string = std::str::from_utf8(&bytes).unwrap();
917
918        let line_protocol = split_line_protocol(string);
919        assert_eq!("vector", line_protocol.0);
920        assert_eq!(
921            "as_a_tag=10,metric_type=logs,source_type=file",
922            line_protocol.1
923        );
924        assert_fields(line_protocol.2.to_string(), ["message=\"hello\""].to_vec());
925
926        assert_eq!("1542182950000000011\n", line_protocol.3);
927    }
928
929    #[tokio::test]
930    async fn smoke_v1() {
931        let rx = smoke_test(
932            indoc! {r#"
933            version = "1"
934            database = "my-database"
935        "#},
936            StatusCode::OK,
937            BatchStatus::Delivered,
938        )
939        .await;
940
941        let query = receive_response(rx).await;
942        assert!(query.contains("db=my-database"));
943        assert!(query.contains("precision=ns"));
944    }
945
946    #[tokio::test]
947    async fn smoke_v1_failure() {
948        smoke_test(
949            indoc! {r#"
950            version = "1"
951            database = "my-database"
952        "#},
953            StatusCode::BAD_REQUEST,
954            BatchStatus::Rejected,
955        )
956        .await;
957    }
958
959    #[tokio::test]
960    async fn smoke_v2() {
961        let rx = smoke_test(
962            indoc! {r#"
963            version = "2"
964            bucket = "my-bucket"
965            org = "my-org"
966            token = "my-token"
967        "#},
968            StatusCode::OK,
969            BatchStatus::Delivered,
970        )
971        .await;
972
973        let query = receive_response(rx).await;
974        assert!(query.contains("org=my-org"));
975        assert!(query.contains("bucket=my-bucket"));
976        assert!(query.contains("precision=ns"));
977    }
978
979    #[tokio::test]
980    async fn smoke_v2_failure() {
981        smoke_test(
982            indoc! {r#"
983            version = "2"
984            bucket = "my-bucket"
985            org = "my-org"
986            token = "my-token"
987        "#},
988            StatusCode::BAD_REQUEST,
989            BatchStatus::Rejected,
990        )
991        .await;
992    }
993
994    async fn smoke_test(
995        config: &str,
996        status_code: StatusCode,
997        batch_status: BatchStatus,
998    ) -> Receiver {
999        let config = format!(
1000            indoc! {r#"
1001            measurement = "vector"
1002            endpoint = "http://localhost:9999"
1003            {}
1004        "#},
1005            config
1006        );
1007        let (mut config, cx) = load_sink::<InfluxDbLogsConfig>(&config).unwrap();
1008
1009        // Make sure we can build the config
1010        _ = config.build(cx.clone()).await.unwrap();
1011
1012        let (_guard, addr) = next_addr();
1013        // Swap out the host so we can force send it
1014        // to our local server
1015        let host = format!("http://{addr}");
1016        config.endpoint = HttpEndpoint::parse(&host).unwrap();
1017
1018        let (sink, _) = config.build(cx).await.unwrap();
1019
1020        let (rx, _trigger, server) = build_test_server_status(addr, status_code);
1021        tokio::spawn(server);
1022
1023        let (batch, mut receiver) = BatchNotifier::new_with_receiver();
1024
1025        let lines = std::iter::repeat(())
1026            .map(move |_| "message_value")
1027            .take(5)
1028            .collect::<Vec<_>>();
1029        let mut events = Vec::new();
1030
1031        // Create 5 events with custom field
1032        for (i, line) in lines.iter().enumerate() {
1033            let mut event = LogEvent::from(line.to_string()).with_batch_notifier(&batch);
1034            event.insert(event_path!(format!("key{i}").as_str()), format!("value{i}"));
1035
1036            let timestamp = Utc
1037                .with_ymd_and_hms(1970, 1, 1, 0, 0, (i as u32) + 1)
1038                .single()
1039                .expect("invalid timestamp");
1040            event.insert(event_path!("timestamp"), timestamp);
1041            event.insert(event_path!("source_type"), "file");
1042
1043            events.push(Event::Log(event));
1044        }
1045        drop(batch);
1046
1047        if batch_status == BatchStatus::Delivered {
1048            run_and_assert_sink_compliance(sink, stream::iter(events), &HTTP_SINK_TAGS).await;
1049        } else {
1050            run_and_assert_sink_error(sink, stream::iter(events), &COMPONENT_ERROR_TAGS).await;
1051        }
1052
1053        assert_eq!(receiver.try_recv(), Ok(batch_status));
1054
1055        rx
1056    }
1057
1058    async fn receive_response(mut rx: Receiver) -> String {
1059        let output = rx.next().await.unwrap();
1060
1061        let request = &output.0;
1062        let query = request.uri.query().unwrap();
1063
1064        let body = std::str::from_utf8(&output.1[..]).unwrap();
1065        let mut lines = body.lines();
1066
1067        assert_eq!(5, lines.clone().count());
1068        assert_line_protocol(0, lines.next());
1069
1070        query.into()
1071    }
1072
1073    fn assert_line_protocol(i: i64, value: Option<&str>) {
1074        //vector,metric_type=logs key0="value0",message="message_value" 1000000000
1075        let line_protocol = split_line_protocol(value.unwrap());
1076        assert_eq!("vector", line_protocol.0);
1077        assert_eq!("metric_type=logs,source_type=file", line_protocol.1);
1078        assert_fields(
1079            line_protocol.2.to_string(),
1080            [
1081                &*format!("key{i}=\"value{i}\""),
1082                "message=\"message_value\"",
1083            ]
1084            .to_vec(),
1085        );
1086
1087        assert_eq!(((i + 1) * 1000000000).to_string(), line_protocol.3);
1088    }
1089
1090    fn create_sink(
1091        uri: &str,
1092        token: &str,
1093        protocol_version: ProtocolVersion,
1094        measurement: &str,
1095        tags: Vec<&str>,
1096    ) -> InfluxDbLogsSink {
1097        let uri = uri.parse::<Uri>().unwrap();
1098        let token = token.to_string();
1099        let measurement = measurement.to_string();
1100        let tags: HashSet<_> = tags.into_iter().map(|tag| tag.into()).collect();
1101        InfluxDbLogsSink {
1102            uri,
1103            token,
1104            protocol_version,
1105            measurement,
1106            tags,
1107            transformer: Default::default(),
1108            host_key: owned_value_path!("host"),
1109            message_key: owned_value_path!("message"),
1110            source_type_key: owned_value_path!("source_type"),
1111        }
1112    }
1113}
1114
1115#[cfg(feature = "influxdb-integration-tests")]
1116#[cfg(test)]
1117mod integration_tests {
1118    use std::sync::Arc;
1119
1120    use chrono::Utc;
1121    use futures::stream;
1122    use vector_lib::{
1123        codecs::BytesDeserializerConfig,
1124        config::{LegacyKey, LogNamespace},
1125        event::{BatchNotifier, BatchStatus, Event, LogEvent},
1126        lookup::{owned_value_path, path},
1127    };
1128    use vrl::value;
1129
1130    use super::*;
1131    use crate::{
1132        config::SinkContext,
1133        sinks::influxdb::{
1134            InfluxDbVersion,
1135            logs::InfluxDbLogsConfig,
1136            test_util::{BUCKET, ORG, TOKEN, address_v2, onboarding_v2},
1137        },
1138        test_util::components::{HTTP_SINK_TAGS, run_and_assert_sink_compliance},
1139    };
1140
1141    #[tokio::test]
1142    async fn influxdb2_logs_put_data() {
1143        let endpoint = address_v2();
1144        onboarding_v2(&endpoint).await;
1145
1146        let now = Utc::now();
1147        let measure = format!(
1148            "vector-{}",
1149            now.timestamp_nanos_opt().expect("Timestamp out of range")
1150        );
1151
1152        let cx = SinkContext::default();
1153
1154        let config = InfluxDbLogsConfig {
1155            measurement: Some(measure.clone()),
1156            endpoint: HttpEndpoint::parse(&endpoint).unwrap(),
1157            tags: Default::default(),
1158            version: Some(InfluxDbVersion::V2),
1159            database: None,
1160            consistency: None,
1161            retention_policy_name: None,
1162            username: None,
1163            password: None,
1164            org: Some(ORG.to_string()),
1165            bucket: Some(BUCKET.to_string()),
1166            token: Some(TOKEN.to_string().into()),
1167            encoding: Default::default(),
1168            batch: Default::default(),
1169            request: Default::default(),
1170            tls: None,
1171            acknowledgements: Default::default(),
1172            host_key: None,
1173            message_key: None,
1174            source_type_key: None,
1175        };
1176
1177        let (sink, _) = config.build(cx).await.unwrap();
1178
1179        let (batch, mut receiver) = BatchNotifier::new_with_receiver();
1180
1181        let mut event1 = LogEvent::from("message_1").with_batch_notifier(&batch);
1182        event1.insert(event_path!("host"), "aws.cloud.eur");
1183        event1.insert(event_path!("source_type"), "file");
1184
1185        let mut event2 = LogEvent::from("message_2").with_batch_notifier(&batch);
1186        event2.insert(event_path!("host"), "aws.cloud.eur");
1187        event2.insert(event_path!("source_type"), "file");
1188
1189        let mut namespaced_log =
1190            LogEvent::from(value!("namespaced message")).with_batch_notifier(&batch);
1191        LogNamespace::Vector.insert_source_metadata(
1192            "file",
1193            &mut namespaced_log,
1194            Some(LegacyKey::Overwrite(path!("host"))),
1195            path!("host"),
1196            "aws.cloud.eur",
1197        );
1198        LogNamespace::Vector.insert_standard_vector_source_metadata(
1199            &mut namespaced_log,
1200            "file",
1201            now,
1202        );
1203        let schema = BytesDeserializerConfig
1204            .schema_definition(LogNamespace::Vector)
1205            .with_metadata_field(
1206                &owned_value_path!("file", "host"),
1207                Kind::bytes(),
1208                Some("host"),
1209            );
1210        namespaced_log
1211            .metadata_mut()
1212            .set_schema_definition(&Arc::new(schema));
1213
1214        drop(batch);
1215
1216        let events = vec![
1217            Event::Log(event1),
1218            Event::Log(event2),
1219            Event::Log(namespaced_log),
1220        ];
1221
1222        run_and_assert_sink_compliance(sink, stream::iter(events), &HTTP_SINK_TAGS).await;
1223
1224        assert_eq!(receiver.try_recv(), Ok(BatchStatus::Delivered));
1225
1226        let mut body = std::collections::HashMap::new();
1227        body.insert("query", format!("from(bucket:\"my-bucket\") |> range(start: 0) |> filter(fn: (r) => r._measurement == \"{}\")", measure.clone()));
1228        body.insert("type", "flux".to_owned());
1229
1230        let client = reqwest::Client::builder()
1231            .danger_accept_invalid_certs(true)
1232            .build()
1233            .unwrap();
1234
1235        let res = client
1236            .post(format!("{endpoint}/api/v2/query?org=my-org"))
1237            .json(&body)
1238            .header("accept", "application/json")
1239            .header("Authorization", "Token my-token")
1240            .send()
1241            .await
1242            .unwrap();
1243        let string = res.text().await.unwrap();
1244
1245        let lines = string.split('\n').collect::<Vec<&str>>();
1246        let header = lines[0].split(',').collect::<Vec<&str>>();
1247        let record1 = lines[1].split(',').collect::<Vec<&str>>();
1248        let record2 = lines[2].split(',').collect::<Vec<&str>>();
1249        let record_ns = lines[3].split(',').collect::<Vec<&str>>();
1250
1251        // measurement
1252        assert_eq!(
1253            record1[header
1254                .iter()
1255                .position(|&r| r.trim() == "_measurement")
1256                .unwrap()]
1257            .trim(),
1258            measure.clone()
1259        );
1260        assert_eq!(
1261            record2[header
1262                .iter()
1263                .position(|&r| r.trim() == "_measurement")
1264                .unwrap()]
1265            .trim(),
1266            measure.clone()
1267        );
1268        assert_eq!(
1269            record_ns[header
1270                .iter()
1271                .position(|&r| r.trim() == "_measurement")
1272                .unwrap()]
1273            .trim(),
1274            measure.clone()
1275        );
1276
1277        // tags
1278        assert_eq!(
1279            record1[header
1280                .iter()
1281                .position(|&r| r.trim() == "metric_type")
1282                .unwrap()]
1283            .trim(),
1284            "logs"
1285        );
1286        assert_eq!(
1287            record2[header
1288                .iter()
1289                .position(|&r| r.trim() == "metric_type")
1290                .unwrap()]
1291            .trim(),
1292            "logs"
1293        );
1294        assert_eq!(
1295            record_ns[header
1296                .iter()
1297                .position(|&r| r.trim() == "metric_type")
1298                .unwrap()]
1299            .trim(),
1300            "logs"
1301        );
1302        assert_eq!(
1303            record1[header.iter().position(|&r| r.trim() == "host").unwrap()].trim(),
1304            "aws.cloud.eur"
1305        );
1306        assert_eq!(
1307            record2[header.iter().position(|&r| r.trim() == "host").unwrap()].trim(),
1308            "aws.cloud.eur"
1309        );
1310        assert_eq!(
1311            record_ns[header.iter().position(|&r| r.trim() == "host").unwrap()].trim(),
1312            "aws.cloud.eur"
1313        );
1314        assert_eq!(
1315            record1[header
1316                .iter()
1317                .position(|&r| r.trim() == "source_type")
1318                .unwrap()]
1319            .trim(),
1320            "file"
1321        );
1322        assert_eq!(
1323            record2[header
1324                .iter()
1325                .position(|&r| r.trim() == "source_type")
1326                .unwrap()]
1327            .trim(),
1328            "file"
1329        );
1330        assert_eq!(
1331            record_ns[header
1332                .iter()
1333                .position(|&r| r.trim() == "source_type")
1334                .unwrap()]
1335            .trim(),
1336            "file"
1337        );
1338
1339        // field
1340        assert_eq!(
1341            record1[header.iter().position(|&r| r.trim() == "_field").unwrap()].trim(),
1342            "message"
1343        );
1344        assert_eq!(
1345            record2[header.iter().position(|&r| r.trim() == "_field").unwrap()].trim(),
1346            "message"
1347        );
1348        assert_eq!(
1349            record_ns[header.iter().position(|&r| r.trim() == "_field").unwrap()].trim(),
1350            "message"
1351        );
1352        assert_eq!(
1353            record1[header.iter().position(|&r| r.trim() == "_value").unwrap()].trim(),
1354            "message_1"
1355        );
1356        assert_eq!(
1357            record2[header.iter().position(|&r| r.trim() == "_value").unwrap()].trim(),
1358            "message_2"
1359        );
1360        assert_eq!(
1361            record_ns[header.iter().position(|&r| r.trim() == "_value").unwrap()].trim(),
1362            "namespaced message"
1363        );
1364    }
1365}