1use 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#[derive(Derivative, Clone, clap::Parser)]
55#[derivative(Debug)]
56pub struct TracingCliArgs {
57 #[clap(
90 long,
91 env = "STARTUP_LOG_FILTER",
92 value_name = "FILTER",
93 default_value = "info"
94 )]
95 pub startup_log_filter: CloneableEnvFilter,
96 #[clap(long, env = "LOG_FORMAT", default_value_t, value_enum)]
98 pub log_format: LogFormat,
99 #[clap(long, env = "LOG_PREFIX")]
103 pub log_prefix: Option<String>,
104 #[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 #[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 #[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 #[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 #[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 #[clap(long, env = "OPENTELEMETRY_ENDPOINT")]
156 pub opentelemetry_endpoint: Option<String>,
157 #[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 #[clap(
179 long,
180 env = "STARTUP_OPENTELEMETRY_FILTER",
181 requires = "opentelemetry_endpoint",
182 default_value = "info"
183 )]
184 pub startup_opentelemetry_filter: CloneableEnvFilter,
185 #[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 #[cfg(feature = "tokio-console")]
203 #[clap(long, env = "TOKIO_CONSOLE_LISTEN_ADDR")]
204 pub tokio_console_listen_addr: Option<SocketAddr>,
205 #[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 #[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 #[clap(long, env = "SENTRY_DSN")]
231 pub sentry_dsn: Option<String>,
232 #[clap(long, env = "SENTRY_ENVIRONMENT")]
238 pub sentry_environment: Option<String>,
239 #[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 #[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
341pub const NO_SENTRY_TARGET: &str = "mz_no_sentry";
344
345#[allow(deprecated)]
346pub fn mz_sentry_event_filter(meta: &tracing::Metadata<'_>) -> EventFilter {
347 if meta.target() == "librdkafka" || meta.target() == NO_SENTRY_TARGET {
349 return EventFilter::Ignore;
350 }
351
352 sentry_tracing::default_event_filter(meta)
354}
355
356pub struct StaticTracingConfig {
358 pub service_name: &'static str,
360 pub build_info: BuildInfo,
362}
363
364#[derive(Debug)]
366pub struct TracingOrchestrator {
367 inner: Arc<dyn Orchestrator>,
368 tracing_args: TracingCliArgs,
369}
370
371impl TracingOrchestrator {
372 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#[derive(Debug, Clone, Default, clap::ValueEnum)]
554pub enum LogFormat {
555 #[default]
559 Text,
560 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}