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#[configurable_component(sink("mezmo", "Deliver log event data to Mezmo."))]
31#[derive(Clone, Debug)]
32pub struct MezmoConfig {
33 #[configurable(metadata(docs::examples = "${LOGDNA_API_KEY}"))]
35 #[configurable(metadata(docs::examples = "ef8d5de700e7989468166c40fc8a0ccd"))]
36 api_key: SensitiveString,
37
38 #[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 #[configurable(metadata(docs::examples = "${HOSTNAME}"))]
49 #[configurable(metadata(docs::examples = "my-local-machine"))]
50 hostname: UnconfinedTemplate,
51
52 #[configurable(metadata(docs::examples = "my-mac-address"))]
54 #[configurable(metadata(docs::human_name = "MAC Address"))]
55 mac: Option<String>,
56
57 #[configurable(metadata(docs::examples = "0.0.0.0"))]
59 #[configurable(metadata(docs::human_name = "IP Address"))]
60 ip: Option<String>,
61
62 #[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 #[serde(default = "default_app")]
73 #[configurable(metadata(docs::examples = "my-app"))]
74 default_app: String,
75
76 #[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 _ = config.build(cx.clone()).await.unwrap();
448
449 let (_guard, addr) = next_addr();
450 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 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 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}