linera_rpc/
propagation.rs1use std::task::{Context as TaskContext, Poll};
31
32use futures::{future::BoxFuture, FutureExt};
33use opentelemetry::{
34 global,
35 propagation::{Extractor, Injector},
36 Context,
37};
38use tonic::metadata::{MetadataKey, MetadataMap, MetadataValue};
39use tower::{Layer, Service};
40use tracing::warn;
41
42pub const TRAFFIC_TYPE_KEY: &str = "traffic_type";
47
48pub const TRAFFIC_TYPE_ORGANIC: &str = "organic";
50
51pub const TRAFFIC_TYPE_SYNTHETIC: &str = "synthetic";
53
54pub const TRAFFIC_TYPE_UNKNOWN: &str = "unknown";
56
57pub const TRAFFIC_TYPE_ENV_VAR: &str = "LINERA_TRAFFIC_TYPE";
62
63#[derive(Clone, Copy, Debug, Default)]
79pub struct OtelContextLayer;
80
81#[derive(Clone, Debug)]
83pub struct OtelContextService<S> {
84 inner: S,
85}
86
87#[derive(Clone, Debug)]
89pub struct ExtractedOtelContext(pub Context);
90
91pub trait HasOtelContext {
96 fn get_otel_context(&self) -> Option<&ExtractedOtelContext>;
98}
99
100impl<B> HasOtelContext for http::Request<B> {
101 fn get_otel_context(&self) -> Option<&ExtractedOtelContext> {
102 self.extensions().get::<ExtractedOtelContext>()
103 }
104}
105
106impl<T> HasOtelContext for tonic::Request<T> {
107 fn get_otel_context(&self) -> Option<&ExtractedOtelContext> {
108 self.extensions().get::<ExtractedOtelContext>()
109 }
110}
111
112pub fn inject_context(cx: &Context, metadata: &mut MetadataMap) {
123 global::get_text_map_propagator(|propagator| {
124 propagator.inject_context(cx, &mut MetadataInjector(metadata));
125 });
126}
127
128pub fn extract_context(metadata: &MetadataMap) -> Context {
143 global::get_text_map_propagator(|propagator| propagator.extract(&MetadataExtractor(metadata)))
144}
145
146pub fn get_context_with_traffic_type() -> Context {
155 use opentelemetry::{baggage::BaggageExt, Key, KeyValue};
156
157 let cx = Context::current();
158
159 if std::env::var(TRAFFIC_TYPE_ENV_VAR).is_ok_and(|v| v == TRAFFIC_TYPE_SYNTHETIC) {
160 cx.with_baggage(vec![KeyValue::new(
161 Key::new(TRAFFIC_TYPE_KEY),
162 TRAFFIC_TYPE_SYNTHETIC,
163 )])
164 } else {
165 cx
166 }
167}
168
169pub fn get_traffic_type(cx: &Context) -> &'static str {
173 use opentelemetry::baggage::BaggageExt;
174
175 cx.baggage()
176 .get(TRAFFIC_TYPE_KEY)
177 .map(|v| v.as_str())
178 .and_then(|v| {
179 if v == TRAFFIC_TYPE_SYNTHETIC {
180 Some(TRAFFIC_TYPE_SYNTHETIC)
181 } else {
182 None
183 }
184 })
185 .unwrap_or(TRAFFIC_TYPE_ORGANIC)
186}
187
188pub fn get_traffic_type_from_request<R: HasOtelContext>(request: &R) -> &'static str {
193 request
194 .get_otel_context()
195 .map_or(TRAFFIC_TYPE_ORGANIC, |ext| get_traffic_type(&ext.0))
196}
197
198pub fn get_otel_context_from_tonic_request<T>(request: &tonic::Request<T>) -> Option<Context> {
203 request
204 .extensions()
205 .get::<ExtractedOtelContext>()
206 .map(|ext| ext.0.clone())
207}
208
209pub fn create_request_with_context<T>(inner: T, cx: Option<&Context>) -> tonic::Request<T> {
214 let mut request = tonic::Request::new(inner);
215 if let Some(cx) = cx {
216 inject_context(cx, request.metadata_mut());
217 }
218 request
219}
220
221pub fn create_request_with_current_span_context<T>(inner: T) -> tonic::Request<T> {
227 use tracing_opentelemetry::OpenTelemetrySpanExt;
228 let cx = tracing::Span::current().context();
229 create_request_with_context(inner, Some(&cx))
230}
231
232impl<S> Layer<S> for OtelContextLayer {
233 type Service = OtelContextService<S>;
234
235 fn layer(&self, service: S) -> Self::Service {
236 OtelContextService { inner: service }
237 }
238}
239
240impl<S, B> Service<http::Request<B>> for OtelContextService<S>
241where
242 S: Service<http::Request<B>> + Clone + Send + 'static,
243 S::Future: Send,
244 B: Send + 'static,
245{
246 type Response = S::Response;
247 type Error = S::Error;
248 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
249
250 fn poll_ready(&mut self, cx: &mut TaskContext<'_>) -> Poll<Result<(), Self::Error>> {
251 self.inner.poll_ready(cx)
252 }
253
254 fn call(&mut self, mut request: http::Request<B>) -> Self::Future {
255 use tracing::Instrument;
256 use tracing_opentelemetry::OpenTelemetrySpanExt;
257
258 let cx = global::get_text_map_propagator(|propagator| {
259 propagator.extract(&HttpHeaderExtractor(request.headers()))
260 });
261
262 request
263 .extensions_mut()
264 .insert(ExtractedOtelContext(cx.clone()));
265
266 let span = tracing::info_span!("grpc_request");
267 span.set_parent(cx);
268
269 let mut inner = self.inner.clone();
270 async move { inner.call(request).await }
271 .instrument(span)
272 .boxed()
273 }
274}
275
276struct MetadataInjector<'a>(&'a mut MetadataMap);
277
278impl Injector for MetadataInjector<'_> {
279 fn set(&mut self, key: &str, value: String) {
280 match MetadataKey::from_bytes(key.as_bytes()) {
281 Ok(key) => match MetadataValue::try_from(&value) {
282 Ok(value) => {
283 self.0.insert(key, value);
284 }
285 Err(error) => {
286 warn!(
287 value,
288 error = format!("{error:#}"),
289 "failed to parse metadata value"
290 );
291 }
292 },
293 Err(error) => {
294 warn!(
295 key,
296 error = format!("{error:#}"),
297 "failed to parse metadata key"
298 );
299 }
300 }
301 }
302}
303
304struct MetadataExtractor<'a>(&'a MetadataMap);
305
306impl Extractor for MetadataExtractor<'_> {
307 fn get(&self, key: &str) -> Option<&str> {
308 self.0.get(key).and_then(|value| value.to_str().ok())
309 }
310
311 fn keys(&self) -> Vec<&str> {
312 self.0
313 .keys()
314 .filter_map(|key| match key {
315 tonic::metadata::KeyRef::Ascii(key) => Some(key.as_str()),
316 tonic::metadata::KeyRef::Binary(_) => None,
317 })
318 .collect()
319 }
320}
321
322struct HttpHeaderExtractor<'a>(&'a http::HeaderMap);
323
324impl Extractor for HttpHeaderExtractor<'_> {
325 fn get(&self, key: &str) -> Option<&str> {
326 self.0.get(key).and_then(|v| v.to_str().ok())
327 }
328
329 fn keys(&self) -> Vec<&str> {
330 self.0.keys().map(|k| k.as_str()).collect()
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use opentelemetry::{baggage::BaggageExt, Key, KeyValue};
337
338 use super::*;
339
340 #[test]
341 fn test_inject_and_extract_baggage() {
342 use opentelemetry::propagation::TextMapCompositePropagator;
343 use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator};
344
345 let propagator = TextMapCompositePropagator::new(vec![
346 Box::new(TraceContextPropagator::new()),
347 Box::new(BaggagePropagator::new()),
348 ]);
349 global::set_text_map_propagator(propagator);
350
351 let cx = Context::current().with_baggage(vec![KeyValue::new(
352 Key::new(TRAFFIC_TYPE_KEY),
353 TRAFFIC_TYPE_SYNTHETIC,
354 )]);
355
356 let mut metadata = MetadataMap::new();
357 inject_context(&cx, &mut metadata);
358
359 assert!(
360 metadata.get("baggage").is_some(),
361 "baggage header should be present"
362 );
363
364 let extracted_cx = extract_context(&metadata);
365 let traffic_type = get_traffic_type(&extracted_cx);
366 assert_eq!(traffic_type, TRAFFIC_TYPE_SYNTHETIC);
367 }
368
369 #[test]
370 fn test_default_traffic_type_is_organic() {
371 let cx = Context::current();
372 assert_eq!(get_traffic_type(&cx), TRAFFIC_TYPE_ORGANIC);
373 }
374}