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