1use async_trait::async_trait;
2use futures::StreamExt;
3use futures_util::stream::BoxStream;
4use indoc::indoc;
5use vector_lib::{
6 codecs::JsonSerializerConfig,
7 configurable::configurable_component,
8 lookup,
9 lookup::lookup_v2::{ConfigValuePath, OptionalTargetPath, OptionalValuePath},
10 sensitive_string::SensitiveString,
11 sink::StreamSink,
12};
13
14use super::{
15 config_host_key,
16 logs::{HOST, HumioLogsConfig},
17};
18use crate::{
19 config::{
20 AcknowledgementsConfig, DynValidatedSink, GenerateConfig, Input, SinkConfig, SinkContext,
21 TransformContext, ValidatedSink,
22 },
23 event::{Event, EventArray, EventContainer},
24 sinks::{
25 Healthcheck, VectorSink,
26 splunk_hec::{common::SplunkHecDefaultBatchSettings, logs::config::ValidatedHecLogsSink},
27 util::{BatchConfig, Compression, HttpEndpoint, TowerRequestConfig},
28 },
29 template::Template,
30 tls::TlsConfig,
31 transforms::{
32 FunctionTransform, OutputBuffer,
33 metric_to_log::{MetricToLog, MetricToLogConfig},
34 },
35};
36
37#[configurable_component(sink("humio_metrics", "Deliver metric event data to Humio."))]
46#[derive(Clone, Debug)]
47#[serde(deny_unknown_fields)]
48pub struct HumioMetricsConfig {
49 #[serde(flatten)]
50 transform: MetricToLogConfig,
51
52 #[configurable(metadata(
54 docs::examples = "${HUMIO_TOKEN}",
55 docs::examples = "A94A8FE5CCB19BA61C4C08"
56 ))]
57 token: SensitiveString,
58
59 #[serde(alias = "host")]
66 #[serde(default = "default_endpoint")]
67 #[configurable(metadata(
68 docs::examples = "http://127.0.0.1",
69 docs::examples = "https://example.com",
70 ))]
71 pub(super) endpoint: HttpEndpoint,
72
73 source: Option<Template>,
77
78 #[configurable(metadata(
82 docs::examples = "json",
83 docs::examples = "none",
84 docs::examples = "event_type-{{ event_type }}"
85 ))]
86 event_type: Option<Template>,
87
88 #[serde(default = "config_host_key")]
95 host_key: OptionalValuePath,
96
97 #[serde(default)]
105 indexed_fields: Vec<ConfigValuePath>,
106
107 #[serde(default)]
117 #[configurable(metadata(
118 docs::examples = "index-{{ host }}",
119 docs::examples = "custom_index"
120 ))]
121 index: Option<Template>,
122
123 #[configurable(derived)]
124 #[serde(default)]
125 compression: Compression,
126
127 #[configurable(derived)]
128 #[serde(default)]
129 request: TowerRequestConfig,
130
131 #[configurable(derived)]
132 #[serde(default)]
133 batch: BatchConfig<SplunkHecDefaultBatchSettings>,
134
135 #[configurable(derived)]
136 tls: Option<TlsConfig>,
137
138 #[configurable(derived)]
139 #[serde(
140 default,
141 deserialize_with = "crate::serde::bool_or_struct",
142 skip_serializing_if = "crate::serde::is_default"
143 )]
144 acknowledgements: AcknowledgementsConfig,
145
146 #[serde(flatten)]
147 pub confinement: crate::template::ConfinementConfig,
148}
149
150fn default_endpoint() -> HttpEndpoint {
151 HttpEndpoint::parse(HOST).expect("static default endpoint should be a valid http(s) URL")
152}
153
154impl GenerateConfig for HumioMetricsConfig {
155 fn generate_config() -> serde_json::Value {
156 serde_yaml::from_str(indoc! {r#"
157 host_key: hostname
158 token: ${HUMIO_TOKEN}
159 "#})
160 .unwrap()
161 }
162}
163
164#[async_trait::async_trait]
165#[typetag::serde(name = "humio_metrics")]
166impl SinkConfig for HumioMetricsConfig {
167 fn confinement_config(&self) -> Option<&crate::template::ConfinementConfig> {
168 Some(&self.confinement)
169 }
170
171 fn input(&self) -> Input {
172 Input::metric()
173 }
174
175 fn acknowledgements(&self) -> &AcknowledgementsConfig {
176 &self.acknowledgements
177 }
178
179 fn as_dyn_validated(&self) -> Option<&dyn DynValidatedSink> {
180 Some(self)
181 }
182}
183
184#[derive(Clone, Debug)]
185pub struct ValidatedHumioMetrics {
186 hec: ValidatedHecLogsSink,
187}
188
189#[async_trait::async_trait]
190impl ValidatedSink for HumioMetricsConfig {
191 type Validated = ValidatedHumioMetrics;
192
193 fn validate(&self) -> crate::Result<ValidatedHumioMetrics> {
194 let humio_logs = self.build_humio_logs_config();
195 let validated = humio_logs.validate_with_component_name(Self::NAME)?;
196 Ok(ValidatedHumioMetrics { hec: validated.hec })
197 }
198
199 async fn build(
200 &self,
201 validated: &ValidatedHumioMetrics,
202 cx: SinkContext,
203 ) -> crate::Result<(VectorSink, Healthcheck)> {
204 let transform = self
205 .transform
206 .build_transform(&TransformContext::new_with_globals(cx.globals.clone()));
207
208 let humio_logs = self.build_humio_logs_config();
209
210 let (sink, healthcheck) = humio_logs.build_from_validated(cx, &validated.hec)?;
214
215 let sink = HumioMetricsSink {
216 inner: sink,
217 transform,
218 };
219 Ok((VectorSink::Stream(Box::new(sink)), healthcheck))
220 }
221}
222
223impl HumioMetricsConfig {
224 fn build_humio_logs_config(&self) -> HumioLogsConfig {
225 HumioLogsConfig {
226 token: self.token.clone(),
227 endpoint: self.endpoint.clone(),
228 source: self.source.clone(),
229 encoding: JsonSerializerConfig::default().into(),
230 event_type: self.event_type.clone(),
231 host_key: OptionalTargetPath::from(
232 vrl::path::PathPrefix::Event,
233 self.host_key.path.clone(),
234 ),
235 indexed_fields: self.indexed_fields.clone(),
236 index: self.index.clone(),
237 compression: self.compression,
238 request: self.request,
239 batch: self.batch,
240 tls: self.tls.clone(),
241 timestamp_nanos_key: None,
242 acknowledgements: Default::default(),
243 timestamp_key: OptionalTargetPath::from(
245 vrl::path::PathPrefix::Event,
246 Some(lookup::owned_value_path!("timestamp")),
247 ),
248 confinement: self.confinement.clone(),
249 }
250 }
251}
252
253pub struct HumioMetricsSink {
254 inner: VectorSink,
255 transform: MetricToLog,
256}
257
258#[async_trait]
259impl StreamSink<EventArray> for HumioMetricsSink {
260 async fn run(self: Box<Self>, input: BoxStream<'_, EventArray>) -> Result<(), ()> {
261 let mut transform = self.transform;
262 self.inner
263 .run(input.map(move |events| {
264 let mut buf = OutputBuffer::with_capacity(events.len());
265 for event in events.into_events() {
266 transform.transform(&mut buf, event);
267 }
268 let events = buf.into_events().map(Event::into_log).collect::<Vec<_>>();
270 events.into()
271 }))
272 .await
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use chrono::{Utc, offset::TimeZone};
279 use futures::stream;
280 use indoc::indoc;
281 use similar_asserts::assert_eq;
282 use vector_lib::metric_tags;
283
284 use super::*;
285 use crate::{
286 event::{
287 Event, Metric,
288 metric::{MetricKind, MetricValue, StatisticKind},
289 },
290 sinks::util::test::{build_test_server, load_sink},
291 test_util::{
292 self,
293 components::{HTTP_SINK_TAGS, run_and_assert_sink_compliance},
294 },
295 };
296
297 #[test]
298 fn generate_config() {
299 crate::test_util::test_generate_config::<HumioMetricsConfig>();
300 }
301
302 #[test]
303 fn validate_rejects_unconfined_template() {
304 use crate::config::ValidatedSink;
305
306 let config = HumioMetricsConfig {
307 transform: MetricToLogConfig::default(),
308 token: "token".to_string().into(),
309 endpoint: default_endpoint(),
310 source: None,
311 event_type: None,
312 host_key: config_host_key(),
313 indexed_fields: vec![],
314 index: Some("{{ index }}".try_into().unwrap()),
315 compression: Compression::default(),
316 request: TowerRequestConfig::default(),
317 batch: BatchConfig::default(),
318 tls: None,
319 acknowledgements: Default::default(),
320 confinement: Default::default(),
321 };
322
323 let result = config.validate();
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn test_endpoint_field() {
329 let (config, _) = load_sink::<HumioMetricsConfig>(indoc! {r#"
330 token = "atoken"
331 batch.max_events = 1
332 endpoint = "https://localhost:9200/"
333 "#})
334 .unwrap();
335
336 assert_eq!(
337 HttpEndpoint::parse("https://localhost:9200/").unwrap(),
338 config.endpoint
339 );
340 let (config, _) = load_sink::<HumioMetricsConfig>(indoc! {r#"
341 token = "atoken"
342 batch.max_events = 1
343 host = "https://localhost:9200/"
344 "#})
345 .unwrap();
346
347 assert_eq!(
348 HttpEndpoint::parse("https://localhost:9200/").unwrap(),
349 config.endpoint
350 );
351 }
352
353 #[tokio::test]
354 async fn smoke_json() {
355 let (mut config, cx) = load_sink::<HumioMetricsConfig>(indoc! {r#"
356 token = "atoken"
357 batch.max_events = 1
358 "#})
359 .unwrap();
360
361 let (_guard, addr) = test_util::addr::next_addr();
362 config.endpoint = HttpEndpoint::parse(&format!("http://{addr}")).unwrap();
365
366 let (sink, _) = SinkConfig::build(&config, cx).await.unwrap();
367
368 let (rx, _trigger, server) = build_test_server(addr);
369 tokio::spawn(server);
370
371 let metrics = vec![
373 Event::from(
374 Metric::new(
375 "metric1",
376 MetricKind::Incremental,
377 MetricValue::Counter { value: 42.0 },
378 )
379 .with_tags(Some(metric_tags!("os.host" => "somehost")))
380 .with_timestamp(Some(
381 Utc.with_ymd_and_hms(2020, 8, 18, 21, 0, 1)
382 .single()
383 .expect("invalid timestamp"),
384 )),
385 ),
386 Event::from(
387 Metric::new(
388 "metric2",
389 MetricKind::Absolute,
390 MetricValue::Distribution {
391 samples: vector_lib::samples![1.0 => 100, 2.0 => 200, 3.0 => 300],
392 statistic: StatisticKind::Histogram,
393 },
394 )
395 .with_tags(Some(metric_tags!("os.host" => "somehost")))
396 .with_timestamp(Some(
397 Utc.with_ymd_and_hms(2020, 8, 18, 21, 0, 2)
398 .single()
399 .expect("invalid timestamp"),
400 )),
401 ),
402 ];
403
404 let len = metrics.len();
405 run_and_assert_sink_compliance(sink, stream::iter(metrics), &HTTP_SINK_TAGS).await;
406
407 let output = rx.take(len).collect::<Vec<_>>().await;
408 assert_eq!(
409 r#"{"event":{"counter":{"value":42.0},"kind":"incremental","name":"metric1","tags":{"os.host":"somehost"}},"fields":{},"time":1597784401.0}"#,
410 output[0].1
411 );
412 assert_eq!(
413 r#"{"event":{"distribution":{"samples":[{"rate":100,"value":1.0},{"rate":200,"value":2.0},{"rate":300,"value":3.0}],"statistic":"histogram"},"kind":"absolute","name":"metric2","tags":{"os.host":"somehost"}},"fields":{},"time":1597784402.0}"#,
414 output[1].1
415 );
416 }
417
418 #[tokio::test]
419 async fn multi_value_tags() {
420 let (mut config, cx) = load_sink::<HumioMetricsConfig>(indoc! {r#"
421 token = "atoken"
422 batch.max_events = 1
423 metric_tag_values = "full"
424 "#})
425 .unwrap();
426
427 let (_guard, addr) = test_util::addr::next_addr();
428 config.endpoint = HttpEndpoint::parse(&format!("http://{addr}")).unwrap();
431
432 let (sink, _) = SinkConfig::build(&config, cx).await.unwrap();
433
434 let (rx, _trigger, server) = build_test_server(addr);
435 tokio::spawn(server);
436
437 let metrics = vec![Event::from(
439 Metric::new(
440 "metric1",
441 MetricKind::Incremental,
442 MetricValue::Counter { value: 42.0 },
443 )
444 .with_tags(Some(metric_tags!(
445 "code" => "200",
446 "code" => "success"
447 )))
448 .with_timestamp(Some(
449 Utc.with_ymd_and_hms(2020, 8, 18, 21, 0, 1)
450 .single()
451 .expect("invalid timestamp"),
452 )),
453 )];
454
455 let len = metrics.len();
456 run_and_assert_sink_compliance(sink, stream::iter(metrics), &HTTP_SINK_TAGS).await;
457
458 let output = rx.take(len).collect::<Vec<_>>().await;
459 assert_eq!(
460 r#"{"event":{"counter":{"value":42.0},"kind":"incremental","name":"metric1","tags":{"code":["200","success"]}},"fields":{},"time":1597784401.0}"#,
461 output[0].1
462 );
463 }
464}