Skip to main content

objectstore_options/
lib.rs

1//! Runtime options for Objectstore, backed by [`sentry-options`].
2//!
3//! See the [`Options`] struct for details and usage instructions.
4//!
5//! [`sentry-options`]: https://crates.io/crates/sentry-options
6
7use std::collections::BTreeMap;
8
9use objectstore_typed_options::SentryOptions;
10use objectstore_types::usecase::Usecase;
11use serde::{Deserialize, Serialize};
12
13pub use objectstore_typed_options::Error;
14
15/// Initializes options and spawns a background task that refreshes them periodically.
16///
17/// The refresh interval is 4 seconds, chosen to stay under the 5-second staleness
18/// threshold built into `sentry-options`.
19pub fn init() -> Result<(), Error> {
20    Options::init()?;
21
22    tokio::spawn(async {
23        let mut interval = tokio::time::interval(std::time::Duration::from_secs(4));
24        loop {
25            interval.tick().await;
26
27            // `spawn_blocking` propagates panics as a JoinError. No need to log them, since the
28            // global panic hook does that already.
29            let result = tokio::task::spawn_blocking(Options::refresh).await;
30            if let Ok(Err(ref err)) = result {
31                objectstore_log::error!(!!err, "failed to refresh options");
32            }
33        }
34    });
35
36    Ok(())
37}
38
39/// Runtime options for Objectstore, loaded from sentry-options.
40///
41/// Obtain a snapshot of the current options via [`Options::get`]. Before calling `get`,
42/// the global instance must be initialized with [`Options::init`].
43#[derive(Debug, SentryOptions)]
44#[sentry_options(namespace = "objectstore", path = "../../sentry-options")]
45pub struct Options {
46    /// Active killswitches that may disable access to specific object contexts.
47    killswitches: Vec<Killswitch>,
48}
49
50impl Options {
51    /// Returns the list of active killswitches.
52    pub fn killswitches(&self) -> &[Killswitch] {
53        &self.killswitches
54    }
55}
56
57/// A killswitch that may disable access to certain object contexts.
58///
59/// Note that at least one of the fields should be set, or else the killswitch will match all
60/// contexts and discard all requests.
61#[derive(Debug, Deserialize, Serialize, PartialEq)]
62pub struct Killswitch {
63    /// Optional validated usecase name to match exactly.
64    ///
65    /// If `None`, matches any usecase.
66    #[serde(default)]
67    pub usecase: Option<Usecase>,
68
69    /// Scopes to match.
70    ///
71    /// If empty, matches any scopes. Additional scopes in the context are ignored, so a killswitch
72    /// matches if all of the specified scopes are present in the request with matching values.
73    #[serde(default)]
74    pub scopes: BTreeMap<String, String>,
75
76    /// Optional service glob pattern to match.
77    ///
78    /// If `None`, matches any service (or absence of service header).
79    /// If specified, the request must have a matching `x-downstream-service` header. The header
80    /// value is normalized before matching: any trailing Kubernetes ReplicaSet hash and pod
81    /// suffix are stripped, so patterns should match the base service name (e.g. `relay*`, not
82    /// `relay-7d8f9c5b6d-*`).
83    #[serde(default)]
84    pub service: Option<String>,
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    // Required for schema validation.
92    #[cfg(not(feature = "testing"))]
93    compile_error!("tests require the `testing` feature: run with `--features testing`");
94
95    #[test]
96    fn schema_is_valid() {
97        let _ = Options::get();
98    }
99
100    #[test]
101    fn killswitch_deserialization_validates_usecase() {
102        let switch: Killswitch = serde_json::from_str(r#"{"usecase":"Debug_Files"}"#).unwrap();
103        assert_eq!(switch.usecase.as_ref().unwrap().as_str(), "Debug_Files");
104        let serialized = serde_json::to_value(&switch).unwrap();
105        assert_eq!(serialized["usecase"], "Debug_Files");
106
107        let wildcard: Killswitch = serde_json::from_str("{}").unwrap();
108        assert!(wildcard.usecase.is_none());
109        assert!(serde_json::from_str::<Killswitch>(r#"{"usecase":"test/name"}"#).is_err());
110    }
111}