Skip to main content

vector/sinks/
mezmo.rs

1use std::time::SystemTime;
2
3use bytes::Bytes;
4use futures::{FutureExt, SinkExt};
5use http::{Request, StatusCode, Uri};
6use serde_json::json;
7use vector_lib::{configurable::configurable_component, sensitive_string::SensitiveString};
8use vrl::{
9    event_path,
10    value::{Kind, Value},
11};
12
13use crate::{
14    codecs::Transformer,
15    config::{AcknowledgementsConfig, GenerateConfig, Input, SinkConfig, SinkContext},
16    event::Event,
17    http::{Auth, HttpClient},
18    schema,
19    sinks::util::{
20        BatchConfig, BoxedRawValue, JsonArrayBuffer, PartitionBuffer, PartitionInnerBuffer,
21        RealtimeSizeBasedDefaultBatchSettings, TowerRequestConfig, UriSerde,
22        http::{HttpEventEncoder, HttpSink, PartitionHttpSink},
23    },
24    template::{TemplateRenderingError, UnconfinedTemplate},
25};
26
27const PATH: &str = "/logs/ingest";
28
29/// Configuration for the `mezmo` (formerly `logdna`) sink.
30#[configurable_component(sink("mezmo", "Deliver log event data to Mezmo."))]
31#[derive(Clone, Debug)]
32pub struct MezmoConfig {
33    /// The Ingestion API key.
34    #[configurable(metadata(docs::examples = "${LOGDNA_API_KEY}"))]
35    #[configurable(metadata(docs::examples = "ef8d5de700e7989468166c40fc8a0ccd"))]
36    api_key: SensitiveString,
37
38    /// The HTTP endpoint to send logs to.
39    ///
40    /// Both IP address and hostname are accepted formats.
41    #[serde(alias = "host")]
42    #[serde(default = "default_endpoint")]
43    #[configurable(metadata(docs::examples = "http://127.0.0.1"))]
44    #[configurable(metadata(docs::examples = "http://example.com"))]
45    endpoint: UriSerde,
46
47    /// The hostname that is attached to each batch of events.
48    #[configurable(metadata(docs::examples = "${HOSTNAME}"))]
49    #[configurable(metadata(docs::examples = "my-local-machine"))]
50    hostname: UnconfinedTemplate,
51
52    /// The MAC address that is attached to each batch of events.
53    #[configurable(metadata(docs::examples = "my-mac-address"))]
54    #[configurable(metadata(docs::human_name = "MAC Address"))]
55    mac: Option<String>,
56
57    /// The IP address that is attached to each batch of events.
58    #[configurable(metadata(docs::examples = "0.0.0.0"))]
59    #[configurable(metadata(docs::human_name = "IP Address"))]
60    ip: Option<String>,
61
62    /// The tags that are attached to each batch of events.
63    #[configurable(metadata(docs::examples = "tag1"))]
64    #[configurable(metadata(docs::examples = "tag2"))]
65    tags: Option<Vec<UnconfinedTemplate>>,
66
67    #[configurable(derived)]
68    #[serde(default, skip_serializing_if = "crate::serde::is_default")]
69    pub encoding: Transformer,
70
71    /// The default app that is set for events that do not contain a `file` or `app` field.
72    #[serde(default = "default_app")]
73    #[configurable(metadata(docs::examples = "my-app"))]
74    default_app: String,
75
76    /// The default environment that is set for events that do not contain an `env` field.
77    #[serde(default = "default_env")]
78    #[configurable(metadata(docs::examples = "staging"))]
79    default_env: String,
80
81    #[configurable(derived)]
82    #[serde(default)]
83    batch: BatchConfig<RealtimeSizeBasedDefaultBatchSettings>,
84
85    #[configurable(derived)]
86    #[serde(default)]
87    request: TowerRequestConfig,
88
89    #[configurable(derived)]
90    #[serde(
91        default,
92        deserialize_with = "crate::serde::bool_or_struct",
93        skip_serializing_if = "crate::serde::is_default"
94    )]
95    acknowledgements: AcknowledgementsConfig,
96}
97
98fn default_endpoint() -> UriSerde {
99    UriSerde {
100        uri: Uri::from_static("https://logs.mezmo.com"),
101        auth: None,
102    }
103}
104
105fn default_app() -> String {
106    "vector".to_owned()
107}
108
109fn default_env() -> String {
110    "production".to_owned()
111}
112
113impl GenerateConfig for MezmoConfig {
114    fn generate_config() -> serde_json::Value {
115        serde_yaml::from_str(indoc::indoc! {
116            r#"hostname: hostname
117            api_key: ${LOGDNA_API_KEY}"#,
118        })
119        .unwrap()
120    }
121}
122
123#[async_trait::async_trait]
124#[typetag::serde(name = "mezmo")]
125impl SinkConfig for MezmoConfig {
126    async fn build(
127        &self,
128        cx: SinkContext,
129    ) -> crate::Result<(super::VectorSink, super::Healthcheck)> {
130        let request_settings = self.request.into_settings();
131        let batch_settings = self.batch.into_batch_settings()?;
132        let client = HttpClient::new(None, cx.proxy())?;
133
134        let sink = PartitionHttpSink::new(
135            self.clone(),
136            PartitionBuffer::new(JsonArrayBuffer::new(batch_settings.size)),
137            request_settings,
138            batch_settings.timeout,
139            client.clone(),
140        )
141        .sink_map_err(|error| error!(message = "Fatal mezmo sink error.", %error, internal_log_rate_limit = false));
142
143        let healthcheck = healthcheck(self.clone(), client).boxed();
144
145        #[allow(deprecated)]
146        Ok((super::VectorSink::from_event_sink(sink), healthcheck))
147    }
148
149    fn input(&self) -> Input {
150        let requirement = schema::Requirement::empty()
151            .optional_meaning("timestamp", Kind::timestamp())
152            .optional_meaning("message", Kind::bytes());
153
154        Input::log().with_schema_requirement(requirement)
155    }
156
157    fn acknowledgements(&self) -> &AcknowledgementsConfig {
158        &self.acknowledgements
159    }
160}
161
162#[derive(Hash, Eq, PartialEq, Clone)]
163pub struct PartitionKey {
164    hostname: String,
165    tags: Option<Vec<String>>,
166}
167
168pub struct MezmoEventEncoder {
169    hostname: UnconfinedTemplate,
170    tags: Option<Vec<UnconfinedTemplate>>,
171    transformer: Transformer,
172    default_app: String,
173    default_env: String,
174}
175
176impl MezmoEventEncoder {
177    fn render_key(
178        &self,
179        event: &Event,
180    ) -> Result<PartitionKey, (Option<&str>, TemplateRenderingError)> {
181        let hostname = self
182            .hostname
183            .render_string(event)
184            .map_err(|e| (Some("hostname"), e))?;
185        let tags = self
186            .tags
187            .as_ref()
188            .map(|tags| {
189                let mut vec = Vec::with_capacity(tags.len());
190                for tag in tags {
191                    vec.push(tag.render_string(event).map_err(|e| (None, e))?);
192                }
193                Ok(Some(vec))
194            })
195            .unwrap_or(Ok(None))?;
196        Ok(PartitionKey { hostname, tags })
197    }
198}
199
200impl HttpEventEncoder<PartitionInnerBuffer<serde_json::Value, PartitionKey>> for MezmoEventEncoder {
201    fn encode_event(
202        &mut self,
203        mut event: Event,
204    ) -> Option<PartitionInnerBuffer<serde_json::Value, PartitionKey>> {
205        let key = self
206            .render_key(&event)
207            .map_err(|(field, error)| {
208                emit!(crate::internal_events::TemplateRenderingError {
209                    error,
210                    field,
211                    drop_event: true,
212                });
213            })
214            .ok()?;
215
216        self.transformer.transform(&mut event);
217        let mut log = event.into_log();
218
219        let line = log
220            .message_path()
221            .cloned()
222            .as_ref()
223            .and_then(|path| log.remove(path))
224            .unwrap_or_else(|| String::from("").into());
225
226        let timestamp: Value = log
227            .timestamp_path()
228            .cloned()
229            .and_then(|path| log.remove(&path))
230            .unwrap_or_else(|| chrono::Utc::now().into());
231
232        let mut map = serde_json::map::Map::new();
233
234        map.insert("line".to_string(), json!(line));
235        map.insert("timestamp".to_string(), json!(timestamp));
236
237        if let Some(env) = log.remove(event_path!("env")) {
238            map.insert("env".to_string(), json!(env));
239        }
240
241        if let Some(app) = log.remove(event_path!("app")) {
242            map.insert("app".to_string(), json!(app));
243        }
244
245        if let Some(file) = log.remove(event_path!("file")) {
246            map.insert("file".to_string(), json!(file));
247        }
248
249        if !map.contains_key("env") {
250            map.insert("env".to_string(), json!(self.default_env));
251        }
252
253        if !map.contains_key("app") && !map.contains_key("file") {
254            map.insert("app".to_string(), json!(self.default_app.as_str()));
255        }
256
257        if !log.is_empty_object() {
258            map.insert("meta".into(), json!(&log));
259        }
260
261        Some(PartitionInnerBuffer::new(map.into(), key))
262    }
263}
264
265impl HttpSink for MezmoConfig {
266    type Input = PartitionInnerBuffer<serde_json::Value, PartitionKey>;
267    type Output = PartitionInnerBuffer<Vec<BoxedRawValue>, PartitionKey>;
268    type Encoder = MezmoEventEncoder;
269
270    fn build_encoder(&self) -> Self::Encoder {
271        MezmoEventEncoder {
272            hostname: self.hostname.clone(),
273            tags: self.tags.clone(),
274            transformer: self.encoding.clone(),
275            default_app: self.default_app.clone(),
276            default_env: self.default_env.clone(),
277        }
278    }
279
280    async fn build_request(&self, output: Self::Output) -> crate::Result<http::Request<Bytes>> {
281        let (events, key) = output.into_parts();
282        let mut query = url::form_urlencoded::Serializer::new(String::new());
283
284        let now = SystemTime::now()
285            .duration_since(SystemTime::UNIX_EPOCH)
286            .expect("Time can't drift behind the epoch!")
287            .as_millis();
288
289        query.append_pair("hostname", &key.hostname);
290        query.append_pair("now", &now.to_string());
291
292        if let Some(mac) = &self.mac {
293            query.append_pair("mac", mac);
294        }
295
296        if let Some(ip) = &self.ip {
297            query.append_pair("ip", ip);
298        }
299
300        if let Some(tags) = &key.tags {
301            let tags = tags.join(",");
302            query.append_pair("tags", &tags);
303        }
304
305        let query = query.finish();
306
307        let body = crate::serde::json::to_bytes(&json!({
308            "lines": events,
309        }))
310        .unwrap()
311        .freeze();
312
313        let uri = self.build_uri(&query);
314
315        let mut request = Request::builder()
316            .uri(uri)
317            .method("POST")
318            .header("Content-Type", "application/json")
319            .body(body)
320            .unwrap();
321
322        let auth = Auth::Basic {
323            user: self.api_key.inner().to_string(),
324            password: SensitiveString::default(),
325        };
326
327        auth.apply(&mut request);
328
329        Ok(request)
330    }
331}
332
333impl MezmoConfig {
334    fn build_uri(&self, query: &str) -> Uri {
335        let host = &self.endpoint.uri;
336
337        let uri = format!("{host}{PATH}?{query}");
338
339        uri.parse::<http::Uri>()
340            .expect("This should be a valid uri")
341    }
342}
343
344async fn healthcheck(config: MezmoConfig, client: HttpClient) -> crate::Result<()> {
345    let uri = config.build_uri("");
346
347    let req = Request::post(uri).body(hyper::Body::empty()).unwrap();
348
349    let res = client.send(req).await?;
350
351    if res.status().is_server_error() {
352        return Err("Server returned a server error".into());
353    }
354
355    if res.status() == StatusCode::FORBIDDEN {
356        return Err("Token is not valid, 403 returned.".into());
357    }
358
359    Ok(())
360}
361
362#[cfg(test)]
363mod tests {
364    use futures::{StreamExt, channel::mpsc};
365    use futures_util::stream;
366    use http::{StatusCode, request::Parts};
367    use serde_json::json;
368    use vector_lib::event::{BatchNotifier, BatchStatus, Event, LogEvent};
369
370    use super::*;
371    use crate::{
372        config::SinkConfig,
373        sinks::util::test::{build_test_server_status, load_sink},
374        test_util::{
375            addr::next_addr,
376            components::{HTTP_SINK_TAGS, assert_sink_compliance},
377            random_lines,
378        },
379    };
380
381    #[test]
382    fn generate_config() {
383        crate::test_util::test_generate_config::<MezmoConfig>();
384    }
385
386    #[test]
387    fn encode_event() {
388        let (config, _cx) = load_sink::<MezmoConfig>(
389            r#"
390            api_key = "mylogtoken"
391            hostname = "vector"
392            default_env = "acceptance"
393            codec.except_fields = ["magic"]
394        "#,
395        )
396        .unwrap();
397        let mut encoder = config.build_encoder();
398
399        let mut event1 = Event::Log(LogEvent::from("hello world"));
400        event1.as_mut_log().insert(event_path!("app"), "notvector");
401        event1.as_mut_log().insert(event_path!("magic"), "vector");
402
403        let mut event2 = Event::Log(LogEvent::from("hello world"));
404        event2.as_mut_log().insert(event_path!("file"), "log.txt");
405
406        let event3 = Event::Log(LogEvent::from("hello world"));
407
408        let mut event4 = Event::Log(LogEvent::from("hello world"));
409        event4.as_mut_log().insert(event_path!("env"), "staging");
410
411        let event1_out = encoder.encode_event(event1).unwrap().into_parts().0;
412        let event1_out = event1_out.as_object().unwrap();
413        let event2_out = encoder.encode_event(event2).unwrap().into_parts().0;
414        let event2_out = event2_out.as_object().unwrap();
415        let event3_out = encoder.encode_event(event3).unwrap().into_parts().0;
416        let event3_out = event3_out.as_object().unwrap();
417        let event4_out = encoder.encode_event(event4).unwrap().into_parts().0;
418        let event4_out = event4_out.as_object().unwrap();
419
420        assert_eq!(event1_out.get("app").unwrap(), &json!("notvector"));
421        assert_eq!(event2_out.get("file").unwrap(), &json!("log.txt"));
422        assert_eq!(event3_out.get("app").unwrap(), &json!("vector"));
423        assert_eq!(event3_out.get("env").unwrap(), &json!("acceptance"));
424        assert_eq!(event4_out.get("env").unwrap(), &json!("staging"));
425    }
426
427    async fn smoke_start(
428        status_code: StatusCode,
429        batch_status: BatchStatus,
430    ) -> (
431        Vec<&'static str>,
432        Vec<Vec<String>>,
433        mpsc::Receiver<(Parts, bytes::Bytes)>,
434    ) {
435        let (mut config, cx) = load_sink::<MezmoConfig>(
436            r#"
437            api_key = "mylogtoken"
438            ip = "127.0.0.1"
439            mac = "some-mac-addr"
440            hostname = "{{ hostname }}"
441            tags = ["test","maybeanothertest"]
442        "#,
443        )
444        .unwrap();
445
446        // Make sure we can build the config
447        _ = config.build(cx.clone()).await.unwrap();
448
449        let (_guard, addr) = next_addr();
450        // Swap out the host so we can force send it
451        // to our local server
452        let endpoint = UriSerde {
453            uri: format!("http://{addr}").parse::<http::Uri>().unwrap(),
454            auth: None,
455        };
456        config.endpoint = endpoint;
457
458        let (sink, _) = config.build(cx).await.unwrap();
459
460        let (rx, _trigger, server) = build_test_server_status(addr, status_code);
461        tokio::spawn(server);
462
463        let lines = random_lines(100).take(10).collect::<Vec<_>>();
464        let mut events = Vec::new();
465        let hosts = vec!["host0", "host1"];
466
467        let (batch, mut receiver) = BatchNotifier::new_with_receiver();
468        let mut partitions = vec![Vec::new(), Vec::new()];
469        // Create 10 events where the first one contains custom
470        // fields that are not just `message`.
471        for (i, line) in lines.iter().enumerate() {
472            let mut event = LogEvent::from(line.as_str()).with_batch_notifier(&batch);
473            let p = i % 2;
474            event.insert(event_path!("hostname"), hosts[p]);
475
476            partitions[p].push(line.into());
477            events.push(Event::Log(event));
478        }
479        drop(batch);
480
481        let events = stream::iter(events).map(Into::into);
482        sink.run(events).await.expect("Running sink failed");
483
484        assert_eq!(receiver.try_recv(), Ok(batch_status));
485
486        (hosts, partitions, rx)
487    }
488
489    #[tokio::test]
490    async fn smoke_fails() {
491        let (_hosts, _partitions, mut rx) =
492            smoke_start(StatusCode::FORBIDDEN, BatchStatus::Rejected).await;
493        assert!(rx.try_recv().is_err());
494    }
495
496    #[tokio::test]
497    async fn smoke() {
498        assert_sink_compliance(&HTTP_SINK_TAGS, async {
499            let (hosts, partitions, mut rx) =
500                smoke_start(StatusCode::OK, BatchStatus::Delivered).await;
501
502            for _ in 0..partitions.len() {
503                let output = rx.next().await.unwrap();
504
505                let request = &output.0;
506                let body: serde_json::Value = serde_json::from_slice(&output.1[..]).unwrap();
507
508                let query = request.uri.query().unwrap();
509
510                let (p, host) = hosts
511                    .iter()
512                    .enumerate()
513                    .find(|(_, host)| query.contains(&format!("hostname={host}")))
514                    .expect("invalid hostname");
515                let lines = &partitions[p];
516
517                assert!(query.contains("ip=127.0.0.1"));
518                assert!(query.contains("mac=some-mac-addr"));
519                assert!(query.contains("tags=test%2Cmaybeanothertest"));
520
521                let output = body
522                    .as_object()
523                    .unwrap()
524                    .get("lines")
525                    .unwrap()
526                    .as_array()
527                    .unwrap();
528
529                for (i, line) in output.iter().enumerate() {
530                    // All lines are json objects
531                    let line = line.as_object().unwrap();
532
533                    assert_eq!(line.get("app").unwrap(), &json!("vector"));
534                    assert_eq!(line.get("env").unwrap(), &json!("production"));
535                    assert_eq!(line.get("line").unwrap(), &json!(lines[i]));
536
537                    assert_eq!(
538                        line.get("meta").unwrap(),
539                        &json!({
540                            "hostname": host,
541                        })
542                    );
543                }
544            }
545        })
546        .await;
547    }
548}