Skip to main content

common_telemetry/
logging.rs

1// Copyright 2023 Greptime Team
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//! logging stuffs, inspired by databend
16mod file_retention;
17
18use std::collections::HashMap;
19use std::env;
20use std::io::IsTerminal;
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::{Arc, Mutex, Once};
23use std::time::Duration;
24
25use common_base::readable_size::ReadableSize;
26use common_base::serde::empty_string_as_default;
27use file_retention::{DirectoryRetention, LogFileKind, build_file_appender};
28use once_cell::sync::{Lazy, OnceCell};
29use opentelemetry::trace::TracerProvider;
30use opentelemetry::{KeyValue, global};
31use opentelemetry_otlp::{Protocol, SpanExporter, WithExportConfig, WithHttpConfig};
32use opentelemetry_sdk::propagation::TraceContextPropagator;
33use opentelemetry_sdk::trace::{Sampler, Tracer};
34use opentelemetry_semantic_conventions::resource;
35use serde::{Deserialize, Serialize};
36use tracing::callsite;
37use tracing::metadata::LevelFilter;
38use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
39use tracing_log::LogTracer;
40use tracing_subscriber::filter::{FilterFn, Targets};
41use tracing_subscriber::fmt::Layer;
42use tracing_subscriber::layer::{Layered, SubscriberExt};
43use tracing_subscriber::prelude::*;
44use tracing_subscriber::{EnvFilter, Registry, filter};
45
46use crate::tracing_sampler::{TracingSampleOptions, create_sampler};
47
48/// The default endpoint when use gRPC exporter protocol.
49pub const DEFAULT_OTLP_GRPC_ENDPOINT: &str = "http://localhost:4317";
50
51/// The default endpoint when use HTTP exporter protocol.
52pub const DEFAULT_OTLP_HTTP_ENDPOINT: &str = "http://localhost:4318/v1/traces";
53
54/// The default logs directory.
55pub const DEFAULT_LOGGING_DIR: &str = "logs";
56
57/// Handle for reloading log level
58pub static LOG_RELOAD_HANDLE: OnceCell<tracing_subscriber::reload::Handle<Targets, Registry>> =
59    OnceCell::new();
60
61/// The independent dispatcher used by the `slow!` macro.
62#[doc(hidden)]
63pub static SLOW_QUERY_LOGGER: OnceCell<tracing::Dispatch> = OnceCell::new();
64
65type DynSubscriber = Layered<tracing_subscriber::reload::Layer<Targets, Registry>, Registry>;
66type OtelTraceLayer = tracing_opentelemetry::OpenTelemetryLayer<DynSubscriber, Tracer>;
67
68struct TraceLayerState {
69    enabled: AtomicBool,
70    layer: OnceCell<OtelTraceLayer>,
71}
72
73#[derive(Clone)]
74pub struct TraceReloadHandle {
75    inner: Arc<TraceLayerState>,
76}
77
78impl TraceReloadHandle {
79    fn new(inner: Arc<TraceLayerState>) -> Self {
80        Self { inner }
81    }
82
83    /// Enables or disables OTLP data collection, initializing it on first enable.
84    /// Disabling stops new spans, events, fields and links. Existing spans still
85    /// finish their lifecycle and export on close.
86    pub fn set_enabled(&self, enabled: bool) -> Result<(), &'static str> {
87        self.set_enabled_with(enabled, || {
88            get_or_init_tracer().map(|tracer| tracing_opentelemetry::layer().with_tracer(tracer))
89        })
90    }
91
92    fn set_enabled_with(
93        &self,
94        enabled: bool,
95        init: impl FnOnce() -> Result<OtelTraceLayer, &'static str>,
96    ) -> Result<(), &'static str> {
97        if enabled {
98            self.inner.layer.get_or_try_init(init)?;
99        }
100        self.inner.enabled.store(enabled, Ordering::Release);
101        callsite::rebuild_interest_cache();
102        Ok(())
103    }
104}
105
106/// An OTLP layer with a runtime switch and a stable address for downcasts.
107struct TraceLayer {
108    inner: Arc<TraceLayerState>,
109}
110
111impl TraceLayer {
112    fn new(initial: Option<OtelTraceLayer>) -> (Self, TraceReloadHandle) {
113        let inner = Arc::new(TraceLayerState {
114            enabled: AtomicBool::new(initial.is_some()),
115            layer: initial.map(OnceCell::with_value).unwrap_or_default(),
116        });
117        (
118            Self {
119                inner: inner.clone(),
120            },
121            TraceReloadHandle::new(inner),
122        )
123    }
124
125    fn with_layer<R>(&self, f: impl FnOnce(&OtelTraceLayer) -> R) -> Option<R> {
126        self.inner.layer.get().map(f)
127    }
128
129    fn is_enabled(&self) -> bool {
130        self.inner.enabled.load(Ordering::Acquire)
131    }
132}
133
134impl tracing_subscriber::Layer<DynSubscriber> for TraceLayer {
135    fn on_register_dispatch(&self, subscriber: &tracing::Dispatch) {
136        let _ = self.with_layer(|layer| layer.on_register_dispatch(subscriber));
137    }
138
139    fn register_callsite(
140        &self,
141        metadata: &'static tracing::Metadata<'static>,
142    ) -> tracing::subscriber::Interest {
143        self.with_layer(|layer| layer.register_callsite(metadata))
144            .unwrap_or_else(tracing::subscriber::Interest::always)
145    }
146
147    fn enabled(
148        &self,
149        metadata: &tracing::Metadata<'_>,
150        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
151    ) -> bool {
152        self.with_layer(|layer| layer.enabled(metadata, ctx))
153            .unwrap_or(true)
154    }
155
156    fn on_new_span(
157        &self,
158        attrs: &tracing::span::Attributes<'_>,
159        id: &tracing::span::Id,
160        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
161    ) {
162        if self.is_enabled() {
163            let _ = self.with_layer(|layer| layer.on_new_span(attrs, id, ctx));
164        }
165    }
166
167    fn max_level_hint(&self) -> Option<LevelFilter> {
168        self.with_layer(|layer| layer.max_level_hint()).flatten()
169    }
170
171    fn on_record(
172        &self,
173        span: &tracing::span::Id,
174        values: &tracing::span::Record<'_>,
175        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
176    ) {
177        if self.is_enabled() {
178            let _ = self.with_layer(|layer| layer.on_record(span, values, ctx));
179        }
180    }
181
182    fn on_follows_from(
183        &self,
184        span: &tracing::span::Id,
185        follows: &tracing::span::Id,
186        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
187    ) {
188        if self.is_enabled() {
189            let _ = self.with_layer(|layer| layer.on_follows_from(span, follows, ctx));
190        }
191    }
192
193    fn event_enabled(
194        &self,
195        event: &tracing::Event<'_>,
196        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
197    ) -> bool {
198        self.with_layer(|layer| layer.event_enabled(event, ctx))
199            .unwrap_or(true)
200    }
201
202    fn on_event(
203        &self,
204        event: &tracing::Event<'_>,
205        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
206    ) {
207        if self.is_enabled() {
208            let _ = self.with_layer(|layer| layer.on_event(event, ctx));
209        }
210    }
211
212    fn on_enter(
213        &self,
214        id: &tracing::span::Id,
215        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
216    ) {
217        let _ = self.with_layer(|layer| layer.on_enter(id, ctx));
218    }
219
220    fn on_exit(
221        &self,
222        id: &tracing::span::Id,
223        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
224    ) {
225        let _ = self.with_layer(|layer| layer.on_exit(id, ctx));
226    }
227
228    fn on_close(
229        &self,
230        id: tracing::span::Id,
231        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
232    ) {
233        let _ = self.with_layer(|layer| layer.on_close(id, ctx));
234    }
235
236    fn on_id_change(
237        &self,
238        old: &tracing::span::Id,
239        new: &tracing::span::Id,
240        ctx: tracing_subscriber::layer::Context<'_, DynSubscriber>,
241    ) {
242        let _ = self.with_layer(|layer| layer.on_id_change(old, new, ctx));
243    }
244
245    unsafe fn downcast_raw(&self, id: std::any::TypeId) -> Option<*const ()> {
246        // Keep downcasts available while disabled: an in-flight WithContext
247        // callback may still need the layer. OnceCell keeps both addresses valid
248        // for the subscriber's lifetime, even across concurrent toggles.
249        self.inner
250            .layer
251            .get()
252            .and_then(|layer| unsafe { layer.downcast_raw(id) })
253    }
254}
255
256/// Handle for reloading trace level
257pub static TRACE_RELOAD_HANDLE: OnceCell<TraceReloadHandle> = OnceCell::new();
258
259static TRACER: OnceCell<Mutex<TraceState>> = OnceCell::new();
260
261#[derive(Debug)]
262enum TraceState {
263    Ready(Tracer),
264    Deferred(TraceContext),
265}
266
267/// The logging options that used to initialize the logger.
268#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
269#[serde(default)]
270pub struct LoggingOptions {
271    /// The directory to store log files. If not set, logs will be written to stdout.
272    pub dir: String,
273
274    /// The log level that can be one of "trace", "debug", "info", "warn", "error". Default is "info".
275    pub level: Option<String>,
276
277    /// The log format that can be one of "json" or "text". Default is "text".
278    #[serde(default, deserialize_with = "empty_string_as_default")]
279    pub log_format: LogFormat,
280
281    /// The maximum number of log files set by default.
282    pub max_log_files: usize,
283
284    /// The maximum total size of managed log files in `dir`. Zero disables size-based retention.
285    pub max_log_dir_size: ReadableSize,
286
287    /// Whether to append logs to stdout. Default is true.
288    pub append_stdout: bool,
289
290    /// Whether to write logs to files in `dir`. Default is true.
291    pub enable_file_logging: bool,
292
293    /// Whether to enable tracing with OTLP. Default is false.
294    pub enable_otlp_tracing: bool,
295
296    /// The endpoint of OTLP.
297    pub otlp_endpoint: Option<String>,
298
299    /// The tracing sample ratio.
300    pub tracing_sample_ratio: Option<TracingSampleOptions>,
301
302    /// The protocol of OTLP export.
303    pub otlp_export_protocol: Option<OtlpExportProtocol>,
304
305    /// Additional HTTP headers for OTLP exporter.
306    #[serde(skip_serializing_if = "HashMap::is_empty")]
307    pub otlp_headers: HashMap<String, String>,
308
309    /// Whether to enable per-region metrics.
310    pub enable_per_region_metrics: bool,
311}
312
313/// The protocol of OTLP export.
314#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
315#[serde(rename_all = "snake_case")]
316pub enum OtlpExportProtocol {
317    /// GRPC protocol.
318    Grpc,
319
320    /// HTTP protocol with binary protobuf.
321    Http,
322}
323
324/// The options of slow query.
325#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
326#[serde(default)]
327pub struct SlowQueryOptions {
328    /// Whether to enable slow query log.
329    pub enable: bool,
330
331    /// The record type of slow queries. Log records ignore the global log level and use
332    /// the configured stdout and file destinations, format, and retention settings.
333    #[serde(deserialize_with = "empty_string_as_default")]
334    pub record_type: SlowQueriesRecordType,
335
336    /// The threshold of slow queries.
337    #[serde(with = "humantime_serde")]
338    pub threshold: Duration,
339
340    /// The sample ratio of slow queries.
341    pub sample_ratio: f64,
342
343    /// The table TTL of `slow_queries` system table. Default is "90d".
344    /// It's used when `record_type` is `SystemTable`.
345    #[serde(with = "humantime_serde")]
346    pub ttl: Duration,
347}
348
349impl Default for SlowQueryOptions {
350    fn default() -> Self {
351        Self {
352            enable: true,
353            record_type: SlowQueriesRecordType::SystemTable,
354            threshold: Duration::from_secs(30),
355            sample_ratio: 1.0,
356            ttl: Duration::from_secs(90 * 86400),
357        }
358    }
359}
360
361#[derive(Clone, Debug, Serialize, Deserialize, Copy, PartialEq, Default)]
362#[serde(rename_all = "snake_case")]
363pub enum SlowQueriesRecordType {
364    /// Record the slow query in the system table.
365    #[default]
366    SystemTable,
367    /// Record the slow query to the configured stdout and file destinations.
368    Log,
369}
370
371#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
372#[serde(rename_all = "snake_case")]
373pub enum LogFormat {
374    Json,
375    #[default]
376    Text,
377}
378
379#[derive(Clone, Debug)]
380struct TraceContext {
381    app_name: String,
382    node_id: String,
383    logging_opts: LoggingOptions,
384}
385
386impl Default for LoggingOptions {
387    fn default() -> Self {
388        Self {
389            // The directory path will be configured at application startup, typically using the data home directory as a base.
390            dir: "".to_string(),
391            level: None,
392            log_format: LogFormat::Text,
393            enable_otlp_tracing: false,
394            otlp_endpoint: None,
395            tracing_sample_ratio: None,
396            append_stdout: true,
397            enable_file_logging: true,
398            // Rotation hourly, 24 files per day, keeps info log files of 30 days
399            max_log_files: 720,
400            max_log_dir_size: ReadableSize::default(),
401            otlp_export_protocol: None,
402            otlp_headers: HashMap::new(),
403            enable_per_region_metrics: false,
404        }
405    }
406}
407
408#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
409pub struct TracingOptions {
410    #[cfg(feature = "tokio-console")]
411    pub tokio_console_addr: Option<String>,
412}
413
414/// Init tracing for unittest.
415/// Write logs to file `unittest`.
416pub fn init_default_ut_logging() {
417    static START: Once = Once::new();
418
419    START.call_once(|| {
420        let mut g = GLOBAL_UT_LOG_GUARD.as_ref().lock().unwrap();
421
422        // When running in Github's actions, env "UNITTEST_LOG_DIR" is set to a directory other
423        // than "/tmp".
424        // This is to fix the problem that the "/tmp" disk space of action runner's is small,
425        // if we write testing logs in it, actions would fail due to disk out of space error.
426        let dir =
427            env::var("UNITTEST_LOG_DIR").unwrap_or_else(|_| "/tmp/__unittest_logs".to_string());
428
429        let level = env::var("UNITTEST_LOG_LEVEL").unwrap_or_else(|_|
430            "debug,hyper=warn,tower=warn,datafusion=warn,reqwest=warn,sqlparser=warn,h2=info,opendal=info,rskafka=info".to_string()
431        );
432        let opts = LoggingOptions {
433            dir: dir.clone(),
434            level: Some(level),
435            ..Default::default()
436        };
437        *g = Some(init_global_logging(
438            "unittest",
439            &opts,
440            &TracingOptions::default(),
441            None,
442            None,
443        ));
444
445        crate::info!("logs dir = {}", dir);
446    });
447}
448
449static GLOBAL_UT_LOG_GUARD: Lazy<Arc<Mutex<Option<Vec<WorkerGuard>>>>> =
450    Lazy::new(|| Arc::new(Mutex::new(None)));
451
452const DEFAULT_LOG_TARGETS: &str = "info";
453
454#[allow(clippy::print_stdout)]
455pub fn init_global_logging(
456    app_name: &str,
457    opts: &LoggingOptions,
458    tracing_opts: &TracingOptions,
459    node_id: Option<String>,
460    slow_query_opts: Option<&SlowQueryOptions>,
461) -> Vec<WorkerGuard> {
462    static START: Once = Once::new();
463    let mut guards = vec![];
464    let node_id = node_id.unwrap_or_else(|| "none".to_string());
465
466    START.call_once(|| {
467        // Enable log compatible layer to convert log record to tracing span.
468        LogTracer::init().expect("log tracer must be valid");
469
470        let stdout_writer = if opts.append_stdout {
471            let (writer, guard) = tracing_appender::non_blocking(std::io::stdout());
472            guards.push(guard);
473            Some(writer)
474        } else {
475            None
476        };
477        let stdout_logging_layer = stdout_writer.clone().map(|writer| {
478            build_logging_layer(writer, opts.log_format, std::io::stdout().is_terminal())
479        });
480
481        let file_logging_enabled = opts.enable_file_logging && !opts.dir.is_empty();
482        let slow_query_logging_enabled = slow_query_opts
483            .is_some_and(|opts| opts.enable && opts.record_type == SlowQueriesRecordType::Log);
484
485        let retention = file_logging_enabled
486            .then(|| {
487                DirectoryRetention::new(opts.dir.clone(), opts.max_log_dir_size, opts.max_log_files)
488            })
489            .flatten();
490
491        // Configure the file logging layer with rolling policy.
492        let file_logging_layer = if file_logging_enabled {
493            let rolling_appender =
494                build_file_appender(opts, LogFileKind::Default, retention.as_ref());
495            let (writer, guard) = tracing_appender::non_blocking(rolling_appender);
496            guards.push(guard);
497
498            Some(build_logging_layer(writer, opts.log_format, false))
499        } else {
500            None
501        };
502
503        // Configure the error file logging layer with rolling policy.
504        let err_file_logging_layer = if file_logging_enabled {
505            let rolling_appender =
506                build_file_appender(opts, LogFileKind::Error, retention.as_ref());
507            let (writer, guard) = tracing_appender::non_blocking(rolling_appender);
508            guards.push(guard);
509
510            Some(
511                build_logging_layer(writer, opts.log_format, false)
512                    .with_filter(filter::LevelFilter::ERROR),
513            )
514        } else {
515            None
516        };
517
518        if slow_query_logging_enabled {
519            SLOW_QUERY_LOGGER
520                .set(build_slow_query_logger(
521                    opts,
522                    stdout_writer,
523                    retention.as_ref(),
524                    &mut guards,
525                ))
526                .expect("slow query logger already initialized");
527        }
528
529        if let Some(retention) = &retention {
530            retention.initialize();
531        }
532
533        // resolve log level settings from:
534        // - options from command line or config files
535        // - environment variable: RUST_LOG
536        // - default settings
537        let filter = opts
538            .level
539            .as_deref()
540            .or(env::var(EnvFilter::DEFAULT_ENV).ok().as_deref())
541            .unwrap_or(DEFAULT_LOG_TARGETS)
542            .parse::<filter::Targets>()
543            .expect("error parsing log level string");
544
545        let (dyn_filter, reload_handle) = tracing_subscriber::reload::Layer::new(filter.clone());
546
547        LOG_RELOAD_HANDLE
548            .set(reload_handle)
549            .expect("reload handle already set, maybe init_global_logging get called twice?");
550
551        let mut initial_tracer = None;
552        let trace_state = if opts.enable_otlp_tracing {
553            let tracer = create_tracer(app_name, &node_id, opts);
554            initial_tracer = Some(tracer.clone());
555            TraceState::Ready(tracer)
556        } else {
557            TraceState::Deferred(TraceContext {
558                app_name: app_name.to_string(),
559                node_id: node_id.clone(),
560                logging_opts: opts.clone(),
561            })
562        };
563
564        TRACER
565            .set(Mutex::new(trace_state))
566            .expect("trace state already initialized");
567
568        let initial_trace_layer = initial_tracer
569            .as_ref()
570            .map(|tracer| tracing_opentelemetry::layer().with_tracer(tracer.clone()));
571
572        let (dyn_trace_layer, trace_reload_handle) = TraceLayer::new(initial_trace_layer);
573
574        TRACE_RELOAD_HANDLE
575            .set(trace_reload_handle)
576            .unwrap_or_else(|_| panic!("failed to set trace reload handle"));
577
578        // Must enable 'tokio_unstable' cfg to use this feature.
579        // For example: `RUSTFLAGS="--cfg tokio_unstable" cargo run -F common-telemetry/console -- standalone start`
580        #[cfg(feature = "tokio-console")]
581        let subscriber = {
582            let tokio_console_layer =
583                if let Some(tokio_console_addr) = &tracing_opts.tokio_console_addr {
584                    let addr: std::net::SocketAddr = tokio_console_addr.parse().unwrap_or_else(|e| {
585                    panic!("Invalid binding address '{tokio_console_addr}' for tokio-console: {e}");
586                });
587                    println!("tokio-console listening on {addr}");
588
589                    Some(
590                        console_subscriber::ConsoleLayer::builder()
591                            .server_addr(addr)
592                            .spawn(),
593                    )
594                } else {
595                    None
596                };
597
598            Registry::default()
599                .with(dyn_filter)
600                .with(dyn_trace_layer)
601                .with(tokio_console_layer)
602                .with(stdout_logging_layer)
603                .with(file_logging_layer)
604                .with(err_file_logging_layer)
605        };
606
607        // consume the `tracing_opts` to avoid "unused" warnings.
608        let _ = tracing_opts;
609
610        #[cfg(not(feature = "tokio-console"))]
611        let subscriber = Registry::default()
612            .with(dyn_filter)
613            .with(dyn_trace_layer)
614            .with(stdout_logging_layer)
615            .with(file_logging_layer)
616            .with(err_file_logging_layer);
617
618        global::set_text_map_propagator(TraceContextPropagator::new());
619
620        tracing::subscriber::set_global_default(subscriber)
621            .expect("error setting global tracing subscriber");
622    });
623
624    guards
625}
626
627fn create_tracer(app_name: &str, node_id: &str, opts: &LoggingOptions) -> Tracer {
628    let sampler = opts
629        .tracing_sample_ratio
630        .as_ref()
631        .map(create_sampler)
632        .map(Sampler::ParentBased)
633        .unwrap_or(Sampler::ParentBased(Box::new(Sampler::AlwaysOn)));
634
635    let resource = opentelemetry_sdk::Resource::builder_empty()
636        .with_attributes([
637            KeyValue::new(resource::SERVICE_NAME, app_name.to_string()),
638            KeyValue::new(resource::SERVICE_INSTANCE_ID, node_id.to_string()),
639            KeyValue::new(resource::SERVICE_VERSION, common_version::version()),
640            KeyValue::new(resource::PROCESS_PID, std::process::id().to_string()),
641        ])
642        .build();
643
644    opentelemetry_sdk::trace::SdkTracerProvider::builder()
645        .with_batch_exporter(build_otlp_exporter(opts))
646        .with_sampler(sampler)
647        .with_resource(resource)
648        .build()
649        .tracer("greptimedb")
650}
651
652/// Ensure that the OTLP tracer has been constructed, building it lazily if needed.
653pub fn get_or_init_tracer() -> Result<Tracer, &'static str> {
654    let state = TRACER.get().ok_or("trace state is not initialized")?;
655    let mut guard = state.lock().expect("trace state lock poisoned");
656
657    match &mut *guard {
658        TraceState::Ready(tracer) => Ok(tracer.clone()),
659        TraceState::Deferred(context) => {
660            let tracer = create_tracer(&context.app_name, &context.node_id, &context.logging_opts);
661            *guard = TraceState::Ready(tracer.clone());
662            Ok(tracer)
663        }
664    }
665}
666
667fn build_otlp_exporter(opts: &LoggingOptions) -> SpanExporter {
668    let protocol = opts
669        .otlp_export_protocol
670        .clone()
671        .unwrap_or(OtlpExportProtocol::Http);
672
673    let endpoint = opts
674        .otlp_endpoint
675        .as_ref()
676        .map(|e| {
677            if e.starts_with("http") {
678                e.clone()
679            } else {
680                format!("http://{}", e)
681            }
682        })
683        .unwrap_or_else(|| match protocol {
684            OtlpExportProtocol::Grpc => DEFAULT_OTLP_GRPC_ENDPOINT.to_string(),
685            OtlpExportProtocol::Http => DEFAULT_OTLP_HTTP_ENDPOINT.to_string(),
686        });
687
688    match protocol {
689        OtlpExportProtocol::Grpc => SpanExporter::builder()
690            .with_tonic()
691            .with_endpoint(endpoint)
692            .build()
693            .expect("Failed to create OTLP gRPC exporter "),
694
695        OtlpExportProtocol::Http => SpanExporter::builder()
696            .with_http()
697            .with_endpoint(endpoint)
698            .with_protocol(Protocol::HttpBinary)
699            .with_headers(opts.otlp_headers.clone())
700            .build()
701            .expect("Failed to create OTLP HTTP exporter "),
702    }
703}
704
705fn build_slow_query_logger(
706    opts: &LoggingOptions,
707    stdout_writer: Option<NonBlocking>,
708    retention: Option<&DirectoryRetention>,
709    guards: &mut Vec<WorkerGuard>,
710) -> tracing::Dispatch {
711    let stdout_layer = stdout_writer.map(|writer| {
712        build_logging_layer(writer, opts.log_format, std::io::stdout().is_terminal())
713    });
714    let file_layer = if opts.enable_file_logging && !opts.dir.is_empty() {
715        let rolling_appender = build_file_appender(opts, LogFileKind::SlowQuery, retention);
716        let (writer, guard) = tracing_appender::non_blocking(rolling_appender);
717        guards.push(guard);
718        Some(build_logging_layer(writer, opts.log_format, false))
719    } else {
720        None
721    };
722    // Avoid enabling unrelated callsites through this independently registered dispatcher.
723    let filter = FilterFn::new(|metadata| {
724        *metadata.level() == tracing::Level::INFO && metadata.fields().field("slow").is_some()
725    })
726    .with_max_level_hint(LevelFilter::INFO);
727    tracing::Dispatch::new(
728        Registry::default()
729            .with(filter)
730            .with(stdout_layer)
731            .with(file_layer),
732    )
733}
734
735fn build_logging_layer<S>(
736    writer: NonBlocking,
737    log_format: LogFormat,
738    ansi: bool,
739) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync>
740where
741    S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
742{
743    if log_format == LogFormat::Json {
744        Layer::new()
745            .json()
746            .with_writer(writer)
747            .with_ansi(ansi)
748            .boxed()
749    } else {
750        Layer::new().with_writer(writer).with_ansi(ansi).boxed()
751    }
752}
753
754#[cfg(test)]
755mod tests {
756    use std::sync::Barrier;
757    use std::sync::atomic::AtomicUsize;
758
759    use opentelemetry::trace::TraceContextExt;
760    use opentelemetry_sdk::trace::SdkTracerProvider;
761    use tracing_opentelemetry::OpenTelemetrySpanExt;
762
763    use super::*;
764
765    #[test]
766    fn test_slow_query_logging() {
767        const CASE_ENV: &str = "GREPTIME_TEST_SLOW_QUERY_LOGGING";
768        let Ok(case) = env::var(CASE_ENV) else {
769            // Each configuration needs a fresh process because logging initializes only once.
770            for case in [
771                "text",
772                "json",
773                "disabled",
774                "system_table",
775                "file_disabled",
776                "stdout_only_json",
777                "stdout_disabled",
778                "no_directory",
779                "no_options",
780            ] {
781                let output = std::process::Command::new(env::current_exe().unwrap())
782                    .args([
783                        "--exact",
784                        "logging::tests::test_slow_query_logging",
785                        "--nocapture",
786                    ])
787                    .env(CASE_ENV, case)
788                    .output()
789                    .unwrap();
790                assert!(
791                    output.status.success(),
792                    "case {case} failed:\n{}\n{}",
793                    String::from_utf8_lossy(&output.stdout),
794                    String::from_utf8_lossy(&output.stderr),
795                );
796                let stdout = String::from_utf8(output.stdout).unwrap();
797                let expected_slow_queries = if matches!(
798                    case,
799                    "text" | "json" | "file_disabled" | "stdout_only_json" | "no_directory"
800                ) {
801                    4
802                } else {
803                    0
804                };
805                assert_eq!(
806                    stdout.matches("SELECT 1").count(),
807                    expected_slow_queries,
808                    "{case}: {stdout}"
809                );
810                assert_eq!(
811                    stdout.matches("SELECT 2").count(),
812                    expected_slow_queries,
813                    "{case}: {stdout}"
814                );
815                assert_eq!(
816                    stdout.matches("custom_slow_target").count(),
817                    expected_slow_queries,
818                    "{case}: {stdout}"
819                );
820                if case != "stdout_disabled" {
821                    assert_eq!(
822                        stdout.matches("ordinary-info").count(),
823                        1,
824                        "{case}: {stdout}"
825                    );
826                    assert_eq!(
827                        stdout.matches("ordinary-warning").count(),
828                        2,
829                        "{case}: {stdout}"
830                    );
831                } else {
832                    assert!(!stdout.contains("ordinary-"), "{stdout}");
833                }
834                if matches!(case, "json" | "stdout_only_json") {
835                    for line in stdout.lines().filter(|line| line.contains("SELECT")) {
836                        let event: serde_json::Value = serde_json::from_str(line).unwrap();
837                        assert_eq!(event["fields"]["slow"], true);
838                        assert_eq!(event["level"], "INFO");
839                    }
840                }
841            }
842            return;
843        };
844
845        let dir = tempfile::tempdir().unwrap();
846        let opts = LoggingOptions {
847            dir: if case == "no_directory" {
848                String::new()
849            } else {
850                dir.path().to_str().unwrap().to_string()
851            },
852            level: Some("warn".to_string()),
853            log_format: if matches!(case.as_str(), "json" | "stdout_only_json") {
854                LogFormat::Json
855            } else {
856                LogFormat::Text
857            },
858            append_stdout: case != "stdout_disabled",
859            enable_file_logging: !matches!(case.as_str(), "file_disabled" | "stdout_only_json"),
860            max_log_dir_size: ReadableSize::mb(1),
861            ..Default::default()
862        };
863        let slow_opts = SlowQueryOptions {
864            enable: case != "disabled",
865            record_type: if case == "system_table" {
866                SlowQueriesRecordType::SystemTable
867            } else {
868                SlowQueriesRecordType::Log
869            },
870            ..Default::default()
871        };
872        let guards = init_global_logging(
873            "slow-query-test",
874            &opts,
875            &TracingOptions::default(),
876            None,
877            (case != "no_options").then_some(&slow_opts),
878        );
879
880        for level in ["warn", "error", "off", "info"] {
881            LOG_RELOAD_HANDLE
882                .get()
883                .unwrap()
884                .reload(level.parse::<Targets>().unwrap())
885                .unwrap();
886            crate::info!(slow_operation = true, "ordinary-info");
887            crate::warn!("ordinary-warning");
888            crate::slow!(query = "SELECT 1", cost = 31, threshold = 30);
889            crate::slow!(target: "custom_slow_target", query = "SELECT 2");
890        }
891        // Dropping the guards flushes the asynchronous writers before reading the files.
892        drop(guards);
893
894        let mut slow_logs = String::new();
895        let mut regular_logs = String::new();
896        for entry in std::fs::read_dir(dir.path()).unwrap() {
897            let entry = entry.unwrap();
898            let name = entry.file_name();
899            let name = name.to_str().unwrap();
900            if name.starts_with("greptimedb-slow-queries.") {
901                slow_logs.push_str(&std::fs::read_to_string(entry.path()).unwrap());
902            } else if name.starts_with("greptimedb.") {
903                regular_logs.push_str(&std::fs::read_to_string(entry.path()).unwrap());
904            }
905        }
906
907        if matches!(case.as_str(), "text" | "json" | "stdout_disabled") {
908            assert_eq!(slow_logs.lines().count(), 8, "{slow_logs}");
909            assert_eq!(slow_logs.matches("SELECT 1").count(), 4);
910            assert_eq!(slow_logs.matches("SELECT 2").count(), 4);
911            assert_eq!(slow_logs.matches("custom_slow_target").count(), 4);
912            assert!(!slow_logs.contains("ordinary-"));
913            if case == "json" {
914                for line in slow_logs.lines() {
915                    let event: serde_json::Value = serde_json::from_str(line).unwrap();
916                    assert_eq!(event["fields"]["slow"], true);
917                    assert_eq!(event["level"], "INFO");
918                }
919            }
920        } else {
921            assert!(slow_logs.is_empty(), "{slow_logs}");
922        }
923        if matches!(
924            case.as_str(),
925            "file_disabled" | "stdout_only_json" | "no_directory"
926        ) {
927            assert!(regular_logs.is_empty(), "{regular_logs}");
928        } else {
929            assert_eq!(regular_logs.matches("ordinary-info").count(), 1);
930            assert_eq!(regular_logs.matches("ordinary-warning").count(), 2);
931            assert!(!regular_logs.contains("SELECT"), "{regular_logs}");
932        }
933    }
934
935    #[test]
936    fn test_trace_switch_preserves_active_spans() {
937        let provider = SdkTracerProvider::builder()
938            .with_sampler(Sampler::AlwaysOn)
939            .build();
940        for initially_enabled in [false, true] {
941            let new_layer =
942                || tracing_opentelemetry::layer().with_tracer(provider.tracer("switch"));
943            let (filter, _) = tracing_subscriber::reload::Layer::new(
944                Targets::new().with_default(tracing::Level::INFO),
945            );
946            let (layer, handle) = TraceLayer::new(initially_enabled.then(new_layer));
947            let dispatch = tracing::Dispatch::new(Registry::default().with(filter).with(layer));
948            tracing::dispatcher::with_default(&dispatch, || {
949                let initial = tracing::info_span!("initial");
950                assert!(!initial.is_disabled());
951                assert_eq!(
952                    initial.context().span().span_context().is_valid(),
953                    initially_enabled
954                );
955                handle.set_enabled_with(true, || Ok(new_layer())).unwrap();
956                let parent = tracing::info_span!("parent");
957                let parent_context = parent.context();
958                assert!(parent_context.span().span_context().is_valid());
959                let previous_context = opentelemetry::Context::current();
960                let entered = parent.enter();
961                handle.set_enabled(false).unwrap();
962                let disabled = tracing::info_span!("disabled");
963                assert!(!disabled.is_disabled());
964                assert!(!disabled.context().span().span_context().is_valid());
965                assert_eq!(
966                    parent.context().span().span_context(),
967                    parent_context.span().span_context()
968                );
969                assert!(dispatch.downcast_ref::<OtelTraceLayer>().is_some());
970                drop(entered);
971                assert_eq!(
972                    opentelemetry::Context::current().span().span_context(),
973                    previous_context.span().span_context()
974                );
975                let disabled_entered = disabled.enter();
976                handle
977                    .set_enabled_with(true, || panic!("layer must not be replaced"))
978                    .unwrap();
979                drop(disabled_entered);
980                let child = tracing::info_span!(parent: &parent, "child");
981                assert_eq!(
982                    child.context().span().span_context().trace_id(),
983                    parent_context.span().span_context().trace_id()
984                );
985                assert!(!disabled.context().span().span_context().is_valid());
986            });
987        }
988    }
989
990    #[test]
991    fn test_trace_switch_retries_initialization() {
992        let (filter, _) = tracing_subscriber::reload::Layer::new(
993            Targets::new().with_default(tracing::Level::INFO),
994        );
995        let (layer, handle) = TraceLayer::new(None);
996        let subscriber = Registry::default().with(filter).with(layer);
997        tracing::subscriber::with_default(subscriber, || {
998            handle
999                .set_enabled_with(false, || panic!("disabling must not initialize OTLP"))
1000                .unwrap();
1001            assert_eq!(
1002                handle.set_enabled_with(true, || Err("initialization failed")),
1003                Err("initialization failed")
1004            );
1005            let failed = tracing::info_span!("after_failed_enable");
1006            assert!(!failed.context().span().span_context().is_valid());
1007            let provider = SdkTracerProvider::builder()
1008                .with_sampler(Sampler::AlwaysOn)
1009                .build();
1010            handle
1011                .set_enabled_with(true, || {
1012                    Ok(tracing_opentelemetry::layer().with_tracer(provider.tracer("retry")))
1013                })
1014                .unwrap();
1015            let enabled = tracing::info_span!("after_successful_enable");
1016            assert!(enabled.context().span().span_context().is_valid());
1017        });
1018    }
1019
1020    #[test]
1021    fn test_trace_switch_stops_collecting_fields_when_disabled() {
1022        struct CountFormatting(AtomicUsize);
1023
1024        impl std::fmt::Debug for CountFormatting {
1025            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1026                self.0.fetch_add(1, Ordering::Relaxed);
1027                f.write_str("value")
1028            }
1029        }
1030
1031        let value = CountFormatting(AtomicUsize::new(0));
1032        let provider = SdkTracerProvider::builder()
1033            .with_sampler(Sampler::AlwaysOn)
1034            .build();
1035        let (filter, _) = tracing_subscriber::reload::Layer::new(
1036            Targets::new().with_default(tracing::Level::INFO),
1037        );
1038        let (layer, handle) = TraceLayer::new(Some(
1039            tracing_opentelemetry::layer().with_tracer(provider.tracer("fields")),
1040        ));
1041        tracing::subscriber::with_default(Registry::default().with(filter).with(layer), || {
1042            let admitted = tracing::info_span!("admitted", field = tracing::field::Empty);
1043            admitted.record("field", tracing::field::debug(&value));
1044            admitted.in_scope(|| tracing::info!(value = ?value));
1045            assert_eq!(value.0.load(Ordering::Relaxed), 2);
1046
1047            handle.set_enabled(false).unwrap();
1048            let unadmitted = tracing::info_span!("unadmitted", field = tracing::field::Empty);
1049            for span in [&admitted, &unadmitted] {
1050                span.record("field", tracing::field::debug(&value));
1051                span.in_scope(|| tracing::info!(value = ?value));
1052            }
1053            assert_eq!(value.0.load(Ordering::Relaxed), 2);
1054
1055            handle
1056                .set_enabled_with(true, || panic!("layer must not be replaced"))
1057                .unwrap();
1058            admitted.record("field", tracing::field::debug(&value));
1059            admitted.in_scope(|| tracing::info!(value = ?value));
1060            assert_eq!(value.0.load(Ordering::Relaxed), 4);
1061        });
1062    }
1063
1064    #[test]
1065    fn test_trace_switch_finishes_admitted_spans() {
1066        #[derive(Debug)]
1067        struct Exporter(std::sync::mpsc::Sender<String>);
1068
1069        impl opentelemetry_sdk::trace::SpanExporter for Exporter {
1070            async fn export(
1071                &self,
1072                batch: Vec<opentelemetry_sdk::trace::SpanData>,
1073            ) -> opentelemetry_sdk::error::OTelSdkResult {
1074                for span in batch {
1075                    self.0.send(span.name.into_owned()).unwrap();
1076                }
1077                Ok(())
1078            }
1079        }
1080
1081        let (sender, receiver) = std::sync::mpsc::channel();
1082        let provider = SdkTracerProvider::builder()
1083            .with_sampler(Sampler::AlwaysOn)
1084            .with_simple_exporter(Exporter(sender))
1085            .build();
1086        let (filter, _) = tracing_subscriber::reload::Layer::new(
1087            Targets::new().with_default(tracing::Level::INFO),
1088        );
1089        let (layer, handle) = TraceLayer::new(Some(
1090            tracing_opentelemetry::layer().with_tracer(provider.tracer("export")),
1091        ));
1092        tracing::subscriber::with_default(Registry::default().with(filter).with(layer), || {
1093            let active = tracing::info_span!("admitted");
1094            handle.set_enabled(false).unwrap();
1095            let disabled = tracing::info_span!("not_admitted");
1096            drop(active);
1097            drop(disabled);
1098        });
1099        provider.force_flush().unwrap();
1100        assert_eq!(receiver.try_iter().collect::<Vec<_>>(), ["admitted"]);
1101    }
1102
1103    #[test]
1104    fn test_trace_switch_concurrent_context_access() {
1105        let provider = SdkTracerProvider::builder()
1106            .with_sampler(Sampler::AlwaysOn)
1107            .build();
1108        let (filter, _) = tracing_subscriber::reload::Layer::new(
1109            Targets::new().with_default(tracing::Level::INFO),
1110        );
1111        let (layer, handle) = TraceLayer::new(Some(
1112            tracing_opentelemetry::layer().with_tracer(provider.tracer("concurrent")),
1113        ));
1114        let dispatch = tracing::Dispatch::new(Registry::default().with(filter).with(layer));
1115        let parent = tracing::dispatcher::with_default(&dispatch, || {
1116            tracing::info_span!("concurrent_parent")
1117        });
1118        let context = parent.context();
1119        let barrier = Barrier::new(4);
1120        std::thread::scope(|scope| {
1121            for _ in 0..3 {
1122                scope.spawn(|| {
1123                    tracing::dispatcher::with_default(&dispatch, || {
1124                        let _entered = parent.enter();
1125                        barrier.wait();
1126                        for _ in 0..500 {
1127                            assert_eq!(
1128                                parent.context().span().span_context(),
1129                                context.span().span_context()
1130                            );
1131                            {
1132                                let child = tracing::info_span!("concurrent_child");
1133                                let _entered = child.enter();
1134                                let _ = child.context();
1135                                tracing::info!("concurrent event");
1136                            }
1137                            assert_eq!(
1138                                opentelemetry::Context::current().span().span_context(),
1139                                context.span().span_context()
1140                            );
1141                        }
1142                    });
1143                });
1144            }
1145            barrier.wait();
1146            for i in 0..1000 {
1147                handle
1148                    .set_enabled_with(i % 2 == 0, || panic!("layer must not be replaced"))
1149                    .unwrap();
1150            }
1151        });
1152    }
1153
1154    #[test]
1155    fn test_logging_options_deserialization_default() {
1156        let json = r#"{}"#;
1157        let opts: LoggingOptions = serde_json::from_str(json).unwrap();
1158
1159        assert_eq!(opts.log_format, LogFormat::Text);
1160        assert_eq!(opts.dir, "");
1161        assert_eq!(opts.level, None);
1162        assert!(opts.append_stdout);
1163        assert!(opts.enable_file_logging);
1164    }
1165
1166    #[test]
1167    fn test_logging_options_deserialization_enable_file_logging() {
1168        let json = r#"{"enable_file_logging": false}"#;
1169        let opts: LoggingOptions = serde_json::from_str(json).unwrap();
1170
1171        assert!(!opts.enable_file_logging);
1172    }
1173
1174    #[test]
1175    fn test_logging_options_deserialization_max_log_dir_size() {
1176        let json = r#"{"max_log_dir_size": "1MiB"}"#;
1177        let opts: LoggingOptions = serde_json::from_str(json).unwrap();
1178
1179        assert_eq!(opts.max_log_dir_size, ReadableSize::mb(1));
1180    }
1181
1182    #[test]
1183    fn test_logging_options_deserialization_empty_log_format() {
1184        let json = r#"{"log_format": ""}"#;
1185        let opts: LoggingOptions = serde_json::from_str(json).unwrap();
1186
1187        // Empty string should use default (Text)
1188        assert_eq!(opts.log_format, LogFormat::Text);
1189    }
1190
1191    #[test]
1192    fn test_logging_options_deserialization_valid_log_format() {
1193        let json_format = r#"{"log_format": "json"}"#;
1194        let opts: LoggingOptions = serde_json::from_str(json_format).unwrap();
1195        assert_eq!(opts.log_format, LogFormat::Json);
1196
1197        let text_format = r#"{"log_format": "text"}"#;
1198        let opts: LoggingOptions = serde_json::from_str(text_format).unwrap();
1199        assert_eq!(opts.log_format, LogFormat::Text);
1200    }
1201
1202    #[test]
1203    fn test_logging_options_deserialization_missing_log_format() {
1204        let json = r#"{"dir": "/tmp/logs"}"#;
1205        let opts: LoggingOptions = serde_json::from_str(json).unwrap();
1206
1207        // Missing log_format should use default (Text)
1208        assert_eq!(opts.log_format, LogFormat::Text);
1209        assert_eq!(opts.dir, "/tmp/logs");
1210    }
1211
1212    #[test]
1213    fn test_slow_query_options_deserialization_default() {
1214        let json = r#"{"enable": true, "threshold": "30s"}"#;
1215        let opts: SlowQueryOptions = serde_json::from_str(json).unwrap();
1216
1217        assert_eq!(opts.record_type, SlowQueriesRecordType::SystemTable);
1218        assert!(opts.enable);
1219    }
1220
1221    #[test]
1222    fn test_slow_query_options_deserialization_empty_record_type() {
1223        let json = r#"{"enable": true, "record_type": "", "threshold": "30s"}"#;
1224        let opts: SlowQueryOptions = serde_json::from_str(json).unwrap();
1225
1226        // Empty string should use default (SystemTable)
1227        assert_eq!(opts.record_type, SlowQueriesRecordType::SystemTable);
1228        assert!(opts.enable);
1229    }
1230
1231    #[test]
1232    fn test_slow_query_options_deserialization_valid_record_type() {
1233        let system_table_json =
1234            r#"{"enable": true, "record_type": "system_table", "threshold": "30s"}"#;
1235        let opts: SlowQueryOptions = serde_json::from_str(system_table_json).unwrap();
1236        assert_eq!(opts.record_type, SlowQueriesRecordType::SystemTable);
1237
1238        let log_json = r#"{"enable": true, "record_type": "log", "threshold": "30s"}"#;
1239        let opts: SlowQueryOptions = serde_json::from_str(log_json).unwrap();
1240        assert_eq!(opts.record_type, SlowQueriesRecordType::Log);
1241    }
1242
1243    #[test]
1244    fn test_slow_query_options_deserialization_missing_record_type() {
1245        let json = r#"{"enable": false, "threshold": "30s"}"#;
1246        let opts: SlowQueryOptions = serde_json::from_str(json).unwrap();
1247
1248        // Missing record_type should use default (SystemTable)
1249        assert_eq!(opts.record_type, SlowQueriesRecordType::SystemTable);
1250        assert!(!opts.enable);
1251    }
1252
1253    #[test]
1254    fn test_otlp_export_protocol_deserialization_valid_values() {
1255        let grpc_json = r#""grpc""#;
1256        let protocol: OtlpExportProtocol = serde_json::from_str(grpc_json).unwrap();
1257        assert_eq!(protocol, OtlpExportProtocol::Grpc);
1258
1259        let http_json = r#""http""#;
1260        let protocol: OtlpExportProtocol = serde_json::from_str(http_json).unwrap();
1261        assert_eq!(protocol, OtlpExportProtocol::Http);
1262    }
1263
1264    #[test]
1265    fn test_logging_options_partial_eq_all_fields() {
1266        let base = LoggingOptions::default();
1267
1268        let mut log_format = base.clone();
1269        log_format.log_format = LogFormat::Json;
1270        assert_ne!(base, log_format);
1271
1272        let mut max_log_files = base.clone();
1273        max_log_files.max_log_files += 1;
1274        assert_ne!(base, max_log_files);
1275
1276        let mut max_log_dir_size = base.clone();
1277        max_log_dir_size.max_log_dir_size = ReadableSize::mb(1);
1278        assert_ne!(base, max_log_dir_size);
1279
1280        let mut otlp_export_protocol = base.clone();
1281        otlp_export_protocol.otlp_export_protocol = Some(OtlpExportProtocol::Http);
1282        assert_ne!(base, otlp_export_protocol);
1283
1284        let mut otlp_headers = base.clone();
1285        otlp_headers
1286            .otlp_headers
1287            .insert("key".to_string(), "value".to_string());
1288        assert_ne!(base, otlp_headers);
1289    }
1290}