1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use tracing::{Dispatch, Level};
use tracing_subscriber::prelude::*;

use crate::errors::prelude::*;

pub static GLOBAL_LOG: Lazy<Mutex<Option<GlobalLog>>> = Lazy::new(Mutex::default);

/// The global logger/tracer for stdout, file and full open telemetry. Works with the tracing crates (info!, debug!, warn!, error!) and span funcs and decorators.
///
/// [`GlobalLog::meter`] is also provided to create metrics, these aren't native to the tracing crate.
///
/// Unlike my other lang implementations, for file and stdout this uses a separate flow, they won't use metrics and only receive basic span information.
/// Simply because I did it first, plus the rust opentelemetry sdk is very new, so difficult to use and
/// the benefit of handling file and stdout through otlp is minimal.
///
/// Open telemetry support is opinionated: unencrypted/uncompressed output to a local grpc port, the intention is this is a otpl collector sidecar.
///
/// Examples:
///
/// Kitchen sink:
/// ```
/// use bitbazaar::logging::GlobalLog;
/// use tracing_subscriber::prelude::*;
/// use tracing::Level;
///
/// let temp_dir = tempfile::tempdir().unwrap();
/// let log = GlobalLog::builder()
///             .stdout(true, false)
///             .level_from(Level::DEBUG) // Debug and up for stdout, each defaults to INFO
///             .file("my_program.log", temp_dir)
///             .otlp(4317, "service-name", "0.1.0").level_from(Level::INFO)
///             .build().unwrap();
/// log.register_global()?; // Register it as the global sub, this can only be done once
/// ```
pub struct GlobalLog {
    /// Tracing dispatcher, needed to make the global logger.
    pub(crate) dispatch: Option<Dispatch>,

    // tracing_appender not included in wasm:
    #[cfg(not(target_arch = "wasm32"))]
    /// Need to store these guards, when they go out of scope the logging may stop.
    /// When made global these are hoisted into a static lazy var.
    pub(crate) _guards: Vec<tracing_appender::non_blocking::WorkerGuard>,

    #[cfg(any(feature = "opentelemetry-grpc", feature = "opentelemetry-http"))]
    pub(crate) otlp_providers: OtlpProviders,
}

#[cfg(any(feature = "opentelemetry-grpc", feature = "opentelemetry-http"))]
pub struct OtlpProviders {
    pub logger_provider: Option<opentelemetry_sdk::logs::LoggerProvider>,
    pub tracer_provider: Option<opentelemetry_sdk::trace::TracerProvider>,
    // Will always create one, dummy if not being initiated by user, to allow meter() to still work:
    pub meter_provider: opentelemetry_sdk::metrics::MeterProvider,
}

impl GlobalLog {
    /// Create a builder to configure the global logger.
    pub fn builder() -> super::builder::GlobalLogBuilder {
        super::builder::GlobalLogBuilder::default()
    }

    /// A managed wrapper on creation of the GlobalLog and registering it as the global logger.
    ///
    /// Sets up console logging only. Should only be used for quick logging, as an example and testing.
    pub fn setup_quick_stdout_global_logging(level_from: Level) -> Result<(), AnyErr> {
        GlobalLog::builder()
            .stdout(true, false)
            .level_from(level_from)?
            .build()?
            .register_global()?;
        Ok(())
    }

    /// Register the logger as the global logger/tracer/metric manager, can only be done once during the lifetime of the program.
    ///
    /// If you need temporary globality, use the [`GlobalLog::with_tmp_global`] method.
    pub fn register_global(mut self) -> Result<(), AnyErr> {
        if let Some(dispatch) = self.dispatch.take() {
            // Make it global:
            GLOBAL_LOG.lock().replace(self);
            dispatch.init();
            Ok(())
        } else {
            Err(anyerr!("Already registered!"))
        }
    }

    #[cfg(any(feature = "opentelemetry-grpc", feature = "opentelemetry-http"))]
    /// See [`super::global_fns::meter`]`
    pub fn meter(
        &self,
        name: impl Into<std::borrow::Cow<'static, str>>,
    ) -> Result<opentelemetry::metrics::Meter, AnyErr> {
        use opentelemetry::metrics::MeterProvider;

        Ok(self.otlp_providers.meter_provider.meter(name))
    }

    #[cfg(any(feature = "opentelemetry-grpc", feature = "opentelemetry-http"))]
    /// See [`super::global_fns::set_span_parent_from_http_headers`]`
    pub fn set_span_parent_from_http_headers(
        &self,
        span: &tracing::Span,
        headers: &http::HeaderMap,
    ) -> Result<(), AnyErr> {
        use tracing_opentelemetry::OpenTelemetrySpanExt;

        use crate::log::global_log::http_headers::HeaderExtractor;

        let ctx_extractor = HeaderExtractor(headers);
        let ctx = opentelemetry::global::get_text_map_propagator(|propagator| {
            propagator.extract(&ctx_extractor)
        });
        span.set_parent(ctx);
        Ok(())
    }

    #[cfg(any(feature = "opentelemetry-grpc", feature = "opentelemetry-http"))]
    /// See [`super::global_fns::set_response_headers_from_ctx`]`
    pub fn set_response_headers_from_ctx<B>(
        &self,
        response: &mut http::Response<B>,
    ) -> Result<(), AnyErr> {
        use tracing_opentelemetry::OpenTelemetrySpanExt;

        use crate::log::global_log::http_headers::HeaderInjector;

        let ctx = tracing::Span::current().context();
        let mut injector = HeaderInjector(response.headers_mut());
        opentelemetry::global::get_text_map_propagator(|propagator| {
            propagator.inject_context(&ctx, &mut injector);
        });
        Ok(())
    }

    /// Temporarily make the logger global, for the duration of the given closure.
    ///
    /// If you want to make the logger global permanently, use the [`GlobalLog::register_global`] method.
    pub fn with_tmp_global<T>(&self, f: impl FnOnce() -> T) -> Result<T, AnyErr> {
        if let Some(dispatch) = &self.dispatch.as_ref() {
            Ok(tracing::dispatcher::with_default(dispatch, f))
        } else {
            Err(anyerr!("GlobalLog missing internal dispatch object! Remember the dispatcher is taken during the register_global() method and cannot be reused."))
        }
    }

    /// See [`super::global_fns::flush`]`
    pub fn flush(&self) -> Result<(), AnyErr> {
        #[cfg(any(feature = "opentelemetry-grpc", feature = "opentelemetry-http"))]
        {
            if let Some(prov) = &self.otlp_providers.logger_provider {
                prov.force_flush();
            }
            if let Some(prov) = &self.otlp_providers.tracer_provider {
                prov.force_flush();
            }
            self.otlp_providers
                .meter_provider
                .force_flush()
                .change_context(AnyErr)?;
        }
        Ok(())
    }

    /// See [`super::global_fns::shutdown`]`
    pub fn shutdown(&mut self) -> Result<(), AnyErr> {
        #[cfg(any(feature = "opentelemetry-grpc", feature = "opentelemetry-http"))]
        {
            if let Some(prov) = &mut self.otlp_providers.logger_provider {
                prov.shutdown();
            }
            if let Some(prov) = &self.otlp_providers.tracer_provider {
                // Doesn't have a shutdown interface.
                prov.force_flush();
            }
            self.otlp_providers
                .meter_provider
                .shutdown()
                .change_context(AnyErr)?;
        }
        Ok(())
    }
}