Skip to main content

conmonrs/
telemetry.rs

1//! The OpenTelemetry tracing of the server, built with the `tracing` feature:
2//! the exporter of the spans and the trace context the clients pass with their
3//! requests.
4
5use crate::conmon_capnp::conmon;
6use anyhow::{Context, Result};
7use capnp::struct_list::Reader;
8use nix::unistd::gethostname;
9use opentelemetry::{KeyValue, global, propagation::Extractor};
10use opentelemetry_otlp::{SpanExporter, WithExportConfig};
11use opentelemetry_sdk::{Resource, propagation::TraceContextPropagator, trace::SdkTracerProvider};
12use std::{collections::HashMap, process};
13use tracing::Span;
14use tracing_opentelemetry::{OpenTelemetrySpanExt, SetParentError};
15
16// The resource attributes of the OpenTelemetry semantic conventions which
17// the spans carry.
18const HOST_NAME: &str = "host.name";
19const PROCESS_PID: &str = "process.pid";
20
21/// Build the tracer provider which exports the spans to the OTLP endpoint.
22pub(crate) fn tracer_provider(endpoint: &str) -> Result<SdkTracerProvider> {
23    global::set_text_map_propagator(TraceContextPropagator::new());
24
25    let exporter = SpanExporter::builder()
26        .with_tonic()
27        .with_endpoint(endpoint)
28        .build()
29        .context("build exporter")?;
30
31    let hostname = gethostname()
32        .context("get hostname")?
33        .to_str()
34        .context("convert hostname to string")?
35        .to_string();
36
37    let tracer = SdkTracerProvider::builder()
38        .with_batch_exporter(exporter)
39        .with_resource(
40            Resource::builder()
41                .with_service_name(env!("CARGO_PKG_NAME"))
42                .with_attributes([
43                    KeyValue::new(PROCESS_PID, i64::from(process::id())),
44                    KeyValue::new(HOST_NAME, hostname),
45                ])
46                .build(),
47        )
48        .build();
49
50    Ok(tracer)
51}
52
53/// Set the parent context of a span from the provided slice data. It has to
54/// happen before the span got entered the first time, which starts it.
55pub(crate) fn set_parent_context(
56    span: &Span,
57    reader: Reader<conmon::text_text_map_entry::Owned>,
58) -> Result<()> {
59    if reader.is_empty() {
60        // Make it a noop if no data is provided.
61        return Ok(());
62    }
63
64    let metadata = Metadata(into_map(reader)?);
65    let ctx = global::get_text_map_propagator(|prop| prop.extract(&metadata));
66    // Without the telemetry layer, which only exists with tracing enabled,
67    // there is nothing to set.
68    match span.set_parent(ctx) {
69        Ok(()) | Err(SetParentError::LayerNotFound | SetParentError::SpanDisabled) => Ok(()),
70        Err(e) => Err(e).context("set parent context"),
71    }
72}
73
74/// Convert the capnp map of the metadata, a list of key value entries, into
75/// a rust map.
76fn into_map<'a>(
77    reader: Reader<'a, conmon::text_text_map_entry::Owned>,
78) -> capnp::Result<HashMap<&'a str, &'a str>> {
79    reader
80        .iter()
81        .map(|entry| Ok((entry.get_key()?.to_str()?, entry.get_value()?.to_str()?)))
82        .collect()
83}
84
85/// Additional telemetry metadata to carry.
86struct Metadata<'a>(HashMap<&'a str, &'a str>);
87
88impl Extractor for Metadata<'_> {
89    fn get(&self, key: &str) -> Option<&str> {
90        self.0.get(key).copied()
91    }
92
93    /// Collect all the keys from the MetadataMap.
94    fn keys(&self) -> Vec<&str> {
95        self.0.keys().copied().collect()
96    }
97}
98
99#[cfg(test)]
100mod tests;