Skip to content

Commit 86b6312

Browse files
committed
enhancement(antithesis): accept Datadog intake endpoints
1 parent f1148f7 commit 86b6312

6 files changed

Lines changed: 227 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/antithesis/intake/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ datadog-protos = { workspace = true }
2222
headers = { workspace = true }
2323
mime = { workspace = true }
2424
protobuf = { workspace = true }
25+
serde = { workspace = true, features = ["derive"] }
2526
serde_json = { workspace = true }
2627
tokio = { workspace = true, features = [
2728
"macros",
Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,92 @@
1-
//! Datadog-compatible intake routes.
1+
//! Datadog-compatible HTTP intake routes.
2+
//!
3+
//! This module owns the public routes that Datadog Agent and ADP send to:
4+
//!
5+
//! - `POST /api/v2/series`: accepts metric series payloads and records payload
6+
//! shape assertions.
7+
//! - `POST /api/beta/sketches`: accepts distribution sketch payloads.
8+
//! - `POST /api/v1/events_batch`: accepts protobuf event batches.
9+
//! - `POST /api/v1/events`: accepts JSON event intake payloads and rejects
10+
//! malformed bodies.
11+
//! - `POST /intake/`: accepts the shared JSON intake endpoint and ignores
12+
//! non-event bodies.
13+
//! - `POST /api/v1/check_run`: accepts service check payloads.
14+
//! - `GET /api/v1/validate`: accepts Datadog Agent connectivity validation.
215
3-
use axum::{extract::DefaultBodyLimit, middleware::from_fn, routing::post, Router};
16+
use axum::{
17+
extract::DefaultBodyLimit,
18+
http::StatusCode,
19+
middleware::from_fn,
20+
routing::{get, post},
21+
Router,
22+
};
423
use tower::ServiceBuilder;
524
use tower_http::decompression::RequestDecompressionLayer;
625

7-
use self::metrics::handle_series;
826
use super::middleware::measure_compressed_size;
927
use super::state::AppState;
1028

29+
mod events;
1130
mod metrics;
31+
mod service_checks;
1232

1333
/// Build Datadog-compatible intake routes.
1434
pub(crate) fn routes() -> Router<AppState> {
35+
Router::new()
36+
.merge(metric_routes())
37+
.merge(event_routes())
38+
.merge(service_check_routes())
39+
.route("/api/v1/validate", get(|| async { StatusCode::OK }))
40+
}
41+
42+
fn metric_routes() -> Router<AppState> {
1543
// Pyld01-Pyld06 and Pyld22 need the compressed body and raw headers, so the series
1644
// route runs `measure_compressed_size` outermost, then decompresses, then
1745
// lifts the body limit (the middleware's own cap is the backstop).
18-
let series = post(handle_series).layer(
46+
let series = post(metrics::handle_series).layer(
1947
ServiceBuilder::new()
2048
.layer(from_fn(measure_compressed_size))
2149
.layer(RequestDecompressionLayer::new().pass_through_unaccepted(true))
2250
.layer(DefaultBodyLimit::disable()),
2351
);
2452

25-
Router::new().route("/api/v2/series", series)
53+
let decoded_payload_route = ServiceBuilder::new()
54+
.layer(RequestDecompressionLayer::new().pass_through_unaccepted(true))
55+
.layer(DefaultBodyLimit::disable());
56+
57+
Router::new().route("/api/v2/series", series).route(
58+
"/api/beta/sketches",
59+
post(metrics::handle_sketches).layer(decoded_payload_route),
60+
)
61+
}
62+
63+
fn event_routes() -> Router<AppState> {
64+
let decoded_payload_route = ServiceBuilder::new()
65+
.layer(RequestDecompressionLayer::new().pass_through_unaccepted(true))
66+
.layer(DefaultBodyLimit::disable());
67+
68+
Router::new()
69+
.route(
70+
"/api/v1/events_batch",
71+
post(events::handle_events_batch).layer(decoded_payload_route.clone()),
72+
)
73+
.route(
74+
"/api/v1/events",
75+
post(events::handle_events_v1).layer(decoded_payload_route.clone()),
76+
)
77+
.route(
78+
"/intake/",
79+
post(events::handle_intake).layer(decoded_payload_route.clone()),
80+
)
81+
}
82+
83+
fn service_check_routes() -> Router<AppState> {
84+
let decoded_payload_route = ServiceBuilder::new()
85+
.layer(RequestDecompressionLayer::new().pass_through_unaccepted(true))
86+
.layer(DefaultBodyLimit::disable());
87+
88+
Router::new().route(
89+
"/api/v1/check_run",
90+
post(service_checks::handle_check_run_v1).layer(decoded_payload_route),
91+
)
2692
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Event intake handlers.
2+
3+
use std::collections::HashMap;
4+
5+
use axum::{body::Bytes, http::StatusCode};
6+
use datadog_protos::events::EventsPayload;
7+
use protobuf::Message;
8+
use serde::Deserialize;
9+
use tracing::{debug, error};
10+
11+
/// Handler for `POST /api/v1/events_batch`.
12+
pub(crate) async fn handle_events_batch(body: Bytes) -> StatusCode {
13+
match EventsPayload::parse_from_bytes(&body) {
14+
Ok(_) => StatusCode::ACCEPTED,
15+
Err(e) => {
16+
error!(error = %e, "failed to parse events batch payload");
17+
StatusCode::BAD_REQUEST
18+
}
19+
}
20+
}
21+
22+
/// Handler for `POST /api/v1/events`.
23+
pub(crate) async fn handle_events_v1(body: Bytes) -> StatusCode {
24+
record_intake_events(&body, true)
25+
}
26+
27+
/// Handler for `POST /intake/`.
28+
pub(crate) async fn handle_intake(body: Bytes) -> StatusCode {
29+
record_intake_events(&body, false)
30+
}
31+
32+
fn record_intake_events(body: &[u8], strict: bool) -> StatusCode {
33+
let payload = match serde_json::from_slice::<IntakePayload>(body) {
34+
Ok(payload) => payload,
35+
Err(e) if strict => {
36+
error!(error = %e, "failed to parse events intake payload");
37+
return StatusCode::BAD_REQUEST;
38+
}
39+
Err(e) => {
40+
debug!(error = %e, "intake payload did not contain events");
41+
return StatusCode::OK;
42+
}
43+
};
44+
payload.touch();
45+
if strict {
46+
StatusCode::ACCEPTED
47+
} else {
48+
StatusCode::OK
49+
}
50+
}
51+
52+
#[derive(Deserialize)]
53+
struct IntakePayload {
54+
events: Option<HashMap<String, Vec<IntakeEvent>>>,
55+
}
56+
57+
impl IntakePayload {
58+
fn touch(self) {
59+
let Some(events_by_source) = self.events else {
60+
return;
61+
};
62+
for events in events_by_source.into_values() {
63+
for event in events {
64+
event.touch();
65+
}
66+
}
67+
}
68+
}
69+
70+
#[derive(Deserialize)]
71+
struct IntakeEvent {
72+
msg_title: Option<String>,
73+
msg_text: Option<String>,
74+
alert_type: Option<String>,
75+
aggregation_key: Option<String>,
76+
host: Option<String>,
77+
priority: Option<String>,
78+
tags: Option<Vec<String>>,
79+
timestamp: Option<i64>,
80+
}
81+
82+
impl IntakeEvent {
83+
fn touch(self) {
84+
let _ = (
85+
self.msg_title,
86+
self.msg_text,
87+
self.alert_type,
88+
self.aggregation_key,
89+
self.host,
90+
self.priority,
91+
self.tags,
92+
self.timestamp,
93+
);
94+
}
95+
}

test/antithesis/intake/src/http/datadog/metrics.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
use std::time::{SystemTime, UNIX_EPOCH};
88

99
use axum::{
10-
body::to_bytes,
10+
body::{to_bytes, Bytes},
1111
extract::{Request, State},
1212
http::StatusCode,
1313
response::{IntoResponse, Response},
1414
};
15+
use datadog_protos::metrics::SketchPayload;
16+
use protobuf::Message;
1517
use tracing::{debug, error};
1618

1719
use crate::http::middleware::Measurements;
@@ -113,6 +115,17 @@ pub(crate) async fn handle_series(State(state): State<AppState>, request: Reques
113115
Ok(failure.unwrap_or(StatusCode::ACCEPTED))
114116
}
115117

118+
/// Handler for `POST /api/beta/sketches`.
119+
pub(crate) async fn handle_sketches(body: Bytes) -> StatusCode {
120+
match SketchPayload::parse_from_bytes(&body) {
121+
Ok(_) => StatusCode::ACCEPTED,
122+
Err(e) => {
123+
error!(error = %e, "failed to parse sketch payload");
124+
StatusCode::BAD_REQUEST
125+
}
126+
}
127+
}
128+
116129
/// Return the first failed status check, in the given pipeline order, or `None`
117130
/// when every check holds.
118131
fn first_status_failure(checks: &[(bool, StatusCode)]) -> Option<StatusCode> {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//! Service check intake handlers.
2+
3+
use axum::{body::Bytes, http::StatusCode};
4+
use serde::Deserialize;
5+
use tracing::error;
6+
7+
/// Handler for `POST /api/v1/check_run`.
8+
pub(crate) async fn handle_check_run_v1(body: Bytes) -> StatusCode {
9+
let items = match serde_json::from_slice::<Vec<CheckRunItem>>(&body) {
10+
Ok(items) => items,
11+
Err(e) => {
12+
error!(error = %e, "failed to parse check_run payload");
13+
return StatusCode::BAD_REQUEST;
14+
}
15+
};
16+
for item in items {
17+
item.touch();
18+
}
19+
StatusCode::ACCEPTED
20+
}
21+
22+
#[derive(Deserialize)]
23+
struct CheckRunItem {
24+
#[serde(rename = "check")]
25+
name: String,
26+
status: u8,
27+
#[serde(rename = "host_name")]
28+
hostname: Option<String>,
29+
message: Option<String>,
30+
tags: Option<Vec<String>>,
31+
timestamp: Option<u64>,
32+
}
33+
34+
impl CheckRunItem {
35+
fn touch(self) {
36+
let _ = (
37+
self.name,
38+
self.status,
39+
self.hostname,
40+
self.message,
41+
self.tags,
42+
self.timestamp,
43+
);
44+
}
45+
}

0 commit comments

Comments
 (0)