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#[configurable_component(sink("influxdb_logs", "Deliver log event data to InfluxDB."))]
47#[derive(Clone, Debug)]
48#[serde(deny_unknown_fields)]
49pub struct InfluxDbLogsConfig {
50 #[configurable(metadata(docs::examples = "vector-logs"))]
52 #[configurable(metadata(docs::required = true))]
53 pub measurement: Option<String>,
54
55 #[configurable(metadata(docs::examples = "http://localhost:8086"))]
59 pub endpoint: HttpEndpoint,
60
61 #[serde(default)]
66 #[configurable(metadata(docs::examples = "field1"))]
67 #[configurable(metadata(docs::examples = "parent.child_field"))]
68 pub tags: Vec<KeyString>,
69
70 #[configurable(metadata(docs::examples = "2"))]
75 #[configurable(metadata(docs::examples = "1"))]
76 #[configurable(metadata(docs::minimal = true))]
77 pub version: Option<InfluxDbVersion>,
78
79 #[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 #[configurable(metadata(docs::examples = "any"))]
91 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
92 pub consistency: Option<String>,
93
94 #[configurable(metadata(docs::examples = "autogen"))]
98 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
99 pub retention_policy_name: Option<String>,
100
101 #[configurable(metadata(docs::examples = "todd"))]
105 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
106 pub username: Option<String>,
107
108 #[configurable(metadata(docs::examples = "${INFLUXDB_PASSWORD}"))]
112 #[configurable(metadata(docs::relevant_when = "version = \"1\""))]
113 pub password: Option<SensitiveString>,
114
115 #[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 #[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 #[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 #[configurable(metadata(docs::examples = "hostname"))]
175 pub host_key: Option<OptionalValuePath>,
176
177 #[configurable(metadata(docs::examples = "text"))]
181 pub message_key: Option<OptionalValuePath>,
182
183 #[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 let Some(message_path) = log.message_path().cloned().as_ref() {
318 log.rename_key(message_path, (PathPrefix::Event, &self.message_key));
319 }
320 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 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 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 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 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 _ = config.build(cx.clone()).await.unwrap();
1011
1012 let (_guard, addr) = next_addr();
1013 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 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 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 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 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 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}