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};
13use vrl::{event_path, path::OwnedValuePath, value::Kind};
14
15use super::{
16 Field, InfluxDb1Settings, InfluxDb2Settings, ProtocolVersion, encode_timestamp, healthcheck,
17 influx_line_protocol, influxdb_settings,
18};
19use crate::{
20 codecs::Transformer,
21 config::{AcknowledgementsConfig, GenerateConfig, Input, SinkConfig, SinkContext},
22 event::{Event, KeyString, MetricTags, Value},
23 http::HttpClient,
24 internal_events::InfluxdbEncodingError,
25 sinks::{
26 Healthcheck, VectorSink,
27 util::{
28 BatchConfig, Buffer, Compression, SinkBatchSettings, TowerRequestConfig,
29 http::{BatchedHttpSink, HttpEventEncoder, HttpSink},
30 },
31 },
32 tls::{TlsConfig, TlsSettings},
33};
34
35#[derive(Clone, Copy, Debug, Default)]
36pub struct InfluxDbLogsDefaultBatchSettings;
37
38impl SinkBatchSettings for InfluxDbLogsDefaultBatchSettings {
39 const MAX_EVENTS: Option<usize> = None;
40 const MAX_BYTES: Option<usize> = Some(1_000_000);
41 const TIMEOUT_SECS: f64 = 1.0;
42}
43
44#[configurable_component(sink("influxdb_logs", "Deliver log event data to InfluxDB."))]
46#[derive(Clone, Debug, Default)]
47#[serde(deny_unknown_fields)]
48pub struct InfluxDbLogsConfig {
49 #[configurable(
54 deprecated = "This field is deprecated, and `measurement` should be used instead."
55 )]
56 #[configurable(metadata(docs::examples = "service"))]
57 pub namespace: Option<String>,
58
59 #[configurable(metadata(docs::examples = "vector-logs"))]
61 pub measurement: Option<String>,
62
63 #[configurable(metadata(docs::examples = "http://localhost:8086"))]
67 pub endpoint: String,
68
69 #[serde(default)]
74 #[configurable(metadata(docs::examples = "field1"))]
75 #[configurable(metadata(docs::examples = "parent.child_field"))]
76 pub tags: Vec<KeyString>,
77
78 #[serde(flatten)]
79 pub influxdb1_settings: Option<InfluxDb1Settings>,
80
81 #[serde(flatten)]
82 pub influxdb2_settings: Option<InfluxDb2Settings>,
83
84 #[configurable(derived)]
85 #[serde(skip_serializing_if = "crate::serde::is_default", default)]
86 pub encoding: Transformer,
87
88 #[configurable(derived)]
89 #[serde(default)]
90 pub batch: BatchConfig<InfluxDbLogsDefaultBatchSettings>,
91
92 #[configurable(derived)]
93 #[serde(default)]
94 pub request: TowerRequestConfig,
95
96 #[configurable(derived)]
97 pub tls: Option<TlsConfig>,
98
99 #[configurable(derived)]
100 #[serde(
101 default,
102 deserialize_with = "crate::serde::bool_or_struct",
103 skip_serializing_if = "crate::serde::is_default"
104 )]
105 acknowledgements: AcknowledgementsConfig,
106
107 #[configurable(metadata(docs::examples = "hostname"))]
115 pub host_key: Option<OptionalValuePath>,
116
117 #[configurable(metadata(docs::examples = "text"))]
121 pub message_key: Option<OptionalValuePath>,
122
123 #[configurable(metadata(docs::examples = "source"))]
127 pub source_type_key: Option<OptionalValuePath>,
128}
129
130#[derive(Debug)]
131struct InfluxDbLogsSink {
132 uri: Uri,
133 token: String,
134 protocol_version: ProtocolVersion,
135 measurement: String,
136 tags: HashSet<KeyString>,
137 transformer: Transformer,
138 host_key: OwnedValuePath,
139 message_key: OwnedValuePath,
140 source_type_key: OwnedValuePath,
141}
142
143impl GenerateConfig for InfluxDbLogsConfig {
144 fn generate_config() -> toml::Value {
145 toml::from_str(indoc! {r#"
146 endpoint = "http://localhost:8086/"
147 namespace = "my-namespace"
148 tags = []
149 org = "my-org"
150 bucket = "my-bucket"
151 token = "${INFLUXDB_TOKEN}"
152 "#})
153 .unwrap()
154 }
155}
156
157#[async_trait::async_trait]
158#[typetag::serde(name = "influxdb_logs")]
159impl SinkConfig for InfluxDbLogsConfig {
160 async fn build(&self, cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
161 let measurement = self.get_measurement()?;
162 let tags: HashSet<KeyString> = self.tags.iter().cloned().collect();
163
164 let tls_settings = TlsSettings::from_options(self.tls.as_ref())?;
165 let client = HttpClient::new(tls_settings, cx.proxy())?;
166 let healthcheck = self.healthcheck(client.clone())?;
167
168 let batch = self.batch.into_batch_settings()?;
169 let request = self.request.into_settings();
170
171 let settings = influxdb_settings(
172 self.influxdb1_settings.clone(),
173 self.influxdb2_settings.clone(),
174 )
175 .unwrap();
176
177 let endpoint = self.endpoint.clone();
178 let uri = settings.write_uri(endpoint).unwrap();
179
180 let token = settings.token();
181 let protocol_version = settings.protocol_version();
182
183 let host_key = self
184 .host_key
185 .as_ref()
186 .and_then(|k| k.path.clone())
187 .or_else(|| log_schema().host_key().cloned())
188 .expect("global log_schema.host_key to be valid path");
189
190 let message_key = self
191 .message_key
192 .as_ref()
193 .and_then(|k| k.path.clone())
194 .or_else(|| log_schema().message_key().cloned())
195 .expect("global log_schema.message_key to be valid path");
196
197 let source_type_key = self
198 .source_type_key
199 .as_ref()
200 .and_then(|k| k.path.clone())
201 .or_else(|| log_schema().source_type_key().cloned())
202 .expect("global log_schema.source_type_key to be valid path");
203
204 let sink = InfluxDbLogsSink {
205 uri,
206 token: token.inner().to_owned(),
207 protocol_version,
208 measurement,
209 tags,
210 transformer: self.encoding.clone(),
211 host_key,
212 message_key,
213 source_type_key,
214 };
215
216 let sink = BatchedHttpSink::new(
217 sink,
218 Buffer::new(batch.size, Compression::None),
219 request,
220 batch.timeout,
221 client,
222 )
223 .sink_map_err(|error| error!(message = "Fatal influxdb_logs sink error.", %error, internal_log_rate_limit = false));
224
225 #[allow(deprecated)]
226 Ok((VectorSink::from_event_sink(sink), healthcheck))
227 }
228
229 fn input(&self) -> Input {
230 let requirements = schema::Requirement::empty()
231 .optional_meaning("message", Kind::bytes())
232 .optional_meaning("host", Kind::bytes())
233 .optional_meaning("timestamp", Kind::timestamp());
234
235 Input::log().with_schema_requirement(requirements)
236 }
237
238 fn acknowledgements(&self) -> &AcknowledgementsConfig {
239 &self.acknowledgements
240 }
241}
242
243struct InfluxDbLogsEncoder {
244 protocol_version: ProtocolVersion,
245 measurement: String,
246 tags: HashSet<KeyString>,
247 transformer: Transformer,
248 host_key: OwnedValuePath,
249 message_key: OwnedValuePath,
250 source_type_key: OwnedValuePath,
251}
252
253impl HttpEventEncoder<BytesMut> for InfluxDbLogsEncoder {
254 fn encode_event(&mut self, event: Event) -> Option<BytesMut> {
255 let mut log = event.into_log();
256 if let Some(message_path) = log.message_path().cloned().as_ref() {
261 log.rename_key(message_path, (PathPrefix::Event, &self.message_key));
262 }
263 if let Some(host_path) = log.host_path().cloned().as_ref() {
266 self.tags.replace(host_path.path.to_string().into());
267 log.rename_key(host_path, (PathPrefix::Event, &self.host_key));
268 }
269
270 if let Some(source_type_path) = log.source_type_path().cloned().as_ref() {
271 self.tags.replace(source_type_path.path.to_string().into());
272 log.rename_key(source_type_path, (PathPrefix::Event, &self.source_type_key));
273 }
274
275 self.tags.replace("metric_type".into());
276 log.insert(event_path!("metric_type"), "logs");
277
278 let timestamp = encode_timestamp(match log.remove_timestamp() {
280 Some(Value::Timestamp(ts)) => Some(ts),
281 _ => None,
282 });
283
284 let log = {
285 let mut event = Event::from(log);
286 self.transformer.transform(&mut event);
287 event.into_log()
288 };
289
290 let mut tags = MetricTags::default();
292 let mut fields: HashMap<KeyString, Field> = HashMap::new();
293 log.convert_to_fields().for_each(|(key, value)| {
294 if self.tags.contains(key.as_str()) {
295 tags.replace(key.into(), value.to_string_lossy().into_owned());
296 } else {
297 fields.insert(key, to_field(value));
298 }
299 });
300
301 let mut output = BytesMut::new();
302 if let Err(error_message) = influx_line_protocol(
303 self.protocol_version,
304 &self.measurement,
305 Some(tags),
306 Some(fields),
307 timestamp,
308 &mut output,
309 ) {
310 emit!(InfluxdbEncodingError {
311 error_message,
312 count: 1
313 });
314 return None;
315 };
316
317 Some(output)
318 }
319}
320
321impl HttpSink for InfluxDbLogsSink {
322 type Input = BytesMut;
323 type Output = BytesMut;
324 type Encoder = InfluxDbLogsEncoder;
325
326 fn build_encoder(&self) -> Self::Encoder {
327 InfluxDbLogsEncoder {
328 protocol_version: self.protocol_version,
329 measurement: self.measurement.clone(),
330 tags: self.tags.clone(),
331 transformer: self.transformer.clone(),
332 host_key: self.host_key.clone(),
333 message_key: self.message_key.clone(),
334 source_type_key: self.source_type_key.clone(),
335 }
336 }
337
338 async fn build_request(&self, events: Self::Output) -> crate::Result<Request<Bytes>> {
339 Request::post(&self.uri)
340 .header("Content-Type", "text/plain")
341 .header("Authorization", format!("Token {}", &self.token))
342 .body(events.freeze())
343 .map_err(Into::into)
344 }
345}
346
347impl InfluxDbLogsConfig {
348 fn get_measurement(&self) -> Result<String, &'static str> {
349 match (self.measurement.as_ref(), self.namespace.as_ref()) {
350 (Some(measure), Some(_)) => {
351 warn!("Option `namespace` has been superseded by `measurement`.");
352 Ok(measure.clone())
353 }
354 (Some(measure), None) => Ok(measure.clone()),
355 (None, Some(namespace)) => {
356 warn!(
357 "Option `namespace` has been deprecated. Use `measurement` instead. \
358 For example, you can use `measurement=<namespace>.vector` for the \
359 same effect."
360 );
361 Ok(format!("{namespace}.vector"))
362 }
363 (None, None) => Err("The `measurement` option is required."),
364 }
365 }
366
367 fn healthcheck(&self, client: HttpClient) -> crate::Result<Healthcheck> {
368 let config = self.clone();
369
370 let healthcheck = healthcheck(
371 config.endpoint,
372 config.influxdb1_settings,
373 config.influxdb2_settings,
374 client,
375 )?;
376
377 Ok(healthcheck)
378 }
379}
380
381fn to_field(value: &Value) -> Field {
382 match value {
383 Value::Integer(num) => Field::Int(*num),
384 Value::Float(num) => Field::Float(num.into_inner()),
385 Value::Boolean(b) => Field::Bool(*b),
386 _ => Field::String(value.to_string_lossy().into_owned()),
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use chrono::{Utc, offset::TimeZone};
393 use futures::{StreamExt, channel::mpsc, stream};
394 use http::{StatusCode, request::Parts};
395 use indoc::indoc;
396 use vector_lib::{
397 event::{BatchNotifier, BatchStatus, Event, LogEvent},
398 lookup::owned_value_path,
399 };
400
401 use super::*;
402 use crate::{
403 sinks::{
404 influxdb::test_util::{assert_fields, split_line_protocol, ts},
405 util::test::{build_test_server_status, load_sink},
406 },
407 test_util::{
408 addr::next_addr,
409 components::{
410 COMPONENT_ERROR_TAGS, HTTP_SINK_TAGS, run_and_assert_sink_compliance,
411 run_and_assert_sink_error,
412 },
413 },
414 };
415
416 type Receiver = mpsc::Receiver<(Parts, bytes::Bytes)>;
417
418 #[test]
419 fn generate_config() {
420 crate::test_util::test_generate_config::<InfluxDbLogsConfig>();
421 }
422
423 #[test]
424 fn test_config_without_tags() {
425 let config = indoc! {r#"
426 namespace: "vector-logs"
427 endpoint: "http://localhost:9999"
428 bucket: "my-bucket"
429 org: "my-org"
430 token: "my-token"
431 "#};
432
433 serde_yaml::from_str::<InfluxDbLogsConfig>(config).unwrap();
434 }
435
436 #[test]
437 fn test_config_measurement_from_namespace() {
438 let config = indoc! {r#"
439 namespace: "ns"
440 endpoint: "http://localhost:9999"
441 "#};
442
443 let sink_config = serde_yaml::from_str::<InfluxDbLogsConfig>(config).unwrap();
444 assert_eq!("ns.vector", sink_config.get_measurement().unwrap());
445 }
446
447 #[test]
448 fn test_encode_event_apply_rules() {
449 let mut event = Event::Log(LogEvent::from("hello"));
450 event
451 .as_mut_log()
452 .insert(event_path!("host"), "aws.cloud.eur");
453 event.as_mut_log().insert(event_path!("timestamp"), ts());
454
455 let mut sink = create_sink(
456 "http://localhost:9999",
457 "my-token",
458 ProtocolVersion::V1,
459 "vector",
460 ["metric_type", "host"].to_vec(),
461 );
462 sink.transformer
463 .set_except_fields(Some(vec!["host".into()]))
464 .unwrap();
465 let mut encoder = sink.build_encoder();
466
467 let bytes = encoder.encode_event(event.clone()).unwrap();
468 let string = std::str::from_utf8(&bytes).unwrap();
469
470 let line_protocol = split_line_protocol(string);
471 assert_eq!("vector", line_protocol.0);
472 assert_eq!("metric_type=logs", line_protocol.1);
473 assert_fields(line_protocol.2.to_string(), ["message=\"hello\""].to_vec());
474 assert_eq!("1542182950000000011\n", line_protocol.3);
475
476 sink.transformer
477 .set_except_fields(Some(vec!["metric_type".into()]))
478 .unwrap();
479 let mut encoder = sink.build_encoder();
480 let bytes = encoder.encode_event(event.clone()).unwrap();
481 let string = std::str::from_utf8(&bytes).unwrap();
482 let line_protocol = split_line_protocol(string);
483 assert_eq!(
484 "host=aws.cloud.eur", line_protocol.1,
485 "metric_type tag should be excluded"
486 );
487 assert_fields(line_protocol.2, ["message=\"hello\""].to_vec());
488 }
489
490 #[test]
491 fn test_encode_event_v1() {
492 let mut event = Event::Log(LogEvent::from("hello"));
493 event
494 .as_mut_log()
495 .insert(event_path!("host"), "aws.cloud.eur");
496 event
497 .as_mut_log()
498 .insert(event_path!("source_type"), "file");
499
500 event.as_mut_log().insert(event_path!("int"), 4i32);
501 event.as_mut_log().insert(event_path!("float"), 5.5);
502 event.as_mut_log().insert(event_path!("bool"), true);
503 event
504 .as_mut_log()
505 .insert(event_path!("string"), "thisisastring");
506 event.as_mut_log().insert(event_path!("timestamp"), ts());
507
508 let sink = create_sink(
509 "http://localhost:9999",
510 "my-token",
511 ProtocolVersion::V1,
512 "vector",
513 ["source_type", "host", "metric_type"].to_vec(),
514 );
515 let mut encoder = sink.build_encoder();
516
517 let bytes = encoder.encode_event(event).unwrap();
518 let string = std::str::from_utf8(&bytes).unwrap();
519
520 let line_protocol = split_line_protocol(string);
521 assert_eq!("vector", line_protocol.0);
522 assert_eq!(
523 "host=aws.cloud.eur,metric_type=logs,source_type=file",
524 line_protocol.1
525 );
526 assert_fields(
527 line_protocol.2.to_string(),
528 [
529 "int=4i",
530 "float=5.5",
531 "bool=true",
532 "string=\"thisisastring\"",
533 "message=\"hello\"",
534 ]
535 .to_vec(),
536 );
537
538 assert_eq!("1542182950000000011\n", line_protocol.3);
539 }
540
541 #[test]
542 fn test_encode_event() {
543 let mut event = Event::Log(LogEvent::from("hello"));
544 event
545 .as_mut_log()
546 .insert(event_path!("host"), "aws.cloud.eur");
547 event
548 .as_mut_log()
549 .insert(event_path!("source_type"), "file");
550
551 event.as_mut_log().insert(event_path!("int"), 4i32);
552 event.as_mut_log().insert(event_path!("float"), 5.5);
553 event.as_mut_log().insert(event_path!("bool"), true);
554 event
555 .as_mut_log()
556 .insert(event_path!("string"), "thisisastring");
557 event.as_mut_log().insert(event_path!("timestamp"), ts());
558
559 let sink = create_sink(
560 "http://localhost:9999",
561 "my-token",
562 ProtocolVersion::V2,
563 "vector",
564 ["source_type", "host", "metric_type"].to_vec(),
565 );
566 let mut encoder = sink.build_encoder();
567
568 let bytes = encoder.encode_event(event).unwrap();
569 let string = std::str::from_utf8(&bytes).unwrap();
570
571 let line_protocol = split_line_protocol(string);
572 assert_eq!("vector", line_protocol.0);
573 assert_eq!(
574 "host=aws.cloud.eur,metric_type=logs,source_type=file",
575 line_protocol.1
576 );
577 assert_fields(
578 line_protocol.2.to_string(),
579 [
580 "int=4i",
581 "float=5.5",
582 "bool=true",
583 "string=\"thisisastring\"",
584 "message=\"hello\"",
585 ]
586 .to_vec(),
587 );
588
589 assert_eq!("1542182950000000011\n", line_protocol.3);
590 }
591
592 #[test]
593 fn test_encode_event_without_tags() {
594 let mut event = Event::Log(LogEvent::from("hello"));
595
596 event.as_mut_log().insert(event_path!("value"), 100);
597 event.as_mut_log().insert(event_path!("timestamp"), ts());
598
599 let mut sink = create_sink(
600 "http://localhost:9999",
601 "my-token",
602 ProtocolVersion::V2,
603 "vector",
604 [].to_vec(),
605 );
606 sink.transformer
608 .set_except_fields(Some(vec!["metric_type".into()]))
609 .unwrap();
610 let mut encoder = sink.build_encoder();
611
612 let bytes = encoder.encode_event(event).unwrap();
613 let line = std::str::from_utf8(&bytes).unwrap();
614 assert!(
615 line.starts_with("vector "),
616 "measurement (without tags) should ends with space ' '"
617 );
618
619 let line_protocol = split_line_protocol(line);
620 assert_eq!("vector", line_protocol.0);
621 assert_eq!("", line_protocol.1, "tags should be empty");
622 assert_fields(
623 line_protocol.2,
624 ["value=100i", "message=\"hello\""].to_vec(),
625 );
626
627 assert_eq!("1542182950000000011\n", line_protocol.3);
628 }
629
630 #[test]
631 fn test_encode_nested_fields() {
632 let mut event = LogEvent::default();
633
634 event.insert(event_path!("a"), 1);
635 event.insert(event_path!("nested", "field"), "2");
636 event.insert(event_path!("nested", "bool"), true);
637 event.insert(event_path!("nested", "array", 0isize), "example-value");
638 event.insert(event_path!("nested", "array", 2isize), "another-value");
639 event.insert(event_path!("nested", "array", 3isize), 15);
640
641 let sink = create_sink(
642 "http://localhost:9999",
643 "my-token",
644 ProtocolVersion::V2,
645 "vector",
646 ["metric_type"].to_vec(),
647 );
648 let mut encoder = sink.build_encoder();
649
650 let bytes = encoder.encode_event(event.into()).unwrap();
651 let string = std::str::from_utf8(&bytes).unwrap();
652
653 let line_protocol = split_line_protocol(string);
654 assert_eq!("vector", line_protocol.0);
655 assert_eq!("metric_type=logs", line_protocol.1);
656 assert_fields(
657 line_protocol.2,
658 [
659 "a=1i",
660 "nested.array[0]=\"example-value\"",
661 "nested.array[1]=\"<null>\"",
662 "nested.array[2]=\"another-value\"",
663 "nested.array[3]=15i",
664 "nested.bool=true",
665 "nested.field=\"2\"",
666 ]
667 .to_vec(),
668 );
669 }
670
671 #[test]
672 fn test_add_tag() {
673 let mut event = Event::Log(LogEvent::from("hello"));
674 event
675 .as_mut_log()
676 .insert(event_path!("source_type"), "file");
677
678 event.as_mut_log().insert(event_path!("as_a_tag"), 10);
679 event.as_mut_log().insert(event_path!("timestamp"), ts());
680
681 let sink = create_sink(
682 "http://localhost:9999",
683 "my-token",
684 ProtocolVersion::V2,
685 "vector",
686 ["as_a_tag", "not_exists_field", "source_type", "metric_type"].to_vec(),
687 );
688 let mut encoder = sink.build_encoder();
689
690 let bytes = encoder.encode_event(event).unwrap();
691 let string = std::str::from_utf8(&bytes).unwrap();
692
693 let line_protocol = split_line_protocol(string);
694 assert_eq!("vector", line_protocol.0);
695 assert_eq!(
696 "as_a_tag=10,metric_type=logs,source_type=file",
697 line_protocol.1
698 );
699 assert_fields(line_protocol.2.to_string(), ["message=\"hello\""].to_vec());
700
701 assert_eq!("1542182950000000011\n", line_protocol.3);
702 }
703
704 #[tokio::test]
705 async fn smoke_v1() {
706 let rx = smoke_test(
707 r#"database = "my-database""#,
708 StatusCode::OK,
709 BatchStatus::Delivered,
710 )
711 .await;
712
713 let query = receive_response(rx).await;
714 assert!(query.contains("db=my-database"));
715 assert!(query.contains("precision=ns"));
716 }
717
718 #[tokio::test]
719 async fn smoke_v1_failure() {
720 smoke_test(
721 r#"database = "my-database""#,
722 StatusCode::BAD_REQUEST,
723 BatchStatus::Rejected,
724 )
725 .await;
726 }
727
728 #[tokio::test]
729 async fn smoke_v2() {
730 let rx = smoke_test(
731 indoc! {r#"
732 bucket = "my-bucket"
733 org = "my-org"
734 token = "my-token"
735 "#},
736 StatusCode::OK,
737 BatchStatus::Delivered,
738 )
739 .await;
740
741 let query = receive_response(rx).await;
742 assert!(query.contains("org=my-org"));
743 assert!(query.contains("bucket=my-bucket"));
744 assert!(query.contains("precision=ns"));
745 }
746
747 #[tokio::test]
748 async fn smoke_v2_failure() {
749 smoke_test(
750 indoc! {r#"
751 bucket = "my-bucket"
752 org = "my-org"
753 token = "my-token"
754 "#},
755 StatusCode::BAD_REQUEST,
756 BatchStatus::Rejected,
757 )
758 .await;
759 }
760
761 async fn smoke_test(
762 config: &str,
763 status_code: StatusCode,
764 batch_status: BatchStatus,
765 ) -> Receiver {
766 let config = format!(
767 indoc! {r#"
768 measurement = "vector"
769 endpoint = "http://localhost:9999"
770 {}
771 "#},
772 config
773 );
774 let (mut config, cx) = load_sink::<InfluxDbLogsConfig>(&config).unwrap();
775
776 _ = config.build(cx.clone()).await.unwrap();
778
779 let (_guard, addr) = next_addr();
780 let host = format!("http://{addr}");
783 config.endpoint = host;
784
785 let (sink, _) = config.build(cx).await.unwrap();
786
787 let (rx, _trigger, server) = build_test_server_status(addr, status_code);
788 tokio::spawn(server);
789
790 let (batch, mut receiver) = BatchNotifier::new_with_receiver();
791
792 let lines = std::iter::repeat(())
793 .map(move |_| "message_value")
794 .take(5)
795 .collect::<Vec<_>>();
796 let mut events = Vec::new();
797
798 for (i, line) in lines.iter().enumerate() {
800 let mut event = LogEvent::from(line.to_string()).with_batch_notifier(&batch);
801 event.insert(event_path!(format!("key{i}").as_str()), format!("value{i}"));
802
803 let timestamp = Utc
804 .with_ymd_and_hms(1970, 1, 1, 0, 0, (i as u32) + 1)
805 .single()
806 .expect("invalid timestamp");
807 event.insert(event_path!("timestamp"), timestamp);
808 event.insert(event_path!("source_type"), "file");
809
810 events.push(Event::Log(event));
811 }
812 drop(batch);
813
814 if batch_status == BatchStatus::Delivered {
815 run_and_assert_sink_compliance(sink, stream::iter(events), &HTTP_SINK_TAGS).await;
816 } else {
817 run_and_assert_sink_error(sink, stream::iter(events), &COMPONENT_ERROR_TAGS).await;
818 }
819
820 assert_eq!(receiver.try_recv(), Ok(batch_status));
821
822 rx
823 }
824
825 async fn receive_response(mut rx: Receiver) -> String {
826 let output = rx.next().await.unwrap();
827
828 let request = &output.0;
829 let query = request.uri.query().unwrap();
830
831 let body = std::str::from_utf8(&output.1[..]).unwrap();
832 let mut lines = body.lines();
833
834 assert_eq!(5, lines.clone().count());
835 assert_line_protocol(0, lines.next());
836
837 query.into()
838 }
839
840 fn assert_line_protocol(i: i64, value: Option<&str>) {
841 let line_protocol = split_line_protocol(value.unwrap());
843 assert_eq!("vector", line_protocol.0);
844 assert_eq!("metric_type=logs,source_type=file", line_protocol.1);
845 assert_fields(
846 line_protocol.2.to_string(),
847 [
848 &*format!("key{i}=\"value{i}\""),
849 "message=\"message_value\"",
850 ]
851 .to_vec(),
852 );
853
854 assert_eq!(((i + 1) * 1000000000).to_string(), line_protocol.3);
855 }
856
857 fn create_sink(
858 uri: &str,
859 token: &str,
860 protocol_version: ProtocolVersion,
861 measurement: &str,
862 tags: Vec<&str>,
863 ) -> InfluxDbLogsSink {
864 let uri = uri.parse::<Uri>().unwrap();
865 let token = token.to_string();
866 let measurement = measurement.to_string();
867 let tags: HashSet<_> = tags.into_iter().map(|tag| tag.into()).collect();
868 InfluxDbLogsSink {
869 uri,
870 token,
871 protocol_version,
872 measurement,
873 tags,
874 transformer: Default::default(),
875 host_key: owned_value_path!("host"),
876 message_key: owned_value_path!("message"),
877 source_type_key: owned_value_path!("source_type"),
878 }
879 }
880}
881
882#[cfg(feature = "influxdb-integration-tests")]
883#[cfg(test)]
884mod integration_tests {
885 use std::sync::Arc;
886
887 use chrono::Utc;
888 use futures::stream;
889 use vector_lib::{
890 codecs::BytesDeserializerConfig,
891 config::{LegacyKey, LogNamespace},
892 event::{BatchNotifier, BatchStatus, Event, LogEvent},
893 lookup::{owned_value_path, path},
894 };
895 use vrl::value;
896
897 use super::*;
898 use crate::{
899 config::SinkContext,
900 sinks::influxdb::{
901 InfluxDb2Settings,
902 logs::InfluxDbLogsConfig,
903 test_util::{BUCKET, ORG, TOKEN, address_v2, onboarding_v2},
904 },
905 test_util::components::{HTTP_SINK_TAGS, run_and_assert_sink_compliance},
906 };
907
908 #[tokio::test]
909 async fn influxdb2_logs_put_data() {
910 let endpoint = address_v2();
911 onboarding_v2(&endpoint).await;
912
913 let now = Utc::now();
914 let measure = format!(
915 "vector-{}",
916 now.timestamp_nanos_opt().expect("Timestamp out of range")
917 );
918
919 let cx = SinkContext::default();
920
921 let config = InfluxDbLogsConfig {
922 namespace: None,
923 measurement: Some(measure.clone()),
924 endpoint: endpoint.clone(),
925 tags: Default::default(),
926 influxdb1_settings: None,
927 influxdb2_settings: Some(InfluxDb2Settings {
928 org: ORG.to_string(),
929 bucket: BUCKET.to_string(),
930 token: TOKEN.to_string().into(),
931 }),
932 encoding: Default::default(),
933 batch: Default::default(),
934 request: Default::default(),
935 tls: None,
936 acknowledgements: Default::default(),
937 host_key: None,
938 message_key: None,
939 source_type_key: None,
940 };
941
942 let (sink, _) = config.build(cx).await.unwrap();
943
944 let (batch, mut receiver) = BatchNotifier::new_with_receiver();
945
946 let mut event1 = LogEvent::from("message_1").with_batch_notifier(&batch);
947 event1.insert(event_path!("host"), "aws.cloud.eur");
948 event1.insert(event_path!("source_type"), "file");
949
950 let mut event2 = LogEvent::from("message_2").with_batch_notifier(&batch);
951 event2.insert(event_path!("host"), "aws.cloud.eur");
952 event2.insert(event_path!("source_type"), "file");
953
954 let mut namespaced_log =
955 LogEvent::from(value!("namespaced message")).with_batch_notifier(&batch);
956 LogNamespace::Vector.insert_source_metadata(
957 "file",
958 &mut namespaced_log,
959 Some(LegacyKey::Overwrite(path!("host"))),
960 path!("host"),
961 "aws.cloud.eur",
962 );
963 LogNamespace::Vector.insert_standard_vector_source_metadata(
964 &mut namespaced_log,
965 "file",
966 now,
967 );
968 let schema = BytesDeserializerConfig
969 .schema_definition(LogNamespace::Vector)
970 .with_metadata_field(
971 &owned_value_path!("file", "host"),
972 Kind::bytes(),
973 Some("host"),
974 );
975 namespaced_log
976 .metadata_mut()
977 .set_schema_definition(&Arc::new(schema));
978
979 drop(batch);
980
981 let events = vec![
982 Event::Log(event1),
983 Event::Log(event2),
984 Event::Log(namespaced_log),
985 ];
986
987 run_and_assert_sink_compliance(sink, stream::iter(events), &HTTP_SINK_TAGS).await;
988
989 assert_eq!(receiver.try_recv(), Ok(BatchStatus::Delivered));
990
991 let mut body = std::collections::HashMap::new();
992 body.insert("query", format!("from(bucket:\"my-bucket\") |> range(start: 0) |> filter(fn: (r) => r._measurement == \"{}\")", measure.clone()));
993 body.insert("type", "flux".to_owned());
994
995 let client = reqwest::Client::builder()
996 .danger_accept_invalid_certs(true)
997 .build()
998 .unwrap();
999
1000 let res = client
1001 .post(format!("{endpoint}/api/v2/query?org=my-org"))
1002 .json(&body)
1003 .header("accept", "application/json")
1004 .header("Authorization", "Token my-token")
1005 .send()
1006 .await
1007 .unwrap();
1008 let string = res.text().await.unwrap();
1009
1010 let lines = string.split('\n').collect::<Vec<&str>>();
1011 let header = lines[0].split(',').collect::<Vec<&str>>();
1012 let record1 = lines[1].split(',').collect::<Vec<&str>>();
1013 let record2 = lines[2].split(',').collect::<Vec<&str>>();
1014 let record_ns = lines[3].split(',').collect::<Vec<&str>>();
1015
1016 assert_eq!(
1018 record1[header
1019 .iter()
1020 .position(|&r| r.trim() == "_measurement")
1021 .unwrap()]
1022 .trim(),
1023 measure.clone()
1024 );
1025 assert_eq!(
1026 record2[header
1027 .iter()
1028 .position(|&r| r.trim() == "_measurement")
1029 .unwrap()]
1030 .trim(),
1031 measure.clone()
1032 );
1033 assert_eq!(
1034 record_ns[header
1035 .iter()
1036 .position(|&r| r.trim() == "_measurement")
1037 .unwrap()]
1038 .trim(),
1039 measure.clone()
1040 );
1041
1042 assert_eq!(
1044 record1[header
1045 .iter()
1046 .position(|&r| r.trim() == "metric_type")
1047 .unwrap()]
1048 .trim(),
1049 "logs"
1050 );
1051 assert_eq!(
1052 record2[header
1053 .iter()
1054 .position(|&r| r.trim() == "metric_type")
1055 .unwrap()]
1056 .trim(),
1057 "logs"
1058 );
1059 assert_eq!(
1060 record_ns[header
1061 .iter()
1062 .position(|&r| r.trim() == "metric_type")
1063 .unwrap()]
1064 .trim(),
1065 "logs"
1066 );
1067 assert_eq!(
1068 record1[header.iter().position(|&r| r.trim() == "host").unwrap()].trim(),
1069 "aws.cloud.eur"
1070 );
1071 assert_eq!(
1072 record2[header.iter().position(|&r| r.trim() == "host").unwrap()].trim(),
1073 "aws.cloud.eur"
1074 );
1075 assert_eq!(
1076 record_ns[header.iter().position(|&r| r.trim() == "host").unwrap()].trim(),
1077 "aws.cloud.eur"
1078 );
1079 assert_eq!(
1080 record1[header
1081 .iter()
1082 .position(|&r| r.trim() == "source_type")
1083 .unwrap()]
1084 .trim(),
1085 "file"
1086 );
1087 assert_eq!(
1088 record2[header
1089 .iter()
1090 .position(|&r| r.trim() == "source_type")
1091 .unwrap()]
1092 .trim(),
1093 "file"
1094 );
1095 assert_eq!(
1096 record_ns[header
1097 .iter()
1098 .position(|&r| r.trim() == "source_type")
1099 .unwrap()]
1100 .trim(),
1101 "file"
1102 );
1103
1104 assert_eq!(
1106 record1[header.iter().position(|&r| r.trim() == "_field").unwrap()].trim(),
1107 "message"
1108 );
1109 assert_eq!(
1110 record2[header.iter().position(|&r| r.trim() == "_field").unwrap()].trim(),
1111 "message"
1112 );
1113 assert_eq!(
1114 record_ns[header.iter().position(|&r| r.trim() == "_field").unwrap()].trim(),
1115 "message"
1116 );
1117 assert_eq!(
1118 record1[header.iter().position(|&r| r.trim() == "_value").unwrap()].trim(),
1119 "message_1"
1120 );
1121 assert_eq!(
1122 record2[header.iter().position(|&r| r.trim() == "_value").unwrap()].trim(),
1123 "message_2"
1124 );
1125 assert_eq!(
1126 record_ns[header.iter().position(|&r| r.trim() == "_value").unwrap()].trim(),
1127 "namespaced message"
1128 );
1129 }
1130}