minco_plugin_observability/
lib.rs1#![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}