Skip to main content

sofie_client/
telemetry.rs

1//! Spans and metrics never record a header, a body or a secret.
2
3use std::time::{Duration, Instant};
4
5use http::Method;
6use sofie_core::{Error, Result};
7use tracing::field::Empty;
8use tracing::{Instrument, Span};
9
10use crate::Shared;
11use crate::breaker;
12use crate::request::Response;
13use crate::target::Target;
14
15#[cfg(feature = "metrics")]
16static GATEWAY_DURATION: sofie_telemetry::metrics::Instrument =
17    sofie_telemetry::metrics::Instrument {
18        name: "sofie.gateway.duration",
19        description: "How long a call of a gateway's adapter or an operation of its vendor took, by vendor, operation and outcome.",
20        kind: sofie_telemetry::metrics::Kind::Duration,
21        labels: &["gateway.vendor", "gateway.operation", "outcome"],
22    };
23#[cfg(feature = "metrics")]
24static OUTGOING_DURATION: sofie_telemetry::metrics::Instrument =
25    sofie_telemetry::metrics::Instrument {
26        name: "sofie.outgoing.duration",
27        description: "How long a request leaving the app took, by gateway.vendor, gateway.operation (the method) and outcome (status class or error).",
28        kind: sofie_telemetry::metrics::Kind::Duration,
29        labels: &["gateway.vendor", "gateway.operation", "outcome"],
30    };
31
32pub fn call<T>(
33    vendor: &'static str,
34    operation: &'static str,
35    work: impl Future<Output = Result<T>>,
36) -> impl Future<Output = Result<T>> {
37    let span = tracing::info_span!(
38        "gateway",
39        otel.name = format!("{vendor}.{operation}"),
40        gateway.vendor = vendor,
41        gateway.operation = operation,
42        outcome = Empty,
43    );
44    measured(span, vendor, operation, work)
45}
46
47pub(crate) fn operation<T>(
48    shared: &Shared,
49    name: &'static str,
50    work: impl Future<Output = Result<T>>,
51) -> impl Future<Output = Result<T>> {
52    let vendor = shared.vendor;
53    let span = tracing::info_span!(
54        "outgoing",
55        otel.name = format!("{vendor}.{name}"),
56        otel.kind = "client",
57        gateway.vendor = vendor,
58        gateway.operation = name,
59        outcome = Empty,
60    );
61    measured(span, vendor, name, bounded(shared, name, work))
62}
63
64async fn bounded<T>(
65    shared: &Shared,
66    name: &'static str,
67    work: impl Future<Output = Result<T>>,
68) -> Result<T> {
69    let vendor = shared.vendor;
70    let passage = shared.operations.admit(vendor)?;
71    let total = shared.limits.total;
72    let result = tokio::time::timeout(total, work).await.unwrap_or_else(|_| {
73        Err(Error::Unavailable(format!(
74            "{vendor}'s {name} did not finish within {} ms",
75            total.as_millis()
76        )))
77    });
78    match &result {
79        Err(error) if breaker::refused_open(error) => {}
80        Err(Error::Unavailable(_)) => {
81            shared
82                .operations
83                .record(vendor, passage, false, Instant::now());
84        }
85        _ => shared
86            .operations
87            .record(vendor, passage, true, Instant::now()),
88    }
89    result
90}
91
92/// Requests are measured apart, under `sofie.outgoing.duration`, so none is counted twice.
93async fn measured<T>(
94    span: Span,
95    vendor: &'static str,
96    operation: &'static str,
97    work: impl Future<Output = Result<T>>,
98) -> Result<T> {
99    let started = Instant::now();
100    let result = work.instrument(span.clone()).await;
101    let outcome = if result.is_ok() { "ok" } else { "error" };
102    span.record("outcome", outcome);
103    record(
104        Meter::Gateway,
105        [vendor, operation, outcome],
106        started.elapsed(),
107    );
108    result
109}
110
111pub(crate) fn http_span(vendor: &'static str, method: &Method, target: &Target) -> Span {
112    tracing::info_span!(
113        "http.client",
114        otel.name = method.as_str(),
115        otel.kind = "client",
116        otel.status_code = Empty,
117        gateway.vendor = vendor,
118        server.address = target.host.as_str(),
119        server.port = target.port,
120        http.request.method = method.as_str(),
121        http.response.status_code = Empty,
122        http.request.resend_count = Empty,
123        duration_ms = Empty,
124    )
125}
126
127pub(crate) fn finish_http(
128    span: &Span,
129    vendor: &'static str,
130    method: &Method,
131    result: &Result<Response>,
132    started: Instant,
133) {
134    let elapsed = started.elapsed();
135    span.record(
136        "duration_ms",
137        u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX),
138    );
139    let outcome = match result {
140        Ok(response) => {
141            span.record("http.response.status_code", response.status());
142            if response.status.is_server_error() {
143                span.record("otel.status_code", "ERROR");
144            }
145            status_class(response.status.as_u16())
146        }
147        Err(error) => {
148            span.record("otel.status_code", "ERROR");
149            span.in_scope(|| tracing::warn!(error = %error, "outgoing request failed"));
150            "error"
151        }
152    };
153    record(
154        Meter::Outgoing,
155        [vendor, method_name(method), outcome],
156        elapsed,
157    );
158}
159
160fn status_class(status: u16) -> &'static str {
161    match status {
162        200..=299 => "2xx",
163        300..=399 => "3xx",
164        400..=499 => "4xx",
165        _ => "5xx",
166    }
167}
168
169#[derive(Clone, Copy)]
170enum Meter {
171    Gateway,
172    Outgoing,
173}
174
175fn record(meter: Meter, labels: [&'static str; 3], elapsed: Duration) {
176    #[cfg(feature = "metrics")]
177    {
178        let instrument = match meter {
179            Meter::Gateway => &GATEWAY_DURATION,
180            Meter::Outgoing => &OUTGOING_DURATION,
181        };
182        let labels = labels.map(sofie_telemetry::metrics::Label::from);
183        sofie_telemetry::metrics::record_duration(instrument, &labels, elapsed);
184    }
185    #[cfg(not(feature = "metrics"))]
186    let _ = (meter, labels, elapsed);
187}
188
189/// A label is a `'static` name, which a method made at runtime is not.
190fn method_name(method: &Method) -> &'static str {
191    match *method {
192        Method::GET => "GET",
193        Method::POST => "POST",
194        Method::PUT => "PUT",
195        Method::PATCH => "PATCH",
196        Method::DELETE => "DELETE",
197        Method::HEAD => "HEAD",
198        Method::OPTIONS => "OPTIONS",
199        _ => "_OTHER",
200    }
201}