1use 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::Encoding;
28use micromegas_otel_ingestion::error::OtelError;
29use micromegas_otel_ingestion::handler;
30use micromegas_tracing::prelude::*;
31use prost::Message;
32use std::sync::Arc;
33
34const CONTENT_TYPE_PROTOBUF: &str = "application/x-protobuf";
35const CONTENT_TYPE_JSON: &str = "application/json";
36
37fn content_type_encoding(headers: &HeaderMap) -> Result<Encoding, OtlpHttpError> {
41 let Some(ct) = headers.get(header::CONTENT_TYPE) else {
42 return Err(OtlpHttpError::WrongContentType);
43 };
44 let Ok(ct) = ct.to_str() else {
45 return Err(OtlpHttpError::WrongContentType);
46 };
47 let media = ct
48 .split(';')
49 .next()
50 .unwrap_or("")
51 .trim()
52 .to_ascii_lowercase();
53 match media.as_str() {
54 CONTENT_TYPE_PROTOBUF => Ok(Encoding::Protobuf),
55 CONTENT_TYPE_JSON => Ok(Encoding::Json),
56 _ => Err(OtlpHttpError::WrongContentType),
57 }
58}
59
60enum OtlpHttpError {
64 WrongContentType,
65 Otel(OtelError),
66}
67
68impl OtlpHttpError {
69 fn into_otlp_response(self, encoding: Encoding) -> Response {
70 match self {
71 OtlpHttpError::WrongContentType => build_error_response(
72 StatusCode::UNSUPPORTED_MEDIA_TYPE,
73 3, "Content-Type must be application/x-protobuf or application/json",
75 false,
76 Encoding::Protobuf,
78 ),
79 OtlpHttpError::Otel(err) => {
80 let retryable = err.is_retryable();
81 let status = match err.http_status() {
82 400 => StatusCode::BAD_REQUEST,
83 503 => StatusCode::SERVICE_UNAVAILABLE,
84 other => {
85 StatusCode::from_u16(other).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
86 }
87 };
88 let code = err.grpc_code();
89 error!("OTLP error: {}", err);
93 build_error_response(status, code, &err.public_message(), retryable, encoding)
94 }
95 }
96 }
97}
98
99fn build_error_response(
100 status: StatusCode,
101 code: i32,
102 message: &str,
103 retryable: bool,
104 encoding: Encoding,
105) -> Response {
106 let proto_status = micromegas_otel_ingestion::proto::Status {
107 code,
108 message: message.to_string(),
109 };
110 let (body, content_type) = match encoding {
111 Encoding::Protobuf => (proto_status.encode_to_vec(), CONTENT_TYPE_PROTOBUF),
112 Encoding::Json => (
113 serde_json::to_vec(&proto_status).expect("serializing Status to JSON"),
114 CONTENT_TYPE_JSON,
115 ),
116 };
117 let mut response = Response::builder()
118 .status(status)
119 .header(header::CONTENT_TYPE, HeaderValue::from_static(content_type))
120 .body(Body::from(body))
121 .expect("building OTLP error response");
122 if retryable && let Ok(value) = HeaderValue::from_str(&RETRY_AFTER_SECONDS.to_string()) {
123 response.headers_mut().insert(header::RETRY_AFTER, value);
124 }
125 response
126}
127
128fn success_response<M: Message + serde::Serialize>(msg: M, encoding: Encoding) -> Response {
129 let (body, content_type) = match encoding {
130 Encoding::Protobuf => (msg.encode_to_vec(), CONTENT_TYPE_PROTOBUF),
131 Encoding::Json => (
132 serde_json::to_vec(&msg).expect("serializing OTLP response to JSON"),
133 CONTENT_TYPE_JSON,
134 ),
135 };
136 Response::builder()
137 .status(StatusCode::OK)
138 .header(header::CONTENT_TYPE, HeaderValue::from_static(content_type))
139 .body(Body::from(body))
140 .expect("building OTLP success response")
141}
142
143async fn logs_handler(
144 Extension(service): Extension<Arc<WebIngestionService>>,
145 ctx: Option<Extension<AuthContext>>,
146 headers: HeaderMap,
147 body: bytes::Bytes,
148) -> Response {
149 let encoding = match content_type_encoding(&headers) {
150 Ok(enc) => enc,
151 Err(e) => return e.into_otlp_response(Encoding::Protobuf),
152 };
153 let audience = resolve_write_audience(ctx.as_ref(), service.default_audience());
154 match handler::ingest_logs(service, body, encoding, &audience).await {
155 Ok(resp) => success_response(resp, encoding),
156 Err(e) => OtlpHttpError::Otel(e).into_otlp_response(encoding),
157 }
158}
159
160async fn metrics_handler(
161 Extension(service): Extension<Arc<WebIngestionService>>,
162 ctx: Option<Extension<AuthContext>>,
163 headers: HeaderMap,
164 body: bytes::Bytes,
165) -> Response {
166 let encoding = match content_type_encoding(&headers) {
167 Ok(enc) => enc,
168 Err(e) => return e.into_otlp_response(Encoding::Protobuf),
169 };
170 let audience = resolve_write_audience(ctx.as_ref(), service.default_audience());
171 match handler::ingest_metrics(service, body, encoding, &audience).await {
172 Ok(resp) => success_response(resp, encoding),
173 Err(e) => OtlpHttpError::Otel(e).into_otlp_response(encoding),
174 }
175}
176
177async fn traces_handler(
178 Extension(service): Extension<Arc<WebIngestionService>>,
179 ctx: Option<Extension<AuthContext>>,
180 headers: HeaderMap,
181 body: bytes::Bytes,
182) -> Response {
183 let encoding = match content_type_encoding(&headers) {
184 Ok(enc) => enc,
185 Err(e) => return e.into_otlp_response(Encoding::Protobuf),
186 };
187 let audience = resolve_write_audience(ctx.as_ref(), service.default_audience());
188 match handler::ingest_traces(service, body, encoding, &audience).await {
189 Ok(resp) => success_response(resp, encoding),
190 Err(e) => OtlpHttpError::Otel(e).into_otlp_response(encoding),
191 }
192}
193
194pub fn otlp_router() -> Router {
197 apply_ingestion_body_limits(
198 Router::new()
199 .route("/ingestion/otlp/v1/logs", post(logs_handler))
200 .route("/ingestion/otlp/v1/metrics", post(metrics_handler))
201 .route("/ingestion/otlp/v1/traces", post(traces_handler)),
202 )
203}