saluki_components/encoders/datadog/service_checks/
mod.rs1use async_trait::async_trait;
2use http::{uri::PathAndQuery, HeaderValue, Method, Uri};
3use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
4use saluki_core::{
5 components::{encoders::*, ComponentContext},
6 data_model::{
7 event::{service_check::ServiceCheck, Event, EventType},
8 payload::{HttpPayload, Payload, PayloadMetadata, PayloadType},
9 },
10 observability::ComponentMetricsExt as _,
11 topology::PayloadsDispatcher,
12};
13use saluki_error::{ErrorContext as _, GenericError};
14use saluki_io::compression::CompressionScheme;
15use saluki_metrics::MetricsBuilder;
16use tracing::{debug, error, warn};
17
18use crate::common::datadog::{
19 clamp_payload_limits,
20 io::RB_BUFFER_CHUNK_SIZE,
21 request_builder::{EndpointEncoder, RequestBuilder},
22 telemetry::ComponentTelemetry,
23 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT, DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT,
24};
25
26const MAX_SERVICE_CHECKS_PER_PAYLOAD: usize = 100;
27
28static CONTENT_TYPE_JSON: HeaderValue = HeaderValue::from_static("application/json");
29
30#[derive(Debug)]
34pub struct DatadogServiceChecksConfiguration {
35 pub max_payload_size: usize,
44
45 pub max_uncompressed_payload_size: usize,
54
55 pub compressor_kind: String,
59
60 pub zstd_level: i32,
64
65 pub log_payloads: bool,
71}
72
73#[async_trait]
74impl IncrementalEncoderBuilder for DatadogServiceChecksConfiguration {
75 type Output = DatadogServiceChecks;
76
77 fn input_event_type(&self) -> EventType {
78 EventType::ServiceCheck
79 }
80
81 fn output_payload_type(&self) -> PayloadType {
82 PayloadType::Http
83 }
84
85 async fn build(&self, context: ComponentContext) -> Result<Self::Output, GenericError> {
86 let metrics_builder = MetricsBuilder::from_component_context(&context);
87 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
88 let compression_scheme = CompressionScheme::new(&self.compressor_kind, self.zstd_level);
89
90 let mut request_builder =
92 RequestBuilder::new(ServiceChecksEndpointEncoder, compression_scheme, RB_BUFFER_CHUNK_SIZE).await?;
93 let (uncompressed_limit, compressed_limit) = clamp_payload_limits(
94 self.max_uncompressed_payload_size,
95 self.max_payload_size,
96 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT,
97 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT,
98 );
99 request_builder.with_len_limits(uncompressed_limit, compressed_limit)?;
100 request_builder.with_max_inputs_per_payload(MAX_SERVICE_CHECKS_PER_PAYLOAD);
101
102 Ok(DatadogServiceChecks {
103 request_builder,
104 telemetry,
105 log_payloads: self.log_payloads,
106 })
107 }
108}
109
110impl MemoryBounds for DatadogServiceChecksConfiguration {
111 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
112 builder
120 .minimum()
121 .with_single_value::<DatadogServiceChecks>("component struct");
122
123 builder
124 .firm()
125 .with_array::<ServiceCheck>("service checks split re-encode buffer", MAX_SERVICE_CHECKS_PER_PAYLOAD);
128 }
129}
130
131pub struct DatadogServiceChecks {
132 request_builder: RequestBuilder<ServiceChecksEndpointEncoder>,
133 telemetry: ComponentTelemetry,
134 log_payloads: bool,
135}
136
137#[async_trait]
138impl IncrementalEncoder for DatadogServiceChecks {
139 async fn process_event(&mut self, event: Event) -> Result<ProcessResult, GenericError> {
140 let service_check = match event.try_into_service_check() {
141 Some(eventd) => eventd,
142 None => return Ok(ProcessResult::Continue),
143 };
144
145 if self.log_payloads {
146 debug!(?service_check, "Flushing service check.");
147 }
148
149 match self.request_builder.encode(service_check).await {
150 Ok(None) => Ok(ProcessResult::Continue),
151 Ok(Some(service_check)) => Ok(ProcessResult::FlushRequired(Event::ServiceCheck(service_check))),
152 Err(e) => {
153 if e.is_recoverable() {
154 warn!(error = %e, "Failed to encode Datadog service check due to recoverable error. Continuing...");
155
156 self.telemetry.events_dropped_encoder().increment(1);
158
159 Ok(ProcessResult::Continue)
160 } else {
161 Err(e).error_context("Failed to encode Datadog service check due to unrecoverable error.")
162 }
163 }
164 }
165 }
166
167 async fn flush(&mut self, dispatcher: &PayloadsDispatcher) -> Result<(), GenericError> {
168 let maybe_requests = self.request_builder.flush().await;
169 for maybe_request in maybe_requests {
170 match maybe_request {
171 Ok((events, _data_points, request)) => {
172 let payload_meta = PayloadMetadata::from_event_count(events);
173 let http_payload = HttpPayload::new(payload_meta, request);
174 let payload = Payload::Http(http_payload);
175
176 dispatcher.dispatch(payload).await?;
177 }
178 Err(e) => error!(error = %e, "Failed to build Datadog service checks payload. Continuing..."),
179 }
180 }
181
182 Ok(())
183 }
184}
185
186#[derive(Debug)]
187struct ServiceChecksEndpointEncoder;
188
189impl EndpointEncoder for ServiceChecksEndpointEncoder {
190 type Input = ServiceCheck;
191 type EncodeError = serde_json::Error;
192
193 fn encoder_name() -> &'static str {
194 "service_check"
195 }
196
197 fn compressed_size_limit(&self) -> usize {
198 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT
199 }
200
201 fn uncompressed_size_limit(&self) -> usize {
202 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT
203 }
204
205 fn encode(&mut self, input: &Self::Input, buffer: &mut Vec<u8>) -> Result<(), Self::EncodeError> {
206 serde_json::to_writer(buffer, input)
207 }
208
209 fn get_payload_prefix(&self) -> Option<&'static [u8]> {
210 Some(b"[")
211 }
212
213 fn get_payload_suffix(&self) -> Option<&'static [u8]> {
214 Some(b"]")
215 }
216
217 fn get_input_separator(&self) -> Option<&'static [u8]> {
218 Some(b",")
219 }
220
221 fn endpoint_uri(&self) -> Uri {
222 PathAndQuery::from_static("/api/v1/check_run").into()
223 }
224
225 fn endpoint_method(&self) -> Method {
226 Method::POST
227 }
228
229 fn content_type(&self) -> HeaderValue {
230 CONTENT_TYPE_JSON.clone()
231 }
232}