micromegas/servers/
webhook.rs1use super::ingestion_limits::{RETRY_AFTER_SECONDS, apply_ingestion_body_limits};
18use axum::Extension;
19use axum::Router;
20use axum::body::Body;
21use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
22use axum::response::Response;
23use axum::routing::post;
24use micromegas_ingestion::web_ingestion_service::WebIngestionService;
25use micromegas_otel_ingestion::handler;
26use micromegas_otel_ingestion::proto::{AnyValue, KeyValue, any_value};
27use micromegas_tracing::prelude::*;
28use std::sync::Arc;
29
30const HEADER_SERVICE_NAME: &str = "X-Micromegas-Service-Name";
31const HEADER_SERVICE_NAMESPACE: &str = "X-Micromegas-Service-Namespace";
32const HEADER_TARGET: &str = "X-Micromegas-Target";
33
34fn push_attr_from_header(
39 attrs: &mut Vec<KeyValue>,
40 headers: &HeaderMap,
41 header_name: &str,
42 otel_key: &str,
43) {
44 let Some(value) = headers.get(header_name) else {
45 return;
46 };
47 let Ok(value) = value.to_str() else {
48 return;
49 };
50 attrs.push(KeyValue {
51 key: otel_key.to_string(),
52 key_strindex: 0,
53 value: Some(AnyValue {
54 value: Some(any_value::Value::StringValue(value.to_string())),
55 }),
56 });
57}
58
59fn target_from_header(headers: &HeaderMap) -> String {
61 headers
62 .get(HEADER_TARGET)
63 .and_then(|v| v.to_str().ok())
64 .unwrap_or("")
65 .to_string()
66}
67
68fn canonical_header_bytes(headers: &HeaderMap) -> Vec<u8> {
83 let mut pairs: Vec<(String, &[u8])> = headers
84 .iter()
85 .map(|(name, value)| (name.as_str().to_ascii_lowercase(), value.as_bytes()))
86 .collect();
87 pairs.sort();
88
89 let mut buf = Vec::new();
90 for (name, value) in pairs {
91 buf.extend_from_slice(name.as_bytes());
92 buf.push(0);
93 buf.extend_from_slice(value);
94 buf.push(0);
95 }
96 buf
97}
98
99fn build_error_response(status: StatusCode, message: &str, retryable: bool) -> Response {
100 let mut response = Response::builder()
101 .status(status)
102 .header(
103 header::CONTENT_TYPE,
104 HeaderValue::from_static("text/plain; charset=utf-8"),
105 )
106 .body(Body::from(message.to_string()))
107 .expect("building webhook error response");
108 if retryable && let Ok(value) = HeaderValue::from_str(&RETRY_AFTER_SECONDS.to_string()) {
109 response.headers_mut().insert(header::RETRY_AFTER, value);
110 }
111 response
112}
113
114async fn webhook_handler(
115 Extension(service): Extension<Arc<WebIngestionService>>,
116 headers: HeaderMap,
117 body: bytes::Bytes,
118) -> Response {
119 if body.is_empty() {
120 return build_error_response(StatusCode::BAD_REQUEST, "empty body", false);
121 }
122
123 let mut resource_attrs = Vec::new();
124 push_attr_from_header(
125 &mut resource_attrs,
126 &headers,
127 HEADER_SERVICE_NAME,
128 "service.name",
129 );
130 push_attr_from_header(
131 &mut resource_attrs,
132 &headers,
133 HEADER_SERVICE_NAMESPACE,
134 "service.namespace",
135 );
136 let target = target_from_header(&headers);
137 let header_hash_input = canonical_header_bytes(&headers);
138
139 match handler::ingest_webhook(service, resource_attrs, target, body, &header_hash_input).await {
140 Ok(()) => Response::builder()
141 .status(StatusCode::OK)
142 .body(Body::empty())
143 .expect("building webhook success response"),
144 Err(err) => {
145 let retryable = err.is_retryable();
146 let status = StatusCode::from_u16(err.http_status())
147 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
148 error!("webhook error: {err}");
151 build_error_response(status, &err.public_message(), retryable)
152 }
153 }
154}
155
156pub fn webhook_router() -> Router {
159 apply_ingestion_body_limits(Router::new().route("/ingestion/webhook", post(webhook_handler)))
160}