1use std::ffi::OsString;
16use std::fmt;
17use std::sync::Arc;
18use std::sync::Mutex;
19use std::sync::RwLock;
20
21use serde::Deserialize;
22use serde::Serialize;
23use tracing_subscriber::EnvFilter;
24
25use crate::ObservabilityError;
26use crate::ReloadConfig;
27
28pub const DEFAULT_LOG_FILTER: &str = "info";
29
30pub fn read_rust_log() -> Result<Option<String>, ObservabilityError> {
33 std::env::var_os("RUST_LOG").map(decode_environment_filter).transpose()
34}
35
36fn decode_environment_filter(value: OsString) -> Result<String, ObservabilityError> {
37 value
38 .into_string()
39 .map_err(|_| ObservabilityError::invalid_config("RUST_LOG contains non-Unicode data"))
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum LogFilterSource {
45 Runtime,
46 Cli,
47 Env,
48 Config,
49 Default,
50}
51
52impl fmt::Display for LogFilterSource {
53 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54 formatter.write_str(match self {
55 Self::Runtime => "runtime",
56 Self::Cli => "cli",
57 Self::Env => "env",
58 Self::Config => "config",
59 Self::Default => "default",
60 })
61 }
62}
63
64impl LogFilterSource {
65 pub const fn as_str(self) -> &'static str {
66 match self {
67 Self::Runtime => "runtime",
68 Self::Cli => "cli",
69 Self::Env => "env",
70 Self::Config => "config",
71 Self::Default => "default",
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, Default)]
77pub struct LogFilterInputs<'a> {
78 pub runtime: Option<&'a str>,
79 pub cli: Option<&'a str>,
80 pub environment: Option<&'a str>,
81 pub config: Option<&'a str>,
82 pub legacy_config: Option<&'a str>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
86pub struct ResolvedLogFilter {
87 filter: String,
88 source: LogFilterSource,
89}
90
91impl ResolvedLogFilter {
92 pub fn filter(&self) -> &str {
93 &self.filter
94 }
95
96 pub const fn source(&self) -> LogFilterSource {
97 self.source
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct LogFilterReloadRequest {
103 filter: String,
104}
105
106impl LogFilterReloadRequest {
107 pub fn new(filter: impl Into<String>) -> Self {
108 Self { filter: filter.into() }
109 }
110
111 pub fn filter(&self) -> &str {
112 &self.filter
113 }
114}
115
116type ReloadOperation = dyn Fn(EnvFilter) -> Result<(), ObservabilityError> + Send + Sync + 'static;
117
118struct LogFilterHandleInner {
119 current: RwLock<ResolvedLogFilter>,
120 reload: Box<ReloadOperation>,
121 operation_lock: Mutex<()>,
122 metrics: crate::metrics::log_filter::LogFilterMetrics,
123}
124
125#[derive(Clone)]
126pub struct LogFilterHandle {
127 inner: Arc<LogFilterHandleInner>,
128}
129
130impl LogFilterHandle {
131 pub(crate) fn new<F>(
132 initial: ResolvedLogFilter,
133 metrics: crate::metrics::log_filter::LogFilterMetrics,
134 reload: F,
135 ) -> Self
136 where
137 F: Fn(EnvFilter) -> Result<(), ObservabilityError> + Send + Sync + 'static,
138 {
139 Self {
140 inner: Arc::new(LogFilterHandleInner {
141 metrics,
142 current: RwLock::new(initial),
143 reload: Box::new(reload),
144 operation_lock: Mutex::new(()),
145 }),
146 }
147 }
148
149 pub fn current(&self) -> ResolvedLogFilter {
150 self.inner
151 .current
152 .read()
153 .unwrap_or_else(|poisoned| poisoned.into_inner())
154 .clone()
155 }
156
157 pub fn reload(&self, request: LogFilterReloadRequest) -> Result<ResolvedLogFilter, ObservabilityError> {
158 let resolved = LogFilterResolver::resolve(LogFilterInputs {
159 runtime: Some(request.filter()),
160 ..LogFilterInputs::default()
161 });
162 let resolved = match resolved {
163 Ok(resolved) => resolved,
164 Err(error) => {
165 self.inner.metrics.record_reload(false, LogFilterSource::Runtime);
166 return Err(error);
167 }
168 };
169 self.apply_resolved(resolved)
170 }
171
172 pub fn restore(&self, baseline: &ResolvedLogFilter) -> Result<ResolvedLogFilter, ObservabilityError> {
175 self.apply_resolved(baseline.clone())
176 }
177
178 fn apply_resolved(&self, resolved: ResolvedLogFilter) -> Result<ResolvedLogFilter, ObservabilityError> {
179 let filter = EnvFilter::try_new(resolved.filter())
180 .map_err(|error| ObservabilityError::invalid_log_filter(resolved.filter(), error))?;
181 let _operation = self
182 .inner
183 .operation_lock
184 .lock()
185 .unwrap_or_else(|poisoned| poisoned.into_inner());
186 let previous = self.current();
187 if let Err(error) = (self.inner.reload)(filter) {
188 self.inner.metrics.record_reload(false, resolved.source());
189 return Err(error);
190 }
191 *self
192 .inner
193 .current
194 .write()
195 .unwrap_or_else(|poisoned| poisoned.into_inner()) = resolved.clone();
196 self.inner.metrics.record_reload(true, resolved.source());
197 self.inner
198 .metrics
199 .set_active(Some(previous.source()), resolved.source());
200 Ok(resolved)
201 }
202
203 pub fn set_expiry_timestamp(&self, timestamp_seconds: u64) {
205 self.inner.metrics.set_expiry_timestamp(timestamp_seconds);
206 }
207
208 pub fn record_audit_failure(&self) {
210 self.inner.metrics.record_audit_failure();
211 }
212
213 pub fn record_auto_restore_failure(&self) {
215 self.inner.metrics.record_auto_restore_failure();
216 }
217
218 pub fn record_rollback_failure(&self) {
220 self.inner.metrics.record_rollback_failure();
221 }
222}
223
224#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
225#[serde(default)]
226pub struct LoggingOverrides {
227 pub logging: LoggingOverrideConfig,
228 #[serde(rename = "logFilter", alias = "log_filter")]
229 pub log_filter: Option<String>,
230}
231
232#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
233#[serde(default)]
234pub struct LoggingOverrideConfig {
235 pub filter: Option<String>,
236 pub reload: ReloadConfig,
237}
238
239pub struct LogFilterResolver;
240
241impl LogFilterResolver {
242 pub fn resolve(inputs: LogFilterInputs<'_>) -> Result<ResolvedLogFilter, ObservabilityError> {
243 let config = normalized_config_filter(inputs.config, inputs.legacy_config)?;
244 let (candidate, source) = inputs
245 .runtime
246 .map(|value| (value, LogFilterSource::Runtime))
247 .or_else(|| inputs.cli.map(|value| (value, LogFilterSource::Cli)))
248 .or_else(|| inputs.environment.map(|value| (value, LogFilterSource::Env)))
249 .or_else(|| config.map(|value| (value, LogFilterSource::Config)))
250 .unwrap_or((DEFAULT_LOG_FILTER, LogFilterSource::Default));
251 let candidate = candidate.trim();
252 if candidate.is_empty() {
253 return Err(ObservabilityError::invalid_log_filter_detail(
254 candidate,
255 "log filter must not be blank",
256 ));
257 }
258
259 EnvFilter::try_new(candidate).map_err(|error| ObservabilityError::invalid_log_filter(candidate, error))?;
260 Ok(ResolvedLogFilter {
261 filter: candidate.to_string(),
262 source,
263 })
264 }
265}
266
267fn normalized_config_filter<'a>(
268 config: Option<&'a str>,
269 legacy_config: Option<&'a str>,
270) -> Result<Option<&'a str>, ObservabilityError> {
271 match (config, legacy_config) {
272 (Some(config), Some(legacy)) if config.trim() != legacy.trim() => Err(ObservabilityError::invalid_config(
273 "logging.filter conflicts with the Java-compatible logFilter value",
274 )),
275 (Some(config), _) => Ok(Some(config)),
276 (None, Some(legacy)) => Ok(Some(legacy)),
277 (None, None) => Ok(None),
278 }
279}
280
281#[cfg(all(test, unix))]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn non_unicode_environment_filter_is_rejected() {
287 use std::os::unix::ffi::OsStringExt;
288
289 let error = decode_environment_filter(OsString::from_vec(vec![0xff]))
290 .expect_err("non-Unicode environment values must fail closed");
291
292 assert_eq!(error.code(), rocketmq_error::OBSERVABILITY_CONFIGURATION_INVALID.code());
293 }
294}