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, GenerateConfig, Input, SinkConfig, SinkContext, TransformContext,
21 },
22 event::{Event, EventArray, EventContainer},
23 sinks::{
24 Healthcheck, VectorSink,
25 splunk_hec::common::SplunkHecDefaultBatchSettings,
26 util::{BatchConfig, Compression, TowerRequestConfig},
27 },
28 template::Template,
29 tls::TlsConfig,
30 transforms::{
31 FunctionTransform, OutputBuffer,
32 metric_to_log::{MetricToLog, MetricToLogConfig},
33 },
34};
35
36#[configurable_component(sink("humio_metrics", "Deliver metric event data to Humio."))]
45#[derive(Clone, Debug)]
46#[serde(deny_unknown_fields)]
47pub struct HumioMetricsConfig {
48 #[serde(flatten)]
49 transform: MetricToLogConfig,
50
51 #[configurable(metadata(
53 docs::examples = "${HUMIO_TOKEN}",
54 docs::examples = "A94A8FE5CCB19BA61C4C08"
55 ))]
56 token: SensitiveString,
57
58 #[serde(alias = "host")]
65 #[serde(default = "default_endpoint")]
66 #[configurable(metadata(
67 docs::examples = "http://127.0.0.1",
68 docs::examples = "https://example.com",
69 ))]
70 pub(super) endpoint: String,
71
72 source: Option<Template>,
76
77 #[configurable(metadata(
81 docs::examples = "json",
82 docs::examples = "none",
83 docs::examples = "{{ event_type }}"
84 ))]
85 event_type: Option<Template>,
86
87 #[serde(default = "config_host_key")]
94 host_key: OptionalValuePath,
95
96 #[serde(default)]
104 indexed_fields: Vec<ConfigValuePath>,
105
106 #[serde(default)]
116 #[configurable(metadata(docs::examples = "{{ host }}", docs::examples = "custom_index"))]
117 index: Option<Template>,
118
119 #[configurable(derived)]
120 #[serde(default)]
121 compression: Compression,
122
123 #[configurable(derived)]
124 #[serde(default)]
125 request: TowerRequestConfig,
126
127 #[configurable(derived)]
128 #[serde(default)]
129 batch: BatchConfig<SplunkHecDefaultBatchSettings>,
130
131 #[configurable(derived)]
132 tls: Option<TlsConfig>,
133
134 #[configurable(derived)]
135 #[serde(
136 default,
137 deserialize_with = "crate::serde::bool_or_struct",
138 skip_serializing_if = "crate::serde::is_default"
139 )]
140 acknowledgements: AcknowledgementsConfig,
141
142 #[serde(flatten)]
143 pub confinement: crate::template::ConfinementConfig,
144}
145
146fn default_endpoint() -> String {
147 HOST.to_string()
148}
149
150impl GenerateConfig for HumioMetricsConfig {
151 fn generate_config() -> toml::Value {
152 toml::from_str(indoc! {r#"
153 host_key = "hostname"
154 token = "${HUMIO_TOKEN}"
155 "#})
156 .unwrap()
157 }
158}
159
160#[async_trait::async_trait]
161#[typetag::serde(name = "humio_metrics")]
162impl SinkConfig for HumioMetricsConfig {
163 async fn build(&self, cx: SinkContext) -> crate::Result<(VectorSink, Healthcheck)> {
164 let transform = self
165 .transform
166 .build_transform(&TransformContext::new_with_globals(cx.globals.clone()));
167
168 let sink = HumioLogsConfig {
169 token: self.token.clone(),
170 endpoint: self.endpoint.clone(),
171 source: self.source.clone(),
172 encoding: JsonSerializerConfig::default().into(),
173 event_type: self.event_type.clone(),
174 host_key: OptionalTargetPath::from(
175 vrl::path::PathPrefix::Event,
176 self.host_key.path.clone(),
177 ),
178 indexed_fields: self.indexed_fields.clone(),
179 index: self.index.clone(),
180 compression: self.compression,
181 request: self.request,
182 batch: self.batch,
183 tls: self.tls.clone(),
184 timestamp_nanos_key: None,
185 acknowledgements: Default::default(),
186 timestamp_key: OptionalTargetPath::from(
188 vrl::path::PathPrefix::Event,
189 Some(lookup::owned_value_path!("timestamp")),
190 ),
191 confinement: self.confinement.clone(),
192 };
193
194 let (sink, healthcheck) = sink.build_without_confinement_gauge(cx, Self::NAME)?;
199
200 let sink = HumioMetricsSink {
201 inner: sink,
202 transform,
203 };
204
205 self.confinement.set_confinement_gauge("sink", Self::NAME);
206 Ok((VectorSink::Stream(Box::new(sink)), healthcheck))
207 }
208
209 fn input(&self) -> Input {
210 Input::metric()
211 }
212
213 fn acknowledgements(&self) -> &AcknowledgementsConfig {
214 &self.acknowledgements
215 }
216}
217
218pub struct HumioMetricsSink {
219 inner: VectorSink,
220 transform: MetricToLog,
221}
222
223#[async_trait]
224impl StreamSink<EventArray> for HumioMetricsSink {
225 async fn run(self: Box<Self>, input: BoxStream<'_, EventArray>) -> Result<(), ()> {
226 let mut transform = self.transform;
227 self.inner
228 .run(input.map(move |events| {
229 let mut buf = OutputBuffer::with_capacity(events.len());
230 for event in events.into_events() {
231 transform.transform(&mut buf, event);
232 }
233 let events = buf.into_events().map(Event::into_log).collect::<Vec<_>>();
235 events.into()
236 }))
237 .await
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use chrono::{Utc, offset::TimeZone};
244 use futures::stream;
245 use indoc::indoc;
246 use similar_asserts::assert_eq;
247 use vector_lib::metric_tags;
248
249 use super::*;
250 use crate::{
251 event::{
252 Event, Metric,
253 metric::{MetricKind, MetricValue, StatisticKind},
254 },
255 sinks::util::test::{build_test_server, load_sink},
256 test_util::{
257 self,
258 components::{HTTP_SINK_TAGS, run_and_assert_sink_compliance},
259 },
260 };
261
262 #[test]
263 fn generate_config() {
264 crate::test_util::test_generate_config::<HumioMetricsConfig>();
265 }
266
267 #[test]
268 fn test_endpoint_field() {
269 let (config, _) = load_sink::<HumioMetricsConfig>(indoc! {r#"
270 token = "atoken"
271 batch.max_events = 1
272 endpoint = "https://localhost:9200/"
273 "#})
274 .unwrap();
275
276 assert_eq!("https://localhost:9200/".to_string(), config.endpoint);
277 let (config, _) = load_sink::<HumioMetricsConfig>(indoc! {r#"
278 token = "atoken"
279 batch.max_events = 1
280 host = "https://localhost:9200/"
281 "#})
282 .unwrap();
283
284 assert_eq!("https://localhost:9200/".to_string(), config.endpoint);
285 }
286
287 #[tokio::test]
288 async fn smoke_json() {
289 let (mut config, cx) = load_sink::<HumioMetricsConfig>(indoc! {r#"
290 token = "atoken"
291 batch.max_events = 1
292 "#})
293 .unwrap();
294
295 let (_guard, addr) = test_util::addr::next_addr();
296 config.endpoint = format!("http://{addr}");
299
300 let (sink, _) = config.build(cx).await.unwrap();
301
302 let (rx, _trigger, server) = build_test_server(addr);
303 tokio::spawn(server);
304
305 let metrics = vec![
307 Event::from(
308 Metric::new(
309 "metric1",
310 MetricKind::Incremental,
311 MetricValue::Counter { value: 42.0 },
312 )
313 .with_tags(Some(metric_tags!("os.host" => "somehost")))
314 .with_timestamp(Some(
315 Utc.with_ymd_and_hms(2020, 8, 18, 21, 0, 1)
316 .single()
317 .expect("invalid timestamp"),
318 )),
319 ),
320 Event::from(
321 Metric::new(
322 "metric2",
323 MetricKind::Absolute,
324 MetricValue::Distribution {
325 samples: vector_lib::samples![1.0 => 100, 2.0 => 200, 3.0 => 300],
326 statistic: StatisticKind::Histogram,
327 },
328 )
329 .with_tags(Some(metric_tags!("os.host" => "somehost")))
330 .with_timestamp(Some(
331 Utc.with_ymd_and_hms(2020, 8, 18, 21, 0, 2)
332 .single()
333 .expect("invalid timestamp"),
334 )),
335 ),
336 ];
337
338 let len = metrics.len();
339 run_and_assert_sink_compliance(sink, stream::iter(metrics), &HTTP_SINK_TAGS).await;
340
341 let output = rx.take(len).collect::<Vec<_>>().await;
342 assert_eq!(
343 r#"{"event":{"counter":{"value":42.0},"kind":"incremental","name":"metric1","tags":{"os.host":"somehost"}},"fields":{},"time":1597784401.0}"#,
344 output[0].1
345 );
346 assert_eq!(
347 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}"#,
348 output[1].1
349 );
350 }
351
352 #[tokio::test]
353 async fn multi_value_tags() {
354 let (mut config, cx) = load_sink::<HumioMetricsConfig>(indoc! {r#"
355 token = "atoken"
356 batch.max_events = 1
357 metric_tag_values = "full"
358 "#})
359 .unwrap();
360
361 let (_guard, addr) = test_util::addr::next_addr();
362 config.endpoint = format!("http://{addr}");
365
366 let (sink, _) = config.build(cx).await.unwrap();
367
368 let (rx, _trigger, server) = build_test_server(addr);
369 tokio::spawn(server);
370
371 let metrics = vec![Event::from(
373 Metric::new(
374 "metric1",
375 MetricKind::Incremental,
376 MetricValue::Counter { value: 42.0 },
377 )
378 .with_tags(Some(metric_tags!(
379 "code" => "200",
380 "code" => "success"
381 )))
382 .with_timestamp(Some(
383 Utc.with_ymd_and_hms(2020, 8, 18, 21, 0, 1)
384 .single()
385 .expect("invalid timestamp"),
386 )),
387 )];
388
389 let len = metrics.len();
390 run_and_assert_sink_compliance(sink, stream::iter(metrics), &HTTP_SINK_TAGS).await;
391
392 let output = rx.take(len).collect::<Vec<_>>().await;
393 assert_eq!(
394 r#"{"event":{"counter":{"value":42.0},"kind":"incremental","name":"metric1","tags":{"code":["200","success"]}},"fields":{},"time":1597784401.0}"#,
395 output[0].1
396 );
397 }
398}