1use std::sync::LazyLock;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use datadog_protos::checks::{
6 check_data::Data,
7 checks_server::{Checks, ChecksServer},
8 event::{AlertType as ProtoAlertType, Event as ProtoEvent, Priority as ProtoPriority},
9 log::{Log as ProtoLog, LogLevel},
10 metric::{Metric as ProtoMetric, MetricType},
11 service_check::{ServiceCheck as ProtoServiceCheck, Status as ServiceCheckStatus},
12 SendCheckPayloadRequest, SendCheckPayloadResponse,
13};
14use saluki_core::{
15 accounting::{MemoryBounds, MemoryBoundsBuilder},
16 components::{sources::*, BuildContext},
17 data_model::{
18 event::{
19 eventd::{AlertType, EventD, Priority},
20 log::{Log, LogStatus},
21 metric::{context::Context, Metric},
22 service_check::{CheckStatus, ServiceCheck},
23 Event, EventType,
24 },
25 tags::{Tag, TagSet},
26 },
27 runtime,
28 topology::OutputDefinition,
29};
30use saluki_error::{generic_error, GenericError};
31use saluki_io::net::{
32 server::http::{Http2Config, HttpServer},
33 ListenAddress,
34};
35use stringtheory::MetaString;
36use tokio::select;
37use tokio::sync::mpsc;
38use tonic::{Response, Status};
39use tracing::{debug, trace, warn};
40
41#[derive(Debug)]
43pub struct ChecksIPCConfiguration {
44 default_hostname: MetaString,
45 grpc_endpoint: ListenAddress,
46}
47
48impl ChecksIPCConfiguration {
49 pub fn new(grpc_endpoint: ListenAddress, default_hostname: impl Into<MetaString>) -> Self {
51 Self {
52 default_hostname: default_hostname.into(),
53 grpc_endpoint,
54 }
55 }
56}
57
58#[async_trait]
59impl SourceBuilder for ChecksIPCConfiguration {
60 fn outputs(&self) -> &[OutputDefinition<EventType>] {
61 static OUTPUTS: LazyLock<Vec<OutputDefinition<EventType>>> = LazyLock::new(|| {
62 vec![
63 OutputDefinition::named_output("metrics", EventType::Metric),
64 OutputDefinition::named_output("logs", EventType::Log),
65 OutputDefinition::named_output("events", EventType::EventD),
66 OutputDefinition::named_output("service_checks", EventType::ServiceCheck),
67 ]
68 });
69
70 &OUTPUTS
71 }
72
73 async fn build(&self, _context: BuildContext) -> Result<Box<dyn Source + Send>, GenericError> {
74 Ok(Box::new(ChecksIPC {
75 grpc_endpoint: self.grpc_endpoint.clone(),
76 default_hostname: self.default_hostname.clone(),
77 }))
78 }
79}
80
81impl MemoryBounds for ChecksIPCConfiguration {
82 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
83 builder.minimum().with_single_value::<ChecksIPC>("checks_ipc");
85 }
86}
87
88struct ChecksIPC {
89 grpc_endpoint: ListenAddress,
90 default_hostname: MetaString,
91}
92
93#[async_trait]
94impl Source for ChecksIPC {
95 async fn run(self: Box<Self>, mut context: SourceContext) -> Result<(), GenericError> {
96 let ChecksIPC {
97 grpc_endpoint,
98 default_hostname,
99 } = *self;
100
101 let mut health = context.take_health_handle();
102
103 let (events_tx, mut events_rx) = mpsc::channel(16);
104
105 let ListenAddress::Tcp(grpc_socket_addr) = grpc_endpoint else {
106 return Err(generic_error!("Checks IPC gRPC endpoint must be a TCP address."));
107 };
108
109 let grpc_server = HttpServer::from_listen_address(ListenAddress::Tcp(grpc_socket_addr))
112 .add_grpc_service(ChecksServer::new(ChecksService {
113 events_tx,
114 default_hostname,
115 }))
116 .with_http2_only()
117 .with_http2_config(Http2Config::grpc_defaults())
118 .with_worker_pool(context.topology_context().global_thread_pool().clone());
119
120 runtime::nested_supervisor(grpc_server.into_supervisor())
123 .needed()
124 .stops_with_owner()
125 .spawn();
126
127 health.mark_ready();
128 debug!("Checks IPC source started.");
129
130 loop {
131 select! {
132 _ = health.live() => continue,
133 maybe_event = events_rx.recv() => {
134 let Some(event) = maybe_event else {
135 break;
136 };
137
138 let output_name = match &event {
139 Event::Metric(_) => "metrics",
140 Event::Log(_) => "logs",
141 Event::EventD(_) => "events",
142 Event::ServiceCheck(_) => "service_checks",
143 _ => continue,
144 };
145
146 if let Err(e) = context.dispatcher().dispatch_one_named(output_name, event).await {
147 warn!("Failed to dispatch {output_name} event: {:?}", e);
148 }
149 },
150 }
151 }
152
153 debug!("Checks IPC source stopped.");
154 Ok(())
155 }
156}
157
158struct ChecksService {
159 events_tx: mpsc::Sender<Event>,
160 default_hostname: MetaString,
161}
162
163#[async_trait]
164impl Checks for ChecksService {
165 async fn send_check_payload(
166 &self, request: tonic::Request<SendCheckPayloadRequest>,
167 ) -> Result<Response<SendCheckPayloadResponse>, Status> {
168 trace!("Received check payload.");
169
170 let payload = request.into_inner();
171 for check_data in payload.data.into_iter().filter_map(|data| data.data) {
172 let Some(event) = check_data_to_event(check_data, &self.default_hostname) else {
173 continue;
174 };
175
176 if let Err(e) = self.events_tx.send(event).await {
177 warn!("Failed to send check event: {:?}", e);
178 }
179 }
180
181 Ok(Response::new(SendCheckPayloadResponse {}))
182 }
183}
184
185fn check_data_to_event(check_data: Data, default_hostname: &MetaString) -> Option<Event> {
186 match check_data {
189 Data::Metric(metric) => {
190 let ProtoMetric {
191 r#type,
192 name,
193 value,
194 timestamp,
195 tags,
196 hostname,
197 interval_secs,
198 } = metric;
199
200 let metric_type = MetricType::try_from(r#type).ok()?;
201
202 let tags = tags.into_iter().map(Tag::from).collect::<TagSet>();
203 let mut context = Context::from_parts(name, tags.into_shared());
204 let hostname = if hostname.is_empty() {
205 default_hostname.clone()
206 } else {
207 MetaString::from(hostname)
208 };
209 context = context.with_host(Some(hostname));
210 let metric = match metric_type {
211 MetricType::Counter => Metric::counter(context, (timestamp, value)),
212 MetricType::Gauge => Metric::gauge(context, (timestamp, value)),
213 MetricType::Rate => {
214 if interval_secs == 0 {
215 warn!("Received rate metric from check with interval of zero. Skipping.");
216 return None;
217 }
218 Metric::rate(context, (timestamp, value), Duration::from_secs(interval_secs))
219 }
220 MetricType::Histogram => Metric::histogram(context, (timestamp, value)),
221 MetricType::Unspecified => {
222 warn!("Received metric with unspecified type. Skipping.");
223 return None;
224 }
225 };
226 Some(Event::Metric(metric))
227 }
228 Data::Log(log) => {
229 let ProtoLog { message, level } = log;
230
231 let level = LogLevel::try_from(level).ok()?;
232 let status = log_level_to_log_status(level);
233
234 Some(Event::Log(Log::new(message).with_status(status)))
235 }
236 Data::Event(event) => {
237 let ProtoEvent {
238 title,
239 text,
240 priority,
241 hostname,
242 tags,
243 alert_type,
244 aggregation_key,
245 source_type_name,
246 timestamp,
247 } = event;
248
249 let tags = tags.into_iter().map(Tag::from).collect::<TagSet>();
250 let mut eventd = EventD::new(title, text)
251 .with_timestamp(timestamp)
252 .with_tags(tags.into_shared());
253
254 if !hostname.is_empty() {
255 eventd.set_hostname(MetaString::from(hostname));
256 }
257 if !aggregation_key.is_empty() {
258 eventd.set_aggregation_key(MetaString::from(aggregation_key));
259 }
260 if !source_type_name.is_empty() {
261 eventd.set_source_type_name(MetaString::from(source_type_name));
262 }
263 if let Some(p) = ProtoPriority::try_from(priority)
264 .ok()
265 .and_then(proto_priority_to_priority)
266 {
267 eventd.set_priority(p);
268 }
269 if let Some(a) = ProtoAlertType::try_from(alert_type)
270 .ok()
271 .and_then(proto_alert_type_to_alert_type)
272 {
273 eventd.set_alert_type(a);
274 }
275 Some(Event::EventD(eventd))
276 }
277 Data::ServiceCheck(sc) => {
278 let ProtoServiceCheck {
279 status,
280 name,
281 message,
282 tags,
283 hostname,
284 } = sc;
285
286 let Some(status) = ServiceCheckStatus::try_from(status)
287 .ok()
288 .and_then(service_check_status_to_check_status)
289 else {
290 warn!(
291 "Received service check with unspecified or invalid status: {}. Skipping.",
292 status
293 );
294 return None;
295 };
296 let tags = tags.into_iter().map(Tag::from).collect::<TagSet>();
297 let mut service_check = ServiceCheck::new(name, status)
298 .with_message(MetaString::from(message))
299 .with_tags(tags.into_shared());
300 if !hostname.is_empty() {
301 service_check.set_hostname(MetaString::from(hostname));
302 }
303 Some(Event::ServiceCheck(service_check))
304 }
305 }
306}
307
308fn log_level_to_log_status(log_level: LogLevel) -> LogStatus {
309 match log_level {
310 LogLevel::Trace => LogStatus::Trace,
311 LogLevel::Debug => LogStatus::Debug,
312 LogLevel::Info => LogStatus::Info,
313 LogLevel::Warning => LogStatus::Warning,
314 LogLevel::Error => LogStatus::Error,
315 LogLevel::Critical => LogStatus::Emergency,
316 _ => LogStatus::Info,
317 }
318}
319
320fn service_check_status_to_check_status(status: ServiceCheckStatus) -> Option<CheckStatus> {
321 match status {
322 ServiceCheckStatus::Ok => Some(CheckStatus::Ok),
323 ServiceCheckStatus::Warning => Some(CheckStatus::Warning),
324 ServiceCheckStatus::Critical => Some(CheckStatus::Critical),
325 ServiceCheckStatus::Unknown => Some(CheckStatus::Unknown),
326 ServiceCheckStatus::Unspecified => None,
327 }
328}
329
330fn proto_priority_to_priority(priority: ProtoPriority) -> Option<Priority> {
331 match priority {
332 ProtoPriority::Normal => Some(Priority::Normal),
333 ProtoPriority::Low => Some(Priority::Low),
334 ProtoPriority::Unspecified => None,
335 }
336}
337
338fn proto_alert_type_to_alert_type(alert_type: ProtoAlertType) -> Option<AlertType> {
339 match alert_type {
340 ProtoAlertType::Info => Some(AlertType::Info),
341 ProtoAlertType::Error => Some(AlertType::Error),
342 ProtoAlertType::Warning => Some(AlertType::Warning),
343 ProtoAlertType::Success => Some(AlertType::Success),
344 ProtoAlertType::Unspecified => None,
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use std::num::NonZeroUsize;
351
352 use datadog_protos::checks::{
353 check_data::Data,
354 checks_client::ChecksClient,
355 event::Event as ProtoEvent,
356 log::Log as ProtoLog,
357 metric::{Metric as ProtoMetric, MetricType as ProtoMetricType},
358 service_check::{ServiceCheck as ProtoServiceCheck, Status as ProtoServiceCheckStatus},
359 CheckData,
360 };
361 use saluki_core::components::test_util::TestComponentDriver;
362 use saluki_core::data_model::event::metric::MetricValues;
363
364 use super::*;
365
366 fn metric_data(
367 r#type: i32, name: &str, value: f64, timestamp: u64, interval_secs: u64, tags: &[&str], hostname: &str,
368 ) -> Data {
369 Data::Metric(ProtoMetric {
370 r#type,
371 name: name.to_string(),
372 value,
373 timestamp,
374 tags: tags.iter().map(|t| (*t).to_string()).collect(),
375 hostname: hostname.to_string(),
376 interval_secs,
377 })
378 }
379
380 fn log_data(level: i32, message: &str) -> Data {
381 Data::Log(ProtoLog {
382 message: message.to_string(),
383 level,
384 })
385 }
386
387 fn event_data(title: &str, text: &str, timestamp: u64, tags: &[&str], hostname: &str) -> Data {
388 Data::Event(ProtoEvent {
389 title: title.to_string(),
390 text: text.to_string(),
391 priority: 0,
392 hostname: hostname.to_string(),
393 tags: tags.iter().map(|t| (*t).to_string()).collect(),
394 alert_type: 0,
395 aggregation_key: String::new(),
396 source_type_name: String::new(),
397 timestamp,
398 })
399 }
400
401 fn service_check_data(status: i32, name: &str, message: &str, tags: &[&str], hostname: &str) -> Data {
402 Data::ServiceCheck(ProtoServiceCheck {
403 status,
404 name: name.to_string(),
405 message: message.to_string(),
406 tags: tags.iter().map(|t| (*t).to_string()).collect(),
407 hostname: hostname.to_string(),
408 })
409 }
410
411 fn check_data_to_event_for_tests(check_data: Data) -> Option<Event> {
412 check_data_to_event(check_data, &MetaString::from_static("default-host"))
413 }
414
415 #[test]
416 fn metric_counter_conversion() {
417 let event = check_data_to_event_for_tests(metric_data(
418 ProtoMetricType::Counter as i32,
419 "my_counter",
420 1.0,
421 1234,
422 0,
423 &["tag1:value1", "tag2:value2"],
424 "",
425 ))
426 .expect("counter should convert");
427
428 let Event::Metric(metric) = event else {
429 panic!("expected Metric event");
430 };
431 assert_eq!(metric.context().name().as_ref(), "my_counter");
432 assert!(metric.context().tags().has_tag("tag1:value1"));
433 assert!(metric.context().tags().has_tag("tag2:value2"));
434 assert!(matches!(metric.values(), MetricValues::Counter(_)));
435 }
436
437 #[test]
438 fn metric_gauge_conversion() {
439 let event = check_data_to_event_for_tests(metric_data(
440 ProtoMetricType::Gauge as i32,
441 "my_gauge",
442 42.0,
443 1234,
444 0,
445 &[],
446 "",
447 ))
448 .expect("gauge should convert");
449 let Event::Metric(metric) = event else {
450 panic!("expected Metric event");
451 };
452 assert!(matches!(metric.values(), MetricValues::Gauge(_)));
453 }
454
455 #[test]
456 fn metric_histogram_conversion() {
457 let event = check_data_to_event_for_tests(metric_data(
458 ProtoMetricType::Histogram as i32,
459 "my_hist",
460 1.0,
461 1234,
462 0,
463 &[],
464 "",
465 ))
466 .expect("histogram should convert");
467 let Event::Metric(metric) = event else {
468 panic!("expected Metric event");
469 };
470 assert!(matches!(metric.values(), MetricValues::Histogram(_)));
471 }
472
473 #[test]
474 fn metric_rate_conversion_uses_interval() {
475 let event = check_data_to_event_for_tests(metric_data(
476 ProtoMetricType::Rate as i32,
477 "my_rate",
478 10.0,
479 1234,
480 60,
481 &[],
482 "",
483 ))
484 .expect("rate should convert");
485 let Event::Metric(metric) = event else {
486 panic!("expected Metric event");
487 };
488 match metric.values() {
489 MetricValues::Rate(_, interval) => assert_eq!(*interval, Duration::from_secs(60)),
490 other => panic!("expected Rate values, got {other:?}"),
491 }
492 }
493
494 #[test]
495 fn metric_rate_with_zero_interval_is_skipped() {
496 let event = check_data_to_event_for_tests(metric_data(
497 ProtoMetricType::Rate as i32,
498 "my_rate",
499 10.0,
500 1234,
501 0,
502 &[],
503 "",
504 ));
505 assert!(event.is_none(), "rate with zero interval must be skipped");
506 }
507
508 #[test]
509 fn metric_unspecified_type_is_skipped() {
510 let event = check_data_to_event_for_tests(metric_data(
511 ProtoMetricType::Unspecified as i32,
512 "x",
513 1.0,
514 1234,
515 0,
516 &[],
517 "",
518 ));
519 assert!(event.is_none(), "unspecified metric type must be skipped");
520 }
521
522 #[test]
523 fn metric_unknown_type_is_skipped() {
524 let event = check_data_to_event_for_tests(metric_data(99, "x", 1.0, 1234, 0, &[], ""));
526 assert!(event.is_none(), "unknown metric type must be skipped");
527 }
528
529 #[test]
530 fn log_unknown_level_is_skipped() {
531 let event = check_data_to_event_for_tests(log_data(99, "hello"));
533 assert!(event.is_none(), "unknown log level must be skipped");
534 }
535
536 #[test]
537 fn event_conversion_preserves_fields() {
538 let event = check_data_to_event_for_tests(event_data("title", "body", 1234, &["env:prod", "team:foo"], ""))
539 .expect("event should convert");
540 let Event::EventD(ev) = event else {
541 panic!("expected EventD event");
542 };
543 assert_eq!(ev.title(), "title");
544 assert_eq!(ev.text(), "body");
545 assert_eq!(ev.timestamp(), Some(1234));
546 assert!(ev.tags().has_tag("env:prod"));
547 assert!(ev.tags().has_tag("team:foo"));
548 }
549
550 #[test]
551 fn service_check_status_mapping() {
552 let cases = [
553 (ProtoServiceCheckStatus::Ok, CheckStatus::Ok),
554 (ProtoServiceCheckStatus::Warning, CheckStatus::Warning),
555 (ProtoServiceCheckStatus::Critical, CheckStatus::Critical),
556 (ProtoServiceCheckStatus::Unknown, CheckStatus::Unknown),
557 ];
558
559 for (proto_status, expected) in cases {
560 let event = check_data_to_event_for_tests(service_check_data(proto_status as i32, "n", "m", &[], ""))
561 .unwrap_or_else(|| panic!("status {proto_status:?} should convert"));
562 let Event::ServiceCheck(sc) = event else {
563 panic!("expected ServiceCheck event for {proto_status:?}");
564 };
565 assert_eq!(sc.status(), expected, "status {proto_status:?}");
566 }
567 }
568
569 #[test]
570 fn service_check_unspecified_status_is_skipped() {
571 let event = check_data_to_event_for_tests(service_check_data(
572 ProtoServiceCheckStatus::Unspecified as i32,
573 "n",
574 "m",
575 &[],
576 "",
577 ));
578 assert!(event.is_none(), "service check with unspecified status must be skipped");
579 }
580
581 #[test]
582 fn service_check_unknown_status_value_is_skipped() {
583 let event = check_data_to_event_for_tests(service_check_data(99, "n", "m", &[], ""));
585 assert!(
586 event.is_none(),
587 "service check with out-of-range status must be skipped"
588 );
589 }
590
591 #[test]
592 fn service_check_preserves_name_message_and_tags() {
593 let event = check_data_to_event_for_tests(service_check_data(
594 ProtoServiceCheckStatus::Ok as i32,
595 "my.check",
596 "all good",
597 &["env:prod"],
598 "",
599 ))
600 .expect("service check should convert");
601 let Event::ServiceCheck(sc) = event else {
602 panic!("expected ServiceCheck event");
603 };
604 assert_eq!(sc.name(), "my.check");
605 assert_eq!(sc.status(), CheckStatus::Ok);
606 assert_eq!(sc.message(), Some("all good"));
607 assert!(sc.tags().has_tag("env:prod"));
608 }
609
610 #[test]
611 fn metric_hostname_propagates() {
612 let event = check_data_to_event_for_tests(metric_data(
613 ProtoMetricType::Counter as i32,
614 "n",
615 1.0,
616 0,
617 0,
618 &[],
619 "host-a",
620 ))
621 .expect("metric should convert");
622 let Event::Metric(m) = event else {
623 panic!("expected Metric event");
624 };
625 assert_eq!(m.context().host(), Some("host-a"));
626 }
627
628 #[test]
629 fn metric_empty_hostname_uses_default_host() {
630 let event =
631 check_data_to_event_for_tests(metric_data(ProtoMetricType::Counter as i32, "n", 1.0, 0, 0, &[], ""))
632 .expect("metric should convert");
633 let Event::Metric(m) = event else {
634 panic!("expected Metric event");
635 };
636 assert_eq!(m.context().host(), Some("default-host"));
637 }
638
639 #[test]
640 fn eventd_hostname_propagates() {
641 let event =
642 check_data_to_event_for_tests(event_data("title", "body", 0, &[], "host-b")).expect("event should convert");
643 let Event::EventD(ev) = event else {
644 panic!("expected EventD event");
645 };
646 assert_eq!(ev.hostname(), Some("host-b"));
647 }
648
649 #[test]
650 fn eventd_empty_hostname_stays_unset() {
651 let event =
652 check_data_to_event_for_tests(event_data("title", "body", 0, &[], "")).expect("event should convert");
653 let Event::EventD(ev) = event else {
654 panic!("expected EventD event");
655 };
656 assert_eq!(ev.hostname(), None);
657 }
658
659 #[test]
660 fn service_check_hostname_propagates() {
661 let event = check_data_to_event_for_tests(service_check_data(
662 ProtoServiceCheckStatus::Ok as i32,
663 "n",
664 "m",
665 &[],
666 "host-c",
667 ))
668 .expect("service check should convert");
669 let Event::ServiceCheck(sc) = event else {
670 panic!("expected ServiceCheck event");
671 };
672 assert_eq!(sc.hostname(), Some("host-c"));
673 }
674
675 #[test]
676 fn service_check_empty_hostname_stays_unset() {
677 let event = check_data_to_event_for_tests(service_check_data(
678 ProtoServiceCheckStatus::Ok as i32,
679 "n",
680 "m",
681 &[],
682 "",
683 ))
684 .expect("service check should convert");
685 let Event::ServiceCheck(sc) = event else {
686 panic!("expected ServiceCheck event");
687 };
688 assert_eq!(sc.hostname(), None);
689 }
690
691 #[test]
692 fn eventd_priority_propagates() {
693 let event = check_data_to_event_for_tests(Data::Event(ProtoEvent {
694 priority: ProtoPriority::Low as i32,
695 ..Default::default()
696 }))
697 .expect("event should convert");
698 let Event::EventD(ev) = event else {
699 panic!("expected EventD event");
700 };
701 assert_eq!(ev.priority(), Some(Priority::Low));
702 }
703
704 #[test]
705 fn eventd_alert_type_propagates() {
706 let event = check_data_to_event_for_tests(Data::Event(ProtoEvent {
707 alert_type: ProtoAlertType::Warning as i32,
708 ..Default::default()
709 }))
710 .expect("event should convert");
711 let Event::EventD(ev) = event else {
712 panic!("expected EventD event");
713 };
714 assert_eq!(ev.alert_type(), Some(AlertType::Warning));
715 }
716
717 #[test]
718 fn eventd_aggregation_key_propagates() {
719 let event = check_data_to_event_for_tests(Data::Event(ProtoEvent {
720 aggregation_key: "agg-key-1".to_string(),
721 ..Default::default()
722 }))
723 .expect("event should convert");
724 let Event::EventD(ev) = event else {
725 panic!("expected EventD event");
726 };
727 assert_eq!(ev.aggregation_key(), Some("agg-key-1"));
728 }
729
730 #[test]
731 fn eventd_source_type_name_propagates() {
732 let event = check_data_to_event_for_tests(Data::Event(ProtoEvent {
733 source_type_name: "my-source".to_string(),
734 ..Default::default()
735 }))
736 .expect("event should convert");
737 let Event::EventD(ev) = event else {
738 panic!("expected EventD event");
739 };
740 assert_eq!(ev.source_type_name(), Some("my-source"));
741 }
742
743 #[test]
744 fn eventd_unspecified_proto_keeps_saluki_defaults() {
745 let event = check_data_to_event_for_tests(Data::Event(ProtoEvent::default())).expect("event should convert");
750 let Event::EventD(ev) = event else {
751 panic!("expected EventD event");
752 };
753 assert_eq!(ev.priority(), Some(Priority::Normal));
754 assert_eq!(ev.alert_type(), Some(AlertType::Info));
755 assert_eq!(ev.aggregation_key(), None);
756 assert_eq!(ev.source_type_name(), None);
757 assert_eq!(ev.hostname(), None);
758 }
759
760 #[tokio::test]
761 async fn accepted_events_are_dispatched_before_the_source_stops() {
762 const METRICS: usize = 64;
764
765 let address = std::net::TcpListener::bind("127.0.0.1:0")
766 .and_then(|listener| listener.local_addr())
767 .expect("should reserve an ephemeral port");
768
769 let (mut control, mut outputs) = TestComponentDriver::options()
771 .with_interconnect_capacity(NonZeroUsize::MIN)
772 .source(ChecksIPCConfiguration::new(ListenAddress::Tcp(address), "test-host"))
773 .await
774 .expect("source should build");
775 let mut metrics = outputs.take_named_output("metrics");
776
777 let endpoint =
778 tonic::transport::Endpoint::from_shared(format!("http://{address}")).expect("endpoint should parse");
779 let channel = tokio::time::timeout(Duration::from_secs(5), async {
780 loop {
781 match endpoint.connect().await {
782 Ok(channel) => return channel,
783 Err(_) => tokio::time::sleep(Duration::from_millis(10)).await,
784 }
785 }
786 })
787 .await
788 .expect("the gRPC server should start listening");
789
790 let request = SendCheckPayloadRequest {
791 data: (0..METRICS)
792 .map(|i| CheckData {
793 data: Some(metric_data(
794 ProtoMetricType::Gauge as i32,
795 &format!("metric_{i}"),
796 1.0,
797 0,
798 0,
799 &[],
800 "",
801 )),
802 })
803 .collect(),
804 };
805 let response = tokio::spawn(async move { ChecksClient::new(channel).send_check_payload(request).await });
806
807 let first = metrics.next().await.expect("the first metric should arrive");
808 control.signal_shutdown();
809 let (result, rest) = tokio::join!(control.wait(), metrics.collect_events());
810 result.expect("source should stop cleanly");
811 response
812 .await
813 .expect("request task should not panic")
814 .expect("request should succeed");
815 assert_eq!(
816 first.len() + rest.len(),
817 METRICS,
818 "every accepted metric must be dispatched"
819 );
820 }
821}