1use 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 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(_)) => shared.operations.record(vendor, false, Instant::now()),
81 _ => shared.operations.record(vendor, true, Instant::now()),
82 }
83 result
84}
85
86async fn measured<T>(
88 span: Span,
89 vendor: &'static str,
90 operation: &'static str,
91 work: impl Future<Output = Result<T>>,
92) -> Result<T> {
93 let started = Instant::now();
94 let result = work.instrument(span.clone()).await;
95 let outcome = if result.is_ok() { "ok" } else { "error" };
96 span.record("outcome", outcome);
97 record(
98 Meter::Gateway,
99 [vendor, operation, outcome],
100 started.elapsed(),
101 );
102 result
103}
104
105pub(crate) fn http_span(vendor: &'static str, method: &Method, target: &Target) -> Span {
106 tracing::info_span!(
107 "http.client",
108 otel.name = method.as_str(),
109 otel.kind = "client",
110 otel.status_code = Empty,
111 gateway.vendor = vendor,
112 server.address = target.host.as_str(),
113 server.port = target.port,
114 http.request.method = method.as_str(),
115 http.response.status_code = Empty,
116 http.request.resend_count = Empty,
117 duration_ms = Empty,
118 )
119}
120
121pub(crate) fn finish_http(
122 span: &Span,
123 vendor: &'static str,
124 method: &Method,
125 result: &Result<Response>,
126 started: Instant,
127) {
128 let elapsed = started.elapsed();
129 span.record(
130 "duration_ms",
131 u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX),
132 );
133 let outcome = match result {
134 Ok(response) => {
135 span.record("http.response.status_code", response.status());
136 if response.status.is_server_error() {
137 span.record("otel.status_code", "ERROR");
138 }
139 status_class(response.status.as_u16())
140 }
141 Err(error) => {
142 span.record("otel.status_code", "ERROR");
143 span.in_scope(|| tracing::warn!(error = %error, "outgoing request failed"));
144 "error"
145 }
146 };
147 record(
148 Meter::Outgoing,
149 [vendor, method_name(method), outcome],
150 elapsed,
151 );
152}
153
154fn status_class(status: u16) -> &'static str {
155 match status {
156 200..=299 => "2xx",
157 300..=399 => "3xx",
158 400..=499 => "4xx",
159 _ => "5xx",
160 }
161}
162
163#[derive(Clone, Copy)]
164enum Meter {
165 Gateway,
166 Outgoing,
167}
168
169fn record(meter: Meter, labels: [&'static str; 3], elapsed: Duration) {
170 #[cfg(feature = "metrics")]
171 {
172 let instrument = match meter {
173 Meter::Gateway => &GATEWAY_DURATION,
174 Meter::Outgoing => &OUTGOING_DURATION,
175 };
176 let labels = labels.map(sofie_telemetry::metrics::Label::from);
177 sofie_telemetry::metrics::record_duration(instrument, &labels, elapsed);
178 }
179 #[cfg(not(feature = "metrics"))]
180 let _ = (meter, labels, elapsed);
181}
182
183fn method_name(method: &Method) -> &'static str {
185 match *method {
186 Method::GET => "GET",
187 Method::POST => "POST",
188 Method::PUT => "PUT",
189 Method::PATCH => "PATCH",
190 Method::DELETE => "DELETE",
191 Method::HEAD => "HEAD",
192 Method::OPTIONS => "OPTIONS",
193 _ => "_OTHER",
194 }
195}