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