saluki_components/encoders/datadog/service_checks/
mod.rs1use async_trait::async_trait;
2use facet::Facet;
3use http::{uri::PathAndQuery, HeaderValue, Method, Uri};
4use saluki_config::GenericConfiguration;
5use saluki_core::accounting::{MemoryBounds, MemoryBoundsBuilder};
6use saluki_core::{
7 components::{encoders::*, ComponentContext},
8 data_model::{
9 event::{service_check::ServiceCheck, Event, EventType},
10 payload::{HttpPayload, Payload, PayloadMetadata, PayloadType},
11 },
12 observability::ComponentMetricsExt as _,
13 topology::PayloadsDispatcher,
14};
15use saluki_error::{ErrorContext as _, GenericError};
16use saluki_io::compression::CompressionScheme;
17use saluki_metrics::MetricsBuilder;
18use serde::Deserialize;
19use tracing::{debug, error, warn};
20
21use crate::common::datadog::{
22 clamp_payload_limits,
23 io::RB_BUFFER_CHUNK_SIZE,
24 request_builder::{EndpointEncoder, RequestBuilder},
25 resolve_zstd_compressor_level,
26 telemetry::ComponentTelemetry,
27 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT, DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT,
28};
29
30const DEFAULT_SERIALIZER_COMPRESSOR_KIND: &str = "zstd";
31const MAX_SERVICE_CHECKS_PER_PAYLOAD: usize = 100;
32
33static CONTENT_TYPE_JSON: HeaderValue = HeaderValue::from_static("application/json");
34
35fn default_serializer_compressor_kind() -> String {
36 DEFAULT_SERIALIZER_COMPRESSOR_KIND.to_owned()
37}
38
39const fn default_max_payload_size() -> usize {
40 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT
41}
42
43const fn default_max_uncompressed_payload_size() -> usize {
44 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT
45}
46
47const fn default_log_payloads() -> bool {
48 false
49}
50
51#[derive(Deserialize, Facet)]
55#[cfg_attr(test, derive(Debug, PartialEq, serde::Serialize))]
56pub struct DatadogServiceChecksConfiguration {
57 #[serde(rename = "serializer_max_payload_size", default = "default_max_payload_size")]
66 max_payload_size: usize,
67
68 #[serde(
77 rename = "serializer_max_uncompressed_payload_size",
78 default = "default_max_uncompressed_payload_size"
79 )]
80 max_uncompressed_payload_size: usize,
81
82 #[serde(
86 rename = "serializer_compressor_kind",
87 default = "default_serializer_compressor_kind"
88 )]
89 compressor_kind: String,
90
91 #[serde(rename = "data_plane_serializer_zstd_compressor_level", default)]
94 data_plane_zstd_compressor_level: Option<i32>,
95
96 #[serde(rename = "serializer_zstd_compressor_level", default)]
99 serializer_zstd_compressor_level: Option<i32>,
100
101 #[serde(default = "default_log_payloads")]
107 log_payloads: bool,
108}
109
110impl DatadogServiceChecksConfiguration {
111 pub fn from_configuration(config: &GenericConfiguration) -> Result<Self, GenericError> {
113 Ok(config.as_typed()?)
114 }
115}
116
117#[async_trait]
118impl IncrementalEncoderBuilder for DatadogServiceChecksConfiguration {
119 type Output = DatadogServiceChecks;
120
121 fn input_event_type(&self) -> EventType {
122 EventType::ServiceCheck
123 }
124
125 fn output_payload_type(&self) -> PayloadType {
126 PayloadType::Http
127 }
128
129 async fn build(&self, context: ComponentContext) -> Result<Self::Output, GenericError> {
130 let metrics_builder = MetricsBuilder::from_component_context(&context);
131 let telemetry = ComponentTelemetry::from_builder(&metrics_builder);
132 let zstd_compressor_level = resolve_zstd_compressor_level(
133 self.data_plane_zstd_compressor_level,
134 self.serializer_zstd_compressor_level,
135 );
136 let compression_scheme = CompressionScheme::new(&self.compressor_kind, zstd_compressor_level);
137
138 let mut request_builder =
140 RequestBuilder::new(ServiceChecksEndpointEncoder, compression_scheme, RB_BUFFER_CHUNK_SIZE).await?;
141 let (uncompressed_limit, compressed_limit) = clamp_payload_limits(
142 self.max_uncompressed_payload_size,
143 self.max_payload_size,
144 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT,
145 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT,
146 );
147 request_builder.with_len_limits(uncompressed_limit, compressed_limit)?;
148 request_builder.with_max_inputs_per_payload(MAX_SERVICE_CHECKS_PER_PAYLOAD);
149
150 Ok(DatadogServiceChecks {
151 request_builder,
152 telemetry,
153 log_payloads: self.log_payloads,
154 })
155 }
156}
157
158impl MemoryBounds for DatadogServiceChecksConfiguration {
159 fn specify_bounds(&self, builder: &mut MemoryBoundsBuilder) {
160 builder
168 .minimum()
169 .with_single_value::<DatadogServiceChecks>("component struct");
170
171 builder
172 .firm()
173 .with_array::<ServiceCheck>("service checks split re-encode buffer", MAX_SERVICE_CHECKS_PER_PAYLOAD);
176 }
177}
178
179pub struct DatadogServiceChecks {
180 request_builder: RequestBuilder<ServiceChecksEndpointEncoder>,
181 telemetry: ComponentTelemetry,
182 log_payloads: bool,
183}
184
185#[async_trait]
186impl IncrementalEncoder for DatadogServiceChecks {
187 async fn process_event(&mut self, event: Event) -> Result<ProcessResult, GenericError> {
188 let service_check = match event.try_into_service_check() {
189 Some(eventd) => eventd,
190 None => return Ok(ProcessResult::Continue),
191 };
192
193 if self.log_payloads {
194 debug!(?service_check, "Flushing service check.");
195 }
196
197 match self.request_builder.encode(service_check).await {
198 Ok(None) => Ok(ProcessResult::Continue),
199 Ok(Some(service_check)) => Ok(ProcessResult::FlushRequired(Event::ServiceCheck(service_check))),
200 Err(e) => {
201 if e.is_recoverable() {
202 warn!(error = %e, "Failed to encode Datadog service check due to recoverable error. Continuing...");
203
204 self.telemetry.events_dropped_encoder().increment(1);
206
207 Ok(ProcessResult::Continue)
208 } else {
209 Err(e).error_context("Failed to encode Datadog service check due to unrecoverable error.")
210 }
211 }
212 }
213 }
214
215 async fn flush(&mut self, dispatcher: &PayloadsDispatcher) -> Result<(), GenericError> {
216 let maybe_requests = self.request_builder.flush().await;
217 for maybe_request in maybe_requests {
218 match maybe_request {
219 Ok((events, _data_points, request)) => {
220 let payload_meta = PayloadMetadata::from_event_count(events);
221 let http_payload = HttpPayload::new(payload_meta, request);
222 let payload = Payload::Http(http_payload);
223
224 dispatcher.dispatch(payload).await?;
225 }
226 Err(e) => error!(error = %e, "Failed to build Datadog service checks payload. Continuing..."),
227 }
228 }
229
230 Ok(())
231 }
232}
233
234#[derive(Debug)]
235struct ServiceChecksEndpointEncoder;
236
237impl EndpointEncoder for ServiceChecksEndpointEncoder {
238 type Input = ServiceCheck;
239 type EncodeError = serde_json::Error;
240
241 fn encoder_name() -> &'static str {
242 "service_check"
243 }
244
245 fn compressed_size_limit(&self) -> usize {
246 DEFAULT_SERIALIZER_COMPRESSED_SIZE_LIMIT
247 }
248
249 fn uncompressed_size_limit(&self) -> usize {
250 DEFAULT_SERIALIZER_UNCOMPRESSED_SIZE_LIMIT
251 }
252
253 fn encode(&mut self, input: &Self::Input, buffer: &mut Vec<u8>) -> Result<(), Self::EncodeError> {
254 serde_json::to_writer(buffer, input)
255 }
256
257 fn get_payload_prefix(&self) -> Option<&'static [u8]> {
258 Some(b"[")
259 }
260
261 fn get_payload_suffix(&self) -> Option<&'static [u8]> {
262 Some(b"]")
263 }
264
265 fn get_input_separator(&self) -> Option<&'static [u8]> {
266 Some(b",")
267 }
268
269 fn endpoint_uri(&self) -> Uri {
270 PathAndQuery::from_static("/api/v1/check_run").into()
271 }
272
273 fn endpoint_method(&self) -> Method {
274 Method::POST
275 }
276
277 fn content_type(&self) -> HeaderValue {
278 CONTENT_TYPE_JSON.clone()
279 }
280}
281
282#[cfg(test)]
283mod config_smoke {
284 use datadog_agent_config_testing::config_registry::structs;
285 use datadog_agent_config_testing::run_config_smoke_tests;
286 use serde_json::json;
287
288 use super::DatadogServiceChecksConfiguration;
289 use crate::config::{DatadogRemapper, KEY_ALIASES};
290
291 #[tokio::test]
292 async fn smoke_test() {
293 run_config_smoke_tests(
294 structs::DATADOG_SERVICE_CHECKS_CONFIGURATION,
295 &[],
296 json!({}),
297 |cfg| {
298 cfg.as_typed::<DatadogServiceChecksConfiguration>()
299 .expect("DatadogServiceChecksConfiguration should deserialize")
300 },
301 KEY_ALIASES,
302 DatadogRemapper::from_env_vars,
303 )
304 .await
305 }
306}