Skip to main content

mz_orchestrator_tracing/
lib.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Service orchestration for tracing-aware services.
11
12use std::ffi::OsString;
13use std::fmt;
14use std::sync::Arc;
15use std::time::Duration;
16
17use async_trait::async_trait;
18use clap::{CommandFactory, FromArgMatches};
19use derivative::Derivative;
20use futures_core::stream::BoxStream;
21use http::header::{HeaderName, HeaderValue};
22use mz_build_info::BuildInfo;
23#[cfg(feature = "tokio-console")]
24use mz_orchestrator::ServicePort;
25use mz_orchestrator::{
26    NamespacedOrchestrator, Orchestrator, Service, ServiceAssignments, ServiceConfig, ServiceEvent,
27    ServiceProcessMetrics,
28};
29use mz_ore::cli::KeyValueArg;
30use mz_ore::metrics::MetricsRegistry;
31#[cfg(feature = "tokio-console")]
32use mz_ore::netio::SocketAddr;
33#[cfg(feature = "tokio-console")]
34use mz_ore::tracing::TokioConsoleConfig;
35use mz_ore::tracing::{
36    OpenTelemetryConfig, SentryConfig, StderrLogConfig, StderrLogFormat, TracingConfig,
37    TracingHandle,
38};
39use mz_tracing::CloneableEnvFilter;
40use opentelemetry::KeyValue;
41use opentelemetry_sdk::resource::Resource;
42use sentry_tracing::EventFilter;
43
44/// Command line arguments for application tracing.
45///
46/// These arguments correspond directly to parameters in [`TracingConfig`], and
47/// this type can be directly converted into a `TracingConfig` via the supplied
48/// `From<TracingCliArgs>` implementation.
49///
50/// This logic is separated from `mz_ore::tracing` because the details of how
51/// these command-line arguments are parsed and unparsed is specific to
52/// orchestrators and does not belong in a foundational crate like `mz_ore`.
53#[derive(Derivative, Clone, clap::Parser)]
54#[derivative(Debug)]
55pub struct TracingCliArgs {
56    /// Which tracing events to log to stderr during startup, before the
57    /// real log filter is synced from LaunchDarkly.
58    ///
59    /// WARNING: you probably don't want to set this for `environmentd`. This
60    /// parameter only controls logging for the brief moment before the log
61    /// filter is synced from LaunchDarkly. You probably instead want to pass
62    /// `--system-parameter-default=log_filter=<filter>`, which will set the
63    /// default log filter to use unless overridden by the LaunchDarkly sync.
64    ///
65    /// This value is a comma-separated list of filter directives. Each filter
66    /// directive has the following format:
67    ///
68    /// ```text
69    /// [module::path=]level
70    /// ```
71    ///
72    /// A directive indicates that log messages from the specified module that
73    /// are at least as severe as the specified level should be emitted. If a
74    /// directive omits the module, then it implicitly applies to all modules.
75    /// When directives conflict, the last directive wins. If a log message does
76    /// not match any directive, it is not emitted.
77    ///
78    /// The module path of a log message reflects its location in Materialize's
79    /// source code. Choosing module paths for filter directives requires
80    /// familiarity with Materialize's codebase and is intended for advanced
81    /// users. Note that module paths change frequency from release to release.
82    ///
83    /// The valid levels for a log message are, in increasing order of severity:
84    /// trace, debug, info, warn, and error. The special level "off" may be used
85    /// in a directive to suppress all log messages, even errors.
86    ///
87    /// The default value for this option is "info".
88    #[clap(
89        long,
90        env = "STARTUP_LOG_FILTER",
91        value_name = "FILTER",
92        default_value = "info"
93    )]
94    pub startup_log_filter: CloneableEnvFilter,
95    /// The format to use for stderr log messages.
96    #[clap(long, env = "LOG_FORMAT", default_value_t, value_enum)]
97    pub log_format: LogFormat,
98    /// An optional prefix for each stderr log line.
99    ///
100    /// Only respected when `--log-format` is `text`.
101    #[clap(long, env = "LOG_PREFIX")]
102    pub log_prefix: Option<String>,
103    /// OpenTelemetry batch flag defaults are based on the
104    /// `BatchConfig::default()` in the opentelemetry_sdk crate.
105    /// <https://docs.rs/opentelemetry_sdk/0.21.2/opentelemetry_sdk/trace/struct.BatchConfig.html>
106    ///
107    /// The max number of tracing spans to queue before dropping.
108    #[clap(
109        long,
110        env = "OPENTELEMETRY_MAX_BATCH_QUEUE_SIZE",
111        default_value = "2048",
112        requires = "opentelemetry_endpoint"
113    )]
114    pub opentelemetry_max_batch_queue_size: usize,
115    /// The max number of spans to export in a single batch.
116    #[clap(
117        long,
118        env = "OPENTELEMETRY_MAX_EXPORT_BATCH_SIZE",
119        default_value = "512",
120        requires = "opentelemetry_endpoint"
121    )]
122    pub opentelemetry_max_export_batch_size: usize,
123    /// The max number of concurrent export tasks.
124    #[clap(
125        long,
126        env = "OPENTELEMETRY_MAX_CONCURRENT_EXPORTS",
127        default_value = "1",
128        requires = "opentelemetry_endpoint"
129    )]
130    pub opentelemetry_max_concurrent_exports: usize,
131    /// The delay between sequential sending of batches.
132    #[clap(
133        long,
134        env = "OPENTELEMETRY_SCHED_DELAY",
135        default_value = "5000ms",
136        requires = "opentelemetry_endpoint",
137        value_parser = humantime::parse_duration,
138    )]
139    pub opentelemetry_sched_delay: Duration,
140    /// The max time to attempt exporting a batch.
141    #[clap(
142        long,
143        env = "OPENTELEMETRY_MAX_EXPORT_TIMEOUT",
144        default_value = "30s",
145        requires = "opentelemetry_endpoint",
146        value_parser = humantime::parse_duration,
147    )]
148    pub opentelemetry_max_export_timeout: Duration,
149    /// Export OpenTelemetry tracing events to the provided endpoint.
150    ///
151    /// The specified endpoint should speak the OTLP/HTTP protocol. If the
152    /// backend requires authentication, you can pass authentication metadata
153    /// via the `--opentelemetry-header` option.
154    #[clap(long, env = "OPENTELEMETRY_ENDPOINT")]
155    pub opentelemetry_endpoint: Option<String>,
156    /// A header to pass with every request to the OpenTelemetry endpoint
157    /// specified by `--opentelemetry-endpoint` in the form `NAME=VALUE`.
158    ///
159    /// Requires that the `--opentelemetry-endpoint` option is specified.
160    /// To specify multiple headers, either specify this option multiple times,
161    /// or specify it once with multiple `NAME=VALUE` pairs separated by commas.
162    #[clap(
163        long,
164        env = "OPENTELEMETRY_HEADER",
165        requires = "opentelemetry_endpoint",
166        value_name = "NAME=VALUE",
167        use_value_delimiter = true
168    )]
169    pub opentelemetry_header: Vec<KeyValueArg<HeaderName, HeaderValue>>,
170    /// Which tracing events to export to the OpenTelemetry endpoint specified
171    /// by `--opentelemetry-endpoint`.
172    ///
173    /// The syntax of this option is the same as the syntax of the
174    /// `--startup-log-filter` option.
175    ///
176    /// Requires that the `--opentelemetry-endpoint` option is specified.
177    #[clap(
178        long,
179        env = "STARTUP_OPENTELEMETRY_FILTER",
180        requires = "opentelemetry_endpoint",
181        default_value = "info"
182    )]
183    pub startup_opentelemetry_filter: CloneableEnvFilter,
184    /// Additional key-value pairs to send with all opentelemetry traces.
185    /// Also used as Sentry tags.
186    ///
187    /// Requires that one of the `--opentelemetry-endpoint` or `--sentry-dsn`
188    /// options is specified.
189    #[clap(
190        long,
191        env = "OPENTELEMETRY_RESOURCE",
192        value_name = "NAME=VALUE",
193        use_value_delimiter = true
194    )]
195    pub opentelemetry_resource: Vec<KeyValueArg<String, String>>,
196    /// The address on which to listen for Tokio console connections.
197    ///
198    /// For details about Tokio console, see: <https://github.com/tokio-rs/console>
199    ///
200    /// Requires that the `--tokio-console` option is specified.
201    #[cfg(feature = "tokio-console")]
202    #[clap(long, env = "TOKIO_CONSOLE_LISTEN_ADDR")]
203    pub tokio_console_listen_addr: Option<SocketAddr>,
204    /// How frequently to publish updates to Tokio console clients.
205    ///
206    /// Requires that the `--tokio-console` option is specified.
207    #[cfg(feature = "tokio-console")]
208    #[clap(
209        long,
210        env = "TOKIO_CONSOLE_PUBLISH_INTERVAL",
211        requires = "tokio_console_listen_addr",
212        value_parser = humantime::parse_duration,
213        default_value = "1s",
214    )]
215    pub tokio_console_publish_interval: Duration,
216    /// How long Tokio console data is retained for completed tasks.
217    ///
218    /// Requires that the `--tokio-console` option is specified.
219    #[cfg(feature = "tokio-console")]
220    #[clap(
221        long,
222        env = "TOKIO_CONSOLE_RETENTION",
223        requires = "tokio_console_listen_addr",
224        value_parser = humantime::parse_duration,
225        default_value = "1h",
226    )]
227    pub tokio_console_retention: Duration,
228    /// Sentry data source to submit events and exceptions (e.g. panics) to.
229    #[clap(long, env = "SENTRY_DSN")]
230    pub sentry_dsn: Option<String>,
231    /// The environment name to report to Sentry.
232    ///
233    /// Ignored unless the `--sentry-dsn` option is specified.
234    ///
235    /// See: <https://docs.sentry.io/platforms/rust/configuration/options/#environment>
236    #[clap(long, env = "SENTRY_ENVIRONMENT")]
237    pub sentry_environment: Option<String>,
238    /// Tags to send with all Sentry events, in addition to the tags specified by
239    /// `--opentelemetry-resource`.
240    ///
241    /// Requires that the `--sentry-dsn` option is specified.
242    #[clap(
243        long,
244        env = "SENTRY_TAG",
245        value_name = "NAME=VALUE",
246        use_value_delimiter = true
247    )]
248    pub sentry_tag: Vec<KeyValueArg<String, String>>,
249    /// Test-only feature to enable tracing assertions.
250    #[cfg(feature = "capture")]
251    #[derivative(Debug = "ignore")]
252    #[clap(skip)]
253    pub capture: Option<tracing_capture::SharedStorage>,
254}
255
256impl Default for TracingCliArgs {
257    fn default() -> TracingCliArgs {
258        let matches = TracingCliArgs::command().get_matches_from::<_, OsString>([]);
259        TracingCliArgs::from_arg_matches(&matches)
260            .expect("no arguments produce valid TracingCliArgs")
261    }
262}
263
264impl TracingCliArgs {
265    pub async fn configure_tracing(
266        &self,
267        StaticTracingConfig {
268            service_name,
269            build_info,
270        }: StaticTracingConfig,
271        registry: MetricsRegistry,
272    ) -> Result<TracingHandle, anyhow::Error> {
273        mz_ore::tracing::configure(TracingConfig {
274            service_name,
275            stderr_log: StderrLogConfig {
276                format: match self.log_format {
277                    LogFormat::Text => StderrLogFormat::Text {
278                        prefix: self.log_prefix.clone(),
279                    },
280                    LogFormat::Json => StderrLogFormat::Json,
281                },
282                filter: self.startup_log_filter.clone().into(),
283            },
284            opentelemetry: self.opentelemetry_endpoint.clone().map(|endpoint| {
285                OpenTelemetryConfig {
286                    endpoint,
287                    headers: self
288                        .opentelemetry_header
289                        .iter()
290                        .map(|header| (header.key.clone(), header.value.clone()))
291                        .collect(),
292                    filter: self.startup_opentelemetry_filter.clone().into(),
293                    max_batch_queue_size: self.opentelemetry_max_batch_queue_size,
294                    max_export_batch_size: self.opentelemetry_max_export_batch_size,
295                    max_concurrent_exports: self.opentelemetry_max_concurrent_exports,
296                    batch_scheduled_delay: self.opentelemetry_sched_delay,
297                    max_export_timeout: self.opentelemetry_max_export_timeout,
298                    resource: {
299                        Resource::builder()
300                            .with_attributes(
301                                self.opentelemetry_resource
302                                    .iter()
303                                    .cloned()
304                                    .map(|kv| KeyValue::new(kv.key, kv.value)),
305                            )
306                            .build()
307                    },
308                }
309            }),
310            #[cfg(feature = "tokio-console")]
311            tokio_console: self.tokio_console_listen_addr.clone().map(|listen_addr| {
312                TokioConsoleConfig {
313                    listen_addr,
314                    publish_interval: self.tokio_console_publish_interval,
315                    retention: self.tokio_console_retention,
316                }
317            }),
318            sentry: self.sentry_dsn.clone().map(|dsn| SentryConfig {
319                dsn,
320                environment: self.sentry_environment.clone(),
321                tags: self
322                    .opentelemetry_resource
323                    .iter()
324                    .cloned()
325                    .chain(self.sentry_tag.iter().cloned())
326                    .map(|kv| (kv.key, kv.value))
327                    .collect(),
328                event_filter: mz_sentry_event_filter,
329            }),
330            build_version: build_info.version,
331            build_sha: build_info.sha,
332            registry,
333            #[cfg(feature = "capture")]
334            capture: self.capture.clone(),
335        })
336        .await
337    }
338}
339
340/// Target for events that go to the logs but never to Sentry, e.g. because they carry client IPs.
341/// The Sentry layer otherwise attaches recent info events as breadcrumbs to unrelated errors.
342pub const NO_SENTRY_TARGET: &str = "mz_no_sentry";
343
344pub fn mz_sentry_event_filter(meta: &tracing::Metadata<'_>) -> EventFilter {
345    // special cases
346    if meta.target() == "librdkafka" || meta.target() == NO_SENTRY_TARGET {
347        return EventFilter::Ignore;
348    }
349
350    // default case
351    sentry_tracing::default_event_filter(meta)
352}
353
354/// The fields of [`TracingConfig`] that are not set by command-line arguments.
355pub struct StaticTracingConfig {
356    /// See [`TracingConfig::service_name`].
357    pub service_name: &'static str,
358    /// The build information for this service.
359    pub build_info: BuildInfo,
360}
361
362/// Wraps an [`Orchestrator`] to inject tracing into all created services.
363#[derive(Debug)]
364pub struct TracingOrchestrator {
365    inner: Arc<dyn Orchestrator>,
366    tracing_args: TracingCliArgs,
367}
368
369impl TracingOrchestrator {
370    /// Constructs a new tracing orchestrator.
371    ///
372    /// The orchestrator wraps the provided `inner` orchestrator. It mutates
373    /// [`ServiceConfig`]s to inject the tracing configuration specified by
374    /// `tracing_args`.
375    ///
376    /// All services created by the orchestrator **must** embed the
377    /// [`TracingCliArgs`] in their command-line argument parser.
378    pub fn new(inner: Arc<dyn Orchestrator>, tracing_args: TracingCliArgs) -> TracingOrchestrator {
379        TracingOrchestrator {
380            inner,
381            tracing_args,
382        }
383    }
384}
385
386impl Orchestrator for TracingOrchestrator {
387    fn namespace(&self, namespace: &str) -> Arc<dyn NamespacedOrchestrator> {
388        Arc::new(NamespacedTracingOrchestrator {
389            namespace: namespace.to_string(),
390            inner: self.inner.namespace(namespace),
391            tracing_args: self.tracing_args.clone(),
392        })
393    }
394}
395
396#[derive(Debug)]
397struct NamespacedTracingOrchestrator {
398    namespace: String,
399    inner: Arc<dyn NamespacedOrchestrator>,
400    tracing_args: TracingCliArgs,
401}
402
403#[async_trait]
404impl NamespacedOrchestrator for NamespacedTracingOrchestrator {
405    async fn fetch_service_metrics(
406        &self,
407        id: &str,
408    ) -> Result<Vec<ServiceProcessMetrics>, anyhow::Error> {
409        self.inner.fetch_service_metrics(id).await
410    }
411
412    fn ensure_service(
413        &self,
414        id: &str,
415        mut service_config: ServiceConfig,
416    ) -> Result<Box<dyn Service>, anyhow::Error> {
417        let tracing_args = self.tracing_args.clone();
418        let log_prefix_arg = format!("{}-{}", self.namespace, id);
419        let args_fn = move |assigned: ServiceAssignments| {
420            #[cfg(feature = "tokio-console")]
421            let tokio_console_listen_addr = assigned.listen_addrs.get("tokio-console");
422            let mut args = (service_config.args)(assigned);
423            let TracingCliArgs {
424                startup_log_filter,
425                log_prefix,
426                log_format,
427                opentelemetry_max_batch_queue_size,
428                opentelemetry_max_export_batch_size,
429                opentelemetry_max_concurrent_exports,
430                opentelemetry_sched_delay,
431                opentelemetry_max_export_timeout,
432                opentelemetry_endpoint,
433                opentelemetry_header,
434                startup_opentelemetry_filter: _,
435                opentelemetry_resource,
436                #[cfg(feature = "tokio-console")]
437                    tokio_console_listen_addr: _,
438                #[cfg(feature = "tokio-console")]
439                tokio_console_publish_interval,
440                #[cfg(feature = "tokio-console")]
441                tokio_console_retention,
442                sentry_dsn,
443                sentry_environment,
444                sentry_tag,
445                #[cfg(feature = "capture")]
446                    capture: _,
447            } = &tracing_args;
448            args.push(format!("--startup-log-filter={startup_log_filter}"));
449            args.push(format!("--log-format={log_format}"));
450            if log_prefix.is_some() {
451                args.push(format!("--log-prefix={log_prefix_arg}"));
452            }
453            if let Some(endpoint) = opentelemetry_endpoint {
454                args.push(format!("--opentelemetry-endpoint={endpoint}"));
455                for kv in opentelemetry_header {
456                    args.push(format!(
457                        "--opentelemetry-header={}={}",
458                        kv.key,
459                        kv.value
460                            .to_str()
461                            .expect("opentelemetry-header had non-ascii value"),
462                    ));
463                }
464                args.push(format!(
465                    "--opentelemetry-max-batch-queue-size={opentelemetry_max_batch_queue_size}",
466                ));
467                args.push(format!(
468                    "--opentelemetry-max-export-batch-size={opentelemetry_max_export_batch_size}",
469                ));
470                args.push(format!(
471                    "--opentelemetry-max-concurrent-exports={opentelemetry_max_concurrent_exports}",
472                ));
473                args.push(format!(
474                    "--opentelemetry-sched-delay={}ms",
475                    opentelemetry_sched_delay.as_millis(),
476                ));
477                args.push(format!(
478                    "--opentelemetry-max-export-timeout={}ms",
479                    opentelemetry_max_export_timeout.as_millis(),
480                ));
481            }
482            #[cfg(feature = "tokio-console")]
483            if let Some(tokio_console_listen_addr) = tokio_console_listen_addr {
484                args.push(format!(
485                    "--tokio-console-listen-addr={}",
486                    tokio_console_listen_addr,
487                ));
488                args.push(format!(
489                    "--tokio-console-publish-interval={} us",
490                    tokio_console_publish_interval.as_micros(),
491                ));
492                args.push(format!(
493                    "--tokio-console-retention={} us",
494                    tokio_console_retention.as_micros(),
495                ));
496            }
497            if let Some(dsn) = sentry_dsn {
498                args.push(format!("--sentry-dsn={dsn}"));
499                for kv in sentry_tag {
500                    args.push(format!("--sentry-tag={}={}", kv.key, kv.value));
501                }
502            }
503            if let Some(environment) = sentry_environment {
504                args.push(format!("--sentry-environment={environment}"));
505            }
506
507            if opentelemetry_endpoint.is_some() || sentry_dsn.is_some() {
508                for kv in opentelemetry_resource {
509                    args.push(format!("--opentelemetry-resource={}={}", kv.key, kv.value));
510                }
511            }
512
513            args
514        };
515        service_config.args = Box::new(args_fn);
516        #[cfg(feature = "tokio-console")]
517        if self.tracing_args.tokio_console_listen_addr.is_some() {
518            service_config.ports.push(ServicePort {
519                name: "tokio-console".into(),
520                port_hint: 6669,
521            });
522        }
523        self.inner.ensure_service(id, service_config)
524    }
525
526    fn drop_service(&self, id: &str) -> Result<(), anyhow::Error> {
527        self.inner.drop_service(id)
528    }
529
530    async fn list_services(&self) -> Result<Vec<String>, anyhow::Error> {
531        self.inner.list_services().await
532    }
533
534    async fn flush(&self) -> Result<(), anyhow::Error> {
535        self.inner.flush().await
536    }
537
538    fn watch_services(&self) -> BoxStream<'static, Result<ServiceEvent, anyhow::Error>> {
539        self.inner.watch_services()
540    }
541
542    fn update_scheduling_config(
543        &self,
544        config: mz_orchestrator::scheduling_config::ServiceSchedulingConfig,
545    ) {
546        self.inner.update_scheduling_config(config)
547    }
548}
549
550/// Specifies the format of a stderr log message.
551#[derive(Debug, Clone, Default, clap::ValueEnum)]
552pub enum LogFormat {
553    /// Format as human readable, optionally colored text.
554    ///
555    /// Best suited for direct human consumption in a terminal.
556    #[default]
557    Text,
558    /// Format as JSON (in reality, JSONL).
559    ///
560    /// Best suited for ingestion in structured logging aggregators.
561    Json,
562}
563
564impl fmt::Display for LogFormat {
565    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
566        match self {
567            LogFormat::Text => f.write_str("text"),
568            LogFormat::Json => f.write_str("json"),
569        }
570    }
571}