Skip to main content

feldera_observability/
lib.rs

1use actix_http::header::{HeaderName, HeaderValue};
2use awc::ClientRequest;
3use reqwest::RequestBuilder;
4use sentry::{ClientInitGuard, TransactionContext};
5use std::borrow::Cow;
6use std::env;
7
8/// Initializes Sentry if `FELDERA_SENTRY_ENABLED` is set, tagging the service and release.
9///
10/// Use dsn as the default DSN, which can be overridden by the `SENTRY_DSN` environment variable.
11pub fn init(dsn: &str, service_name: &str, release: &str) -> Option<ClientInitGuard> {
12    env::var("FELDERA_SENTRY_ENABLED")
13        .ok()
14        .filter(|v| v == "1")?;
15
16    const DEFAULT_SAMPLE_RATE: f32 = 1.0;
17    const DEFAULT_TRACE_SAMPLE_RATE: f32 = 0.1;
18    const MAX_BREADCRUMBS: usize = 10;
19
20    let dsn = env::var("SENTRY_DSN").ok().unwrap_or(dsn.to_string());
21    let sample_rate = env::var("SENTRY_SAMPLE_RATE")
22        .ok()
23        .and_then(|rate| rate.parse::<f32>().ok())
24        .unwrap_or(DEFAULT_SAMPLE_RATE);
25    let traces_sample_rate = env::var("SENTRY_TRACES_SAMPLE_RATE")
26        .ok()
27        .and_then(|rate| rate.parse::<f32>().ok())
28        .unwrap_or(DEFAULT_TRACE_SAMPLE_RATE);
29    let environment = env::var("SENTRY_ENVIRONMENT")
30        .unwrap_or_else(|_| String::from("dev"))
31        .into();
32    let accept_invalid_certs = environment == "ci" || environment == "dev";
33    let max_breadcrumbs = env::var("SENTRY_MAX_BREADCRUMBS")
34        .ok()
35        .and_then(|rate| rate.parse::<usize>().ok())
36        .unwrap_or(MAX_BREADCRUMBS);
37
38    let guard = sentry::init((
39        dsn,
40        sentry::ClientOptions {
41            environment: Some(environment),
42            release: Some(Cow::Owned(release.to_string())),
43            max_breadcrumbs,
44            sample_rate,
45            traces_sample_rate,
46            enable_logs: true,
47            attach_stacktrace: true,
48            accept_invalid_certs,
49            ..Default::default()
50        },
51    ));
52
53    sentry::configure_scope(|scope| scope.set_tag("service", service_name));
54    Some(guard)
55}
56
57fn sentry_enabled() -> bool {
58    sentry::Hub::current().client().is_some()
59}
60
61/// Returns an Actix middleware that captures errors and traces when Sentry is enabled.
62pub fn actix_middleware() -> sentry::integrations::actix::Sentry {
63    sentry::integrations::actix::Sentry::builder()
64        .emit_header(true)
65        .capture_server_errors(true)
66        .start_transaction(true)
67        .finish()
68}
69
70pub mod fips;
71pub mod json_logging;
72
73fn trace_header_value() -> Option<String> {
74    if !sentry_enabled() {
75        return None;
76    }
77
78    let mut header = None;
79    sentry::configure_scope(|scope| {
80        if let Some(span) = scope.get_span() {
81            header = span.iter_headers().next().map(|(_, value)| value);
82        }
83    });
84    if header.is_none() {
85        let transaction =
86            sentry::start_transaction(TransactionContext::new("http.client", "http.client"));
87        header = transaction.iter_headers().next().map(|(_, value)| value);
88        transaction.finish();
89    }
90    header
91}
92
93/// Adds Sentry trace headers to outgoing awc requests.
94pub trait AwcRequestTracingExt {
95    fn with_sentry_tracing(self) -> Self;
96}
97
98impl AwcRequestTracingExt for ClientRequest {
99    fn with_sentry_tracing(mut self) -> Self {
100        if let Some(value) = trace_header_value()
101            && let Ok(header_value) = HeaderValue::from_str(&value)
102        {
103            self = self.insert_header((HeaderName::from_static("sentry-trace"), header_value));
104        }
105        self
106    }
107}
108
109/// Adds Sentry trace headers to outgoing reqwest requests.
110pub trait ReqwestTracingExt {
111    fn with_sentry_tracing(self) -> Self;
112}
113
114impl ReqwestTracingExt for RequestBuilder {
115    fn with_sentry_tracing(self) -> Self {
116        if let Some(value) = trace_header_value()
117            && let Ok(header_value) = reqwest::header::HeaderValue::from_str(&value)
118        {
119            return self.header(
120                reqwest::header::HeaderName::from_static("sentry-trace"),
121                header_value,
122            );
123        }
124        self
125    }
126}