Skip to main content

minco_plugin_observability/
lib.rs

1//! Default structured-observability plugin.
2#![forbid(unsafe_code)]
3
4use minco_core::{
5    CapabilityProvision, ConfigurationField, ConfigurationValueKind, Plugin, PluginContext,
6    PluginDescriptor, PluginError, PluginId, PluginStability,
7};
8use semver::{Version, VersionReq};
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11use tracing_subscriber::EnvFilter;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(default)]
15pub struct ObservabilityConfig {
16    pub service_name: String,
17    pub json: bool,
18    pub default_filter: String,
19}
20
21#[derive(Debug, thiserror::Error)]
22pub enum ObservabilityError {
23    #[error("failed to initialize tracing subscriber: {0}")]
24    Initialization(String),
25}
26
27impl Default for ObservabilityConfig {
28    fn default() -> Self {
29        Self {
30            service_name: "minco-app".into(),
31            json: true,
32            default_filter: "info,tower_http=info".into(),
33        }
34    }
35}
36
37impl ObservabilityConfig {
38    pub fn init(&self) -> Result<(), ObservabilityError> {
39        let filter = EnvFilter::try_from_default_env()
40            .unwrap_or_else(|_| EnvFilter::new(&self.default_filter));
41        let result = if self.json {
42            tracing_subscriber::fmt()
43                .with_env_filter(filter)
44                .json()
45                .try_init()
46        } else {
47            tracing_subscriber::fmt().with_env_filter(filter).try_init()
48        };
49        result.map_err(|error| ObservabilityError::Initialization(error.to_string()))
50    }
51}
52
53#[derive(Debug, Clone)]
54pub struct ObservabilityPlugin {
55    config: ObservabilityConfig,
56}
57
58impl ObservabilityPlugin {
59    pub const fn new(config: ObservabilityConfig) -> Self {
60        Self { config }
61    }
62}
63
64impl Default for ObservabilityPlugin {
65    fn default() -> Self {
66        Self::new(ObservabilityConfig::default())
67    }
68}
69
70impl Plugin for ObservabilityPlugin {
71    fn descriptor(&self) -> PluginDescriptor {
72        let mut descriptor = PluginDescriptor::new(
73            PluginId::new("observability").expect("static id"),
74            Version::new(1, 0, 0),
75            "Structured tracing and CloudWatch-compatible JSON logging",
76        );
77        descriptor.core_compatibility =
78            VersionReq::parse(concat!("^", env!("CARGO_PKG_VERSION"))).expect("package version");
79        descriptor.stability = PluginStability::Stable;
80        descriptor.documentation = Some("https://docs.rs/minco-plugin-observability".into());
81        descriptor.default_enabled = true;
82        descriptor.provides.push(CapabilityProvision {
83            name: "observability.tracing".into(),
84            version: Version::new(1, 0, 0),
85        });
86        descriptor.configuration.extend([
87            ConfigurationField {
88                key: "service_name".into(),
89                kind: ConfigurationValueKind::String,
90                required: false,
91                secret: false,
92                description: "Stable service name included in operational telemetry".into(),
93                default: Some(serde_json::json!(self.config.service_name)),
94            },
95            ConfigurationField {
96                key: "json".into(),
97                kind: ConfigurationValueKind::Boolean,
98                required: false,
99                secret: false,
100                description: "Emit structured JSON suitable for CloudWatch Logs".into(),
101                default: Some(serde_json::json!(self.config.json)),
102            },
103            ConfigurationField {
104                key: "default_filter".into(),
105                kind: ConfigurationValueKind::String,
106                required: false,
107                secret: false,
108                description: "Fallback tracing filter when RUST_LOG is unset".into(),
109                default: Some(serde_json::json!(self.config.default_filter)),
110            },
111        ]);
112        descriptor
113    }
114
115    fn install(&self, context: &mut PluginContext<'_>) -> Result<(), PluginError> {
116        let config = context.configuration::<ObservabilityConfig>()?;
117        context.services().insert(Arc::new(config))?;
118        Ok(())
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use minco_core::{PluginManager, PluginSelection};
126
127    #[test]
128    fn runtime_selection_overrides_constructor_defaults() {
129        let mut manager = PluginManager::default();
130        manager
131            .register(ObservabilityPlugin::new(ObservabilityConfig {
132                service_name: "constructor".into(),
133                json: true,
134                default_filter: "info".into(),
135            }))
136            .unwrap();
137        let id = PluginId::new("observability").unwrap();
138        let mut selection = PluginSelection::default();
139        selection
140            .configuration
141            .insert(id, serde_json::json!({ "service_name": "runtime" }));
142
143        let application = manager.compose(&selection).unwrap();
144        let config = application.services.get::<ObservabilityConfig>().unwrap();
145        assert_eq!(config.service_name, "runtime");
146        assert!(config.json);
147        assert_eq!(config.default_filter, "info");
148    }
149}