mz_deploy/log.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//! Logging utilities for mz-deploy.
11//!
12//! This module provides a simple verbose logging system that can be enabled
13//! via the `--verbose` CLI flag. When verbose mode is enabled, diagnostic
14//! messages are printed to stdout to help users understand what the tool
15//! is doing.
16
17use std::sync::atomic::{AtomicBool, Ordering};
18
19/// Global verbose mode flag.
20///
21/// This is a thread-safe atomic boolean that stores whether verbose
22/// logging is enabled. It uses relaxed memory ordering since the exact
23/// timing of when verbose mode is enabled/disabled doesn't matter.
24static VERBOSE: AtomicBool = AtomicBool::new(false);
25
26/// Global JSON output flag.
27static JSON_OUTPUT: AtomicBool = AtomicBool::new(false);
28
29/// Global quiet mode flag.
30static QUIET: AtomicBool = AtomicBool::new(false);
31
32/// Enable or disable JSON output mode.
33pub fn set_json_output(v: bool) {
34 JSON_OUTPUT.store(v, Ordering::Relaxed);
35}
36
37/// Check if JSON output mode is currently enabled.
38pub fn json_output_enabled() -> bool {
39 JSON_OUTPUT.load(Ordering::Relaxed)
40}
41
42/// Enable or disable quiet mode.
43pub fn set_quiet(v: bool) {
44 QUIET.store(v, Ordering::Relaxed);
45}
46
47/// Check if quiet mode is currently enabled.
48pub fn quiet_enabled() -> bool {
49 QUIET.load(Ordering::Relaxed)
50}
51
52/// Enable or disable verbose logging.
53pub fn set_verbose(v: bool) {
54 VERBOSE.store(v, Ordering::Relaxed);
55}
56
57/// Check if verbose logging is currently enabled.
58pub fn verbose_enabled() -> bool {
59 VERBOSE.load(Ordering::Relaxed)
60}
61
62/// Check if color is enabled on stderr.
63///
64/// Reads `NO_COLOR` / `FORCE_COLOR` / `CLICOLOR_FORCE` and tty status via
65/// the `supports-color` crate. This is the same source `owo-colors` consults
66/// internally.
67pub fn color_enabled() -> bool {
68 supports_color::on_cached(supports_color::Stream::Stderr).is_some()
69}
70
71/// Print a message only when verbose mode is enabled.
72#[macro_export]
73#[allow(clippy::print_stderr)]
74macro_rules! verbose {
75 ($($arg:tt)*) => {
76 if $crate::log::verbose_enabled() {
77 eprintln!($($arg)*);
78 }
79 };
80}
81
82/// A value that can be rendered as both human-readable text and JSON.
83///
84/// Render is the core pattern for command output in mz-deploy. Instead of
85/// branching on `json_output_enabled()` at every call site, commands define a
86/// single struct that implements both `Display` (for humans) and `Serialize`
87/// (for machines), then hand it to [`output()`]:
88///
89/// ```ignore
90/// #[derive(serde::Serialize)]
91/// struct MyResult { name: String, count: usize }
92///
93/// impl fmt::Display for MyResult {
94/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95/// write!(f, " ✓ Processed {} items for '{}'", self.count, self.name)
96/// }
97/// }
98///
99/// log::output(&MyResult { name, count });
100/// ```
101///
102/// Guidelines:
103/// - **Use `output()` with a Render struct** when a command has a single result
104/// that should be available in both text and JSON form. This is the default.
105/// - **Use `output_json()`** for paths with no human representation, like NDJSON
106/// streaming or machine-only pre-execution plan dumps.
107/// - **Use `info!()`** for supplementary stderr messages (hints, progress) that
108/// shouldn't appear in JSON output.
109pub trait Render: std::fmt::Display + serde::Serialize {}
110impl<T: std::fmt::Display + serde::Serialize> Render for T {}
111
112/// Output a value: JSON to stdout when `--output json`, human text to stderr otherwise.
113///
114/// Silenced by `--quiet`.
115#[allow(clippy::print_stdout, clippy::print_stderr)]
116pub fn output(value: &impl Render) {
117 if quiet_enabled() {
118 return;
119 }
120 if json_output_enabled() {
121 println!("{}", serde_json::to_string(value).unwrap());
122 } else {
123 eprintln!("{value}");
124 }
125}
126
127/// Print an informational message to stderr. Silenced by `--quiet`.
128#[macro_export]
129macro_rules! info {
130 ($($arg:tt)*) => {
131 if !$crate::log::quiet_enabled() {
132 #[allow(clippy::print_stderr)]
133 { eprintln!($($arg)*); }
134 }
135 };
136}
137
138/// Write a JSON-only value to stdout.
139///
140/// Use this for paths that have no human-readable representation (NDJSON streaming,
141/// machine-only dry-run plans). Prefer `output()` with a Render type when both
142/// human and JSON representations exist.
143#[allow(clippy::print_stdout)]
144pub fn output_json(value: &impl serde::Serialize) {
145 println!("{}", serde_json::to_string(value).unwrap());
146}
147
148/// Print a successful deployment's ID to stdout.
149///
150/// This is the one intentional stdout write in human mode. `output()` sends
151/// the pretty summary to stderr so terminals still show it, while the deploy
152/// ID — the machine-useful handoff to `wait`/`promote` — goes to stdout on
153/// its own line. That split is what lets callers compose:
154///
155/// ```text
156/// DEPLOY_ID=$(mz-deploy stage)
157/// mz-deploy stage | xargs mz-deploy wait
158/// ```
159///
160/// In JSON mode we skip this, because the structured result already carries
161/// `deploy_id` on stdout and a bare ID would corrupt the JSON stream. The
162/// `print_stdout` allow is intentional for exactly this reason.
163#[allow(clippy::print_stdout)]
164pub fn print_deploy_id(deploy_id: &str) {
165 if json_output_enabled() {
166 return;
167 }
168 println!("{deploy_id}");
169}
170
171/// Print an informational message to stderr without a trailing newline. Silenced by `--quiet`.
172#[macro_export]
173macro_rules! info_nonl {
174 ($($arg:tt)*) => {
175 if !$crate::log::quiet_enabled() {
176 #[allow(clippy::print_stderr)]
177 { eprint!($($arg)*); }
178 }
179 };
180}