1use std::sync::Arc;
16
17use axum::Extension;
18use axum::extract::State;
19use axum::http::header;
20use axum::response::IntoResponse;
21use axum_extra::TypedHeader;
22use bytes::Bytes;
23use common_catalog::consts::{TRACE_TABLE_NAME, TRACE_TABLE_NAME_SESSION_KEY};
24use common_telemetry::tracing;
25use headers::ContentType;
26use mime_guess::mime;
27use opentelemetry_proto::tonic::collector::logs::v1::{
28 ExportLogsServiceRequest, ExportLogsServiceResponse,
29};
30use opentelemetry_proto::tonic::collector::metrics::v1::ExportMetricsServiceResponse;
31use opentelemetry_proto::tonic::collector::trace::v1::{
32 ExportTraceServiceRequest, ExportTraceServiceResponse,
33};
34use otel_arrow_rust::proto::opentelemetry::collector::metrics::v1::ExportMetricsServiceRequest;
35use pipeline::PipelineWay;
36use prost::Message;
37use session::context::{Channel, QueryContext};
38use session::protocol_ctx::{MetricType, OtlpMetricCtx, ProtocolCtx};
39use snafu::prelude::*;
40
41use crate::error::{self, PipelineSnafu, Result};
42use crate::http::extractor::{
43 LogTableName, OtlpMetricOptions, PipelineInfo, SelectInfoWrapper, TraceTableName,
44};
45use crate::http::header::{CONTENT_TYPE_PROTOBUF, write_cost_header_map};
46use crate::metrics::METRIC_HTTP_OPENTELEMETRY_LOGS_ELAPSED;
47use crate::query_handler::{OpenTelemetryProtocolHandlerRef, PipelineHandler};
48
49fn is_json_content_type(content_type: Option<&ContentType>) -> bool {
50 match content_type {
51 None => false,
52 Some(ct) => {
53 let mime: mime::Mime = ct.clone().into();
54 mime.subtype() == mime::JSON
55 }
56 }
57}
58
59fn content_type_to_string(content_type: Option<&TypedHeader<ContentType>>) -> String {
60 content_type
61 .map(|h| h.0.to_string())
62 .unwrap_or_else(|| "not specified".to_string())
63}
64
65#[derive(Clone)]
66pub struct OtlpState {
67 pub with_metric_engine: bool,
68 pub handler: OpenTelemetryProtocolHandlerRef,
69}
70
71#[axum_macros::debug_handler]
72#[tracing::instrument(skip_all, fields(protocol = "otlp", request_type = "metrics"))]
73pub async fn metrics(
74 State(state): State<OtlpState>,
75 Extension(mut query_ctx): Extension<QueryContext>,
76 http_opts: OtlpMetricOptions,
77 content_type: Option<TypedHeader<ContentType>>,
78 bytes: Bytes,
79) -> Result<OtlpResponse<ExportMetricsServiceResponse>> {
80 if is_json_content_type(content_type.as_ref().map(|h| &h.0)) {
81 return error::UnsupportedJsonContentTypeSnafu {}.fail();
82 }
83
84 let db = query_ctx.get_db_string();
85 query_ctx.set_channel(Channel::Otlp);
86
87 let _timer = crate::metrics::METRIC_HTTP_OPENTELEMETRY_METRICS_ELAPSED
88 .with_label_values(&[db.as_str()])
89 .start_timer();
90 let request = ExportMetricsServiceRequest::decode(bytes).with_context(|_| {
91 error::DecodeOtlpRequestSnafu {
92 content_type: content_type_to_string(content_type.as_ref()),
93 }
94 })?;
95
96 let OtlpState {
97 with_metric_engine,
98 handler,
99 } = state;
100
101 query_ctx.set_protocol_ctx(ProtocolCtx::OtlpMetric(OtlpMetricCtx {
102 promote_all_resource_attrs: http_opts.promote_all_resource_attrs,
103 resource_attrs: http_opts.resource_attrs,
104 promote_scope_attrs: http_opts.promote_scope_attrs,
105 with_metric_engine,
106 is_legacy: false,
108 metric_type: MetricType::Init,
109 }));
110 let query_ctx = Arc::new(query_ctx);
111
112 handler
113 .metrics(request, query_ctx)
114 .await
115 .map(|o| OtlpResponse {
116 resp_body: ExportMetricsServiceResponse {
117 partial_success: None,
118 },
119 write_cost: o.meta.cost,
120 })
121}
122
123#[axum_macros::debug_handler]
124#[tracing::instrument(skip_all, fields(protocol = "otlp", request_type = "traces"))]
125pub async fn traces(
126 State(state): State<OtlpState>,
127 TraceTableName(table_name): TraceTableName,
128 pipeline_info: PipelineInfo,
129 Extension(mut query_ctx): Extension<QueryContext>,
130 content_type: Option<TypedHeader<ContentType>>,
131 bytes: Bytes,
132) -> Result<OtlpResponse<ExportTraceServiceResponse>> {
133 if is_json_content_type(content_type.as_ref().map(|h| &h.0)) {
134 return error::UnsupportedJsonContentTypeSnafu {}.fail();
135 }
136
137 let db = query_ctx.get_db_string();
138 let table_name = table_name.unwrap_or_else(|| TRACE_TABLE_NAME.to_string());
139
140 query_ctx.set_channel(Channel::Otlp);
141 query_ctx.set_extension(TRACE_TABLE_NAME_SESSION_KEY, &table_name);
142
143 let query_ctx = Arc::new(query_ctx);
144 let _timer = crate::metrics::METRIC_HTTP_OPENTELEMETRY_TRACES_ELAPSED
145 .with_label_values(&[db.as_str()])
146 .start_timer();
147 let request = ExportTraceServiceRequest::decode(bytes).with_context(|_| {
148 error::DecodeOtlpRequestSnafu {
149 content_type: content_type_to_string(content_type.as_ref()),
150 }
151 })?;
152
153 let pipeline = PipelineWay::from_name_and_default(
154 pipeline_info.pipeline_name.as_deref(),
155 pipeline_info.pipeline_version.as_deref(),
156 None,
157 )
158 .context(PipelineSnafu)?;
159
160 let pipeline_params = pipeline_info.pipeline_params;
161
162 let OtlpState { handler, .. } = state;
163
164 let pipeline_handler: Arc<dyn PipelineHandler + Send + Sync> = handler.clone();
167
168 handler
169 .traces(
170 pipeline_handler,
171 request,
172 pipeline,
173 pipeline_params,
174 table_name,
175 query_ctx,
176 )
177 .await
178 .map(|o| OtlpResponse {
179 resp_body: ExportTraceServiceResponse {
180 partial_success: None,
181 },
182 write_cost: o.meta.cost,
183 })
184}
185
186#[axum_macros::debug_handler]
187#[tracing::instrument(skip_all, fields(protocol = "otlp", request_type = "logs"))]
188pub async fn logs(
189 State(state): State<OtlpState>,
190 Extension(mut query_ctx): Extension<QueryContext>,
191 pipeline_info: PipelineInfo,
192 LogTableName(tablename): LogTableName,
193 SelectInfoWrapper(select_info): SelectInfoWrapper,
194 content_type: Option<TypedHeader<ContentType>>,
195 bytes: Bytes,
196) -> Result<OtlpResponse<ExportLogsServiceResponse>> {
197 if is_json_content_type(content_type.as_ref().map(|h| &h.0)) {
198 return error::UnsupportedJsonContentTypeSnafu {}.fail();
199 }
200
201 let tablename = tablename.unwrap_or_else(|| "opentelemetry_logs".to_string());
202 let db = query_ctx.get_db_string();
203 query_ctx.set_channel(Channel::Otlp);
204 let query_ctx = Arc::new(query_ctx);
205 let _timer = METRIC_HTTP_OPENTELEMETRY_LOGS_ELAPSED
206 .with_label_values(&[db.as_str()])
207 .start_timer();
208 let request = ExportLogsServiceRequest::decode(bytes).with_context(|_| {
209 error::DecodeOtlpRequestSnafu {
210 content_type: content_type_to_string(content_type.as_ref()),
211 }
212 })?;
213
214 let pipeline = PipelineWay::from_name_and_default(
215 pipeline_info.pipeline_name.as_deref(),
216 pipeline_info.pipeline_version.as_deref(),
217 Some(PipelineWay::OtlpLogDirect(Box::new(select_info))),
218 )
219 .context(PipelineSnafu)?;
220 let pipeline_params = pipeline_info.pipeline_params;
221
222 let OtlpState { handler, .. } = state;
223
224 let pipeline_handler: Arc<dyn PipelineHandler + Send + Sync> = handler.clone();
227 handler
228 .logs(
229 pipeline_handler,
230 request,
231 pipeline,
232 pipeline_params,
233 tablename,
234 query_ctx,
235 )
236 .await
237 .map(|o| OtlpResponse {
238 resp_body: ExportLogsServiceResponse {
239 partial_success: None,
240 },
241 write_cost: o.iter().map(|o| o.meta.cost).sum(),
242 })
243}
244
245pub struct OtlpResponse<T: Message> {
246 resp_body: T,
247 write_cost: usize,
248}
249
250impl<T: Message> IntoResponse for OtlpResponse<T> {
251 fn into_response(self) -> axum::response::Response {
252 let mut header_map = write_cost_header_map(self.write_cost);
253 header_map.insert(header::CONTENT_TYPE, CONTENT_TYPE_PROTOBUF.clone());
254
255 (header_map, self.resp_body.encode_to_vec()).into_response()
256 }
257}