Skip to main content

rocketmq_observability/
error.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Catalog-backed error surface for observability bootstrap and shutdown.
16
17use std::backtrace::Backtrace;
18use std::error::Error as StdError;
19use std::fmt;
20use std::panic::Location;
21use std::sync::Arc;
22
23use rocketmq_error::fields;
24use rocketmq_error::CanonicalCondition;
25use rocketmq_error::DiagnosticView;
26use rocketmq_error::Error as CanonicalError;
27use rocketmq_error::ErrorCode;
28use rocketmq_error::ErrorContext;
29use rocketmq_error::ErrorDescriptor;
30use rocketmq_error::PublicErrorView;
31use rocketmq_error::RecoveryHint;
32use rocketmq_error::SharedError;
33use thiserror::Error;
34
35/// Result returned by observability operations.
36pub type ObservabilityResult<T> = std::result::Result<T, ObservabilityError>;
37
38/// The closed observability operation retained outside catalog policy.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ObservabilityOperation {
41    /// A requested Cargo feature is disabled.
42    FeatureDisabled,
43    /// Observability configuration validation.
44    ValidateConfiguration,
45    /// Metrics initialization.
46    InitializeMetrics,
47    /// Trace initialization.
48    InitializeTraces,
49    /// Log initialization.
50    InitializeLogs,
51    /// Local tracing-subscriber initialization.
52    InitializeSubscriber,
53    /// Log-filter validation.
54    ValidateLogFilter,
55    /// Global subscriber installation.
56    InstallSubscriber {
57        /// Whether installation was attempted.
58        attempted: bool,
59        /// Whether installation succeeded.
60        installed: bool,
61    },
62    /// Metrics shutdown.
63    ShutdownMetrics,
64    /// Trace shutdown.
65    ShutdownTraces,
66    /// Log shutdown.
67    ShutdownLogs,
68}
69
70/// A string-only diagnostic retained as a typed source.
71///
72/// The canonical error never renders this value. Code that deliberately walks
73/// the source chain may inspect it for local diagnostics.
74#[derive(Debug, Error)]
75#[error("observability failure detail is available")]
76pub struct ObservabilityFailureDetail {
77    detail: String,
78}
79
80impl ObservabilityFailureDetail {
81    /// Returns the original diagnostic detail.
82    #[must_use]
83    pub fn detail(&self) -> &str {
84        &self.detail
85    }
86}
87
88/// Opaque observability error backed by one canonical shared error.
89#[derive(Clone)]
90pub struct ObservabilityError {
91    error: SharedError,
92    operation: ObservabilityOperation,
93}
94
95impl ObservabilityError {
96    #[track_caller]
97    fn source_free(
98        descriptor: &'static ErrorDescriptor,
99        operation: ObservabilityOperation,
100        context: ErrorContext,
101    ) -> Self {
102        Self {
103            error: Arc::new(CanonicalError::new(descriptor).with_context(context)),
104            operation,
105        }
106    }
107
108    #[track_caller]
109    fn caused_by(
110        descriptor: &'static ErrorDescriptor,
111        operation: ObservabilityOperation,
112        context: ErrorContext,
113        source: impl StdError + Send + Sync + 'static,
114    ) -> Self {
115        Self {
116            error: Arc::new(CanonicalError::caused_by(descriptor, source).with_context(context)),
117            operation,
118        }
119    }
120
121    #[track_caller]
122    fn with_detail(
123        descriptor: &'static ErrorDescriptor,
124        operation: ObservabilityOperation,
125        context: ErrorContext,
126        detail: impl ToString,
127    ) -> Self {
128        Self::caused_by(
129            descriptor,
130            operation,
131            context,
132            ObservabilityFailureDetail {
133                detail: detail.to_string(),
134            },
135        )
136    }
137
138    /// Creates a failure for a requested observability feature that is disabled.
139    #[must_use]
140    #[track_caller]
141    pub fn feature_disabled(feature: &'static str) -> Self {
142        Self::source_free(
143            &rocketmq_error::OBSERVABILITY_FEATURE_DISABLED,
144            ObservabilityOperation::FeatureDisabled,
145            ErrorContext::new().with_text(fields::FEATURE, feature),
146        )
147    }
148
149    /// Creates an invalid-configuration failure.
150    #[must_use]
151    #[track_caller]
152    pub fn invalid_config(detail: impl Into<String>) -> Self {
153        Self::with_detail(
154            &rocketmq_error::OBSERVABILITY_CONFIGURATION_INVALID,
155            ObservabilityOperation::ValidateConfiguration,
156            ErrorContext::new().with_secret_presence(fields::REASON_PRESENT),
157            detail.into(),
158        )
159    }
160
161    /// Creates a metrics-initialization failure from display-only detail.
162    #[must_use]
163    #[track_caller]
164    pub fn metrics_init(detail: impl ToString) -> Self {
165        Self::initialization_detail(ObservabilityOperation::InitializeMetrics, "metrics", detail)
166    }
167
168    /// Creates a metrics-initialization failure while retaining its typed source.
169    #[must_use]
170    #[track_caller]
171    pub fn metrics_init_source(source: impl StdError + Send + Sync + 'static) -> Self {
172        Self::initialization_source(ObservabilityOperation::InitializeMetrics, "metrics", source)
173    }
174
175    /// Creates a trace-initialization failure from display-only detail.
176    #[must_use]
177    #[track_caller]
178    pub fn traces_init(detail: impl ToString) -> Self {
179        Self::initialization_detail(ObservabilityOperation::InitializeTraces, "traces", detail)
180    }
181
182    /// Creates a trace-initialization failure while retaining its typed source.
183    #[must_use]
184    #[track_caller]
185    pub fn traces_init_source(source: impl StdError + Send + Sync + 'static) -> Self {
186        Self::initialization_source(ObservabilityOperation::InitializeTraces, "traces", source)
187    }
188
189    /// Creates a log-initialization failure from display-only detail.
190    #[must_use]
191    #[track_caller]
192    pub fn logs_init(detail: impl ToString) -> Self {
193        Self::initialization_detail(ObservabilityOperation::InitializeLogs, "logs", detail)
194    }
195
196    /// Creates a log-initialization failure while retaining its typed source.
197    #[must_use]
198    #[track_caller]
199    pub fn logs_init_source(source: impl StdError + Send + Sync + 'static) -> Self {
200        Self::initialization_source(ObservabilityOperation::InitializeLogs, "logs", source)
201    }
202
203    /// Creates a subscriber-initialization failure from display-only detail.
204    #[must_use]
205    #[track_caller]
206    pub fn logging_init(detail: impl ToString) -> Self {
207        Self::initialization_detail(ObservabilityOperation::InitializeSubscriber, "subscriber", detail)
208    }
209
210    /// Creates a subscriber-initialization failure while retaining its typed source.
211    #[must_use]
212    #[track_caller]
213    pub fn logging_init_source(source: impl StdError + Send + Sync + 'static) -> Self {
214        Self::initialization_source(ObservabilityOperation::InitializeSubscriber, "subscriber", source)
215    }
216
217    #[track_caller]
218    fn initialization_detail(operation: ObservabilityOperation, signal: &'static str, detail: impl ToString) -> Self {
219        Self::with_detail(
220            &rocketmq_error::OBSERVABILITY_INITIALIZATION_FAILED,
221            operation,
222            observability_failure_context(signal),
223            detail,
224        )
225    }
226
227    #[track_caller]
228    fn initialization_source(
229        operation: ObservabilityOperation,
230        signal: &'static str,
231        source: impl StdError + Send + Sync + 'static,
232    ) -> Self {
233        Self::caused_by(
234            &rocketmq_error::OBSERVABILITY_INITIALIZATION_FAILED,
235            operation,
236            observability_failure_context(signal),
237            source,
238        )
239    }
240
241    /// Creates an invalid-log-filter failure while retaining the parser source.
242    #[must_use]
243    #[track_caller]
244    pub fn invalid_log_filter(_filter: impl Into<String>, source: impl StdError + Send + Sync + 'static) -> Self {
245        Self::caused_by(
246            &rocketmq_error::OBSERVABILITY_LOG_FILTER_INVALID,
247            ObservabilityOperation::ValidateLogFilter,
248            ErrorContext::new()
249                .with_secret_presence(fields::FILTER_PRESENT)
250                .with_secret_presence(fields::ERROR_PRESENT),
251            source,
252        )
253    }
254
255    /// Creates an invalid-log-filter failure from display-only detail.
256    #[must_use]
257    #[track_caller]
258    pub fn invalid_log_filter_detail(_filter: impl Into<String>, detail: impl ToString) -> Self {
259        Self::with_detail(
260            &rocketmq_error::OBSERVABILITY_LOG_FILTER_INVALID,
261            ObservabilityOperation::ValidateLogFilter,
262            ErrorContext::new()
263                .with_secret_presence(fields::FILTER_PRESENT)
264                .with_secret_presence(fields::ERROR_PRESENT),
265            detail,
266        )
267    }
268
269    /// Creates a subscriber-installation failure.
270    #[must_use]
271    #[track_caller]
272    pub fn subscriber_install_failed(attempted: bool, installed: bool) -> Self {
273        Self::source_free(
274            &rocketmq_error::OBSERVABILITY_SUBSCRIBER_INSTALLATION_FAILED,
275            ObservabilityOperation::InstallSubscriber { attempted, installed },
276            ErrorContext::new()
277                .with_bool(fields::ATTEMPTED, attempted)
278                .with_bool(fields::INSTALLED, installed),
279        )
280    }
281
282    /// Creates a metrics-shutdown failure from display-only detail.
283    #[must_use]
284    #[track_caller]
285    pub fn metrics_shutdown(detail: impl ToString) -> Self {
286        Self::shutdown_detail(ObservabilityOperation::ShutdownMetrics, "metrics", detail)
287    }
288
289    /// Creates a trace-shutdown failure from display-only detail.
290    #[must_use]
291    #[track_caller]
292    pub fn traces_shutdown(detail: impl ToString) -> Self {
293        Self::shutdown_detail(ObservabilityOperation::ShutdownTraces, "traces", detail)
294    }
295
296    /// Creates a log-shutdown failure from display-only detail.
297    #[must_use]
298    #[track_caller]
299    pub fn logs_shutdown(detail: impl ToString) -> Self {
300        Self::shutdown_detail(ObservabilityOperation::ShutdownLogs, "logs", detail)
301    }
302
303    #[track_caller]
304    fn shutdown_detail(operation: ObservabilityOperation, signal: &'static str, detail: impl ToString) -> Self {
305        Self::with_detail(
306            &rocketmq_error::OBSERVABILITY_SHUTDOWN_FAILED,
307            operation,
308            observability_failure_context(signal),
309            detail,
310        )
311    }
312
313    /// Returns the catalog descriptor that owns identity and policy.
314    #[must_use]
315    pub fn descriptor(&self) -> &'static ErrorDescriptor {
316        self.error.descriptor()
317    }
318
319    /// Returns the stable catalog code.
320    #[must_use]
321    pub fn code(&self) -> ErrorCode {
322        self.error.code()
323    }
324
325    /// Returns the descriptor-owned canonical condition.
326    #[must_use]
327    pub fn condition(&self) -> CanonicalCondition {
328        self.error.condition()
329    }
330
331    /// Returns the descriptor-owned recovery hint.
332    #[must_use]
333    pub fn recovery_hint(&self) -> RecoveryHint {
334        self.error.recovery_hint()
335    }
336
337    /// Returns the closed observability operation.
338    #[must_use]
339    pub const fn operation(&self) -> ObservabilityOperation {
340        self.operation
341    }
342
343    /// Returns descriptor-validated bounded context.
344    #[must_use]
345    pub fn context(&self) -> &ErrorContext {
346        self.error.context()
347    }
348
349    /// Returns the first canonical promotion location.
350    #[must_use]
351    pub fn location(&self) -> &'static Location<'static> {
352        self.error.location()
353    }
354
355    /// Returns the catalog-controlled backtrace, when captured.
356    #[must_use]
357    pub fn backtrace(&self) -> Option<&Backtrace> {
358        self.error.backtrace()
359    }
360
361    /// Creates a descriptor-validated public projection.
362    ///
363    /// # Errors
364    ///
365    /// Returns an error only if the facade built context that violates its
366    /// selected descriptor.
367    pub fn public_view(&self) -> Result<PublicErrorView<'_>, rocketmq_error::ViewContextViolation> {
368        self.error.public_view()
369    }
370
371    /// Creates a descriptor-validated diagnostic projection.
372    ///
373    /// # Errors
374    ///
375    /// Returns an error only if the facade built context that violates its
376    /// selected descriptor.
377    pub fn diagnostic_view(&self) -> Result<DiagnosticView<'_>, rocketmq_error::ViewContextViolation> {
378        self.error.diagnostic_view()
379    }
380}
381
382fn observability_failure_context(signal: &'static str) -> ErrorContext {
383    ErrorContext::new()
384        .with_text(fields::OBSERVABILITY_SIGNAL, signal)
385        .with_secret_presence(fields::REASON_PRESENT)
386}
387
388impl fmt::Display for ObservabilityError {
389    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
390        fmt::Display::fmt(self.error.as_ref(), formatter)
391    }
392}
393
394impl fmt::Debug for ObservabilityError {
395    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
396        formatter
397            .debug_struct("ObservabilityError")
398            .field("code", &self.code())
399            .field("condition", &self.condition())
400            .field("operation", &self.operation)
401            .field("has_source", &self.error.source().is_some())
402            .finish()
403    }
404}
405
406impl StdError for ObservabilityError {
407    fn source(&self) -> Option<&(dyn StdError + 'static)> {
408        self.error.source()
409    }
410}