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 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
92async 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
189fn 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}