Skip to main content

vector/sinks/humio/
metrics.rs

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/// Configuration for the `humio_metrics` sink.
37//
38// TODO: This sink overlaps almost entirely with the `humio_logs` sink except for the metric-to-log
39// transform that it uses to get metrics into the shape of a log before sending to Humio. However,
40// due to issues with aliased fields and flattened fields [1] in `serde`, we can't embed the
41// `humio_logs` config here.
42//
43// [1]: https://github.com/serde-rs/serde/issues/1504
44#[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    /// The Humio ingestion token.
52    #[configurable(metadata(
53        docs::examples = "${HUMIO_TOKEN}",
54        docs::examples = "A94A8FE5CCB19BA61C4C08"
55    ))]
56    token: SensitiveString,
57
58    /// The base URL of the Humio instance.
59    ///
60    /// The scheme (`http` or `https`) must be specified. No path should be included since the paths defined
61    /// by the [`Splunk`][splunk] API are used.
62    ///
63    /// [splunk]: https://docs.splunk.com/Documentation/Splunk/8.0.0/Data/HECRESTendpoints
64    #[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    /// The source of events sent to this sink.
73    ///
74    /// Typically the filename the metrics originated from. Maps to `@source` in Humio.
75    source: Option<Template>,
76
77    /// The type of events sent to this sink. Humio uses this as the name of the parser to use to ingest the data.
78    ///
79    /// If unset, Humio defaults it to none.
80    #[configurable(metadata(
81        docs::examples = "json",
82        docs::examples = "none",
83        docs::examples = "{{ event_type }}"
84    ))]
85    event_type: Option<Template>,
86
87    /// Overrides the name of the log field used to retrieve the hostname to send to Humio.
88    ///
89    /// By default, the [global `log_schema.host_key` option][global_host_key] is used if log
90    /// events are Legacy namespaced, or the semantic meaning of "host" is used, if defined.
91    ///
92    /// [global_host_key]: https://vector.dev/docs/reference/configuration/global-options/#log_schema.host_key
93    #[serde(default = "config_host_key")]
94    host_key: OptionalValuePath,
95
96    /// Event fields to be added to Humio’s extra fields.
97    ///
98    /// Can be used to tag events by specifying fields starting with `#`.
99    ///
100    /// For more information, see [Humio’s Format of Data][humio_data_format].
101    ///
102    /// [humio_data_format]: https://docs.humio.com/integrations/data-shippers/hec/#format-of-data
103    #[serde(default)]
104    indexed_fields: Vec<ConfigValuePath>,
105
106    /// Optional name of the repository to ingest into.
107    ///
108    /// In public-facing APIs, this must (if present) be equal to the repository used to create the ingest token used for authentication.
109    ///
110    /// In private cluster setups, Humio can be configured to allow these to be different.
111    ///
112    /// For more information, see [Humio’s Format of Data][humio_data_format].
113    ///
114    /// [humio_data_format]: https://docs.humio.com/integrations/data-shippers/hec/#format-of-data
115    #[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            // hard coded as humio expects this format so no sense in making it configurable
187            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        // Route through the inner Humio helper so the wrapped
195        // `humio_logs` sink's gauge isn't emitted alongside our own —
196        // operators watching `security_confinement_disabled` for a
197        // `humio_metrics` sink should only see one series.
198        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                // Awkward but necessary for the `EventArray` type
234                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        // Swap out the endpoint so we can force send it
297        // to our local server
298        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        // Make our test metrics.
306        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        // Swap out the endpoint so we can force send it
363        // to our local server
364        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        // Make our test metrics.
372        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}