1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use crate::{Metrics, Trace};
use opentelemetry::{
    global::{BoxedTracer, ObjectSafeTracer},
    metrics::MeterProvider,
};
use std::{borrow::Cow, sync::Arc};
use trillium::{Conn, HeaderName};
use trillium_macros::Handler;

/// a handler to send both traces and metrics in accordances with [semantic conventions for
/// http](https://opentelemetry.io/docs/specs/semconv/http/).
///
/// This is composed of a [`Trace`] handler and [`Metrics`] handler.
#[derive(Debug, Handler)]
pub struct Instrument((Trace<BoxedTracer>, Metrics));

/// construct an [`Instrument`] with the provided meter and tracer
pub fn instrument<T: ObjectSafeTracer + Send + Sync + 'static>(
    meter: impl Into<Metrics>,
    tracer: T,
) -> Instrument {
    Instrument::new(meter, tracer)
}

impl Instrument {
    /// construct a new [`Instrument`] with the provided meter and tracer
    pub fn new(
        meter: impl Into<Metrics>,
        tracer: impl ObjectSafeTracer + Send + Sync + 'static,
    ) -> Self {
        Self((Trace::new(BoxedTracer::new(Box::new(tracer))), meter.into()))
    }

    /// provides a route specification
    ///
    /// in order to avoid forcing anyone to use a particular router, this is provided as a
    /// configuration hook.
    ///
    /// for use with [`trillium-router`](https://docs.trillium.rs/trillium_router/index.html),
    /// ```
    /// use trillium_router::RouterConnExt;
    /// trillium_opentelemetry::Metrics::new(&opentelemetry::global::meter("example"))
    ///     .with_route(|conn| conn.route().map(|r| r.to_string().into()));
    /// ```
    pub fn with_route<F>(mut self, route: F) -> Self
    where
        F: Fn(&Conn) -> Option<Cow<'static, str>> + Send + Sync + 'static,
    {
        let route = Arc::new(route);
        self.0 .0.route = Some(route.clone());
        self.0 .1.route = Some(route);
        self
    }

    /// Provides an optional low-cardinality error type specification to the metrics collector.
    ///
    /// The implementation of this is application specific, but will often look like checking the
    /// [`Conn::state`] for an error enum and mapping that to a low-cardinality `&'static str`.
    pub fn with_error_type<F>(mut self, error_type: F) -> Self
    where
        F: Fn(&Conn) -> Option<Cow<'static, str>> + Send + Sync + 'static,
    {
        let error_type = Arc::new(error_type);
        self.0 .0.error_type = Some(error_type.clone());
        self.0 .1.error_type = Some(error_type);
        self
    }

    /// Provides a callback for `server.address` and `server.port` attributes to be used in metrics
    /// attributes. This has no effect on tracing span attributes, where `server.address` and
    /// `server.port` are always enabled.
    ///
    /// These should be set based on request headers according to the [OpenTelemetry HTTP semantic
    /// conventions][semconv-server-address-port].
    ///
    /// It is not recommended to enable this when the server is exposed to clients outside of your
    /// control, as request headers could arbitrarily increase the cardinality of these attributes.
    ///
    /// [semconv-server-address-port]:
    ///     https://opentelemetry.io/docs/specs/semconv/http/http-spans/#setting-serveraddress-and-serverport-attributes
    pub fn with_metrics_server_address_and_port<F>(mut self, server_address_and_port: F) -> Self
    where
        F: Fn(&Conn) -> Option<(Cow<'static, str>, u16)> + Send + Sync + 'static,
    {
        self.0 .1.server_address_and_port = Some(Arc::new(server_address_and_port));
        self
    }

    /// Specify a list of request headers to include in the trace spans
    pub fn with_headers(
        mut self,
        headers: impl IntoIterator<Item = impl Into<HeaderName<'static>>>,
    ) -> Self {
        self.0 .0.headers = headers.into_iter().map(Into::into).collect();
        self
    }

    /// Enable population of the local socket address and port in the trace spans.
    ///
    /// This populates the `network.local.address` and `network.local.port` attributes.
    pub fn with_local_address_and_port(mut self) -> Self {
        self.0 .0.enable_local_address_and_port = true;
        self
    }
}

/// The primary entrypoint if using [`opentelemetry::global`].
///
/// constructs a versioned meter and tracer with the name `"trillium-opentelemetry"`.
pub fn instrument_global() -> Instrument {
    instrument(
        opentelemetry::global::meter_provider().versioned_meter(
            "trillium-opentelemetry",
            Some(env!("CARGO_PKG_VERSION")),
            Some("https://opentelemetry.io/schemas/1.22.0"),
            None,
        ),
        opentelemetry::global::tracer("trillium-opentelemetry"),
    )
}