/home/runner/work/lyquor/lyquor/toolchain/cli/src/lib.rs
Line | Count | Source |
1 | | //! Shared command-line support for Lyquor binaries. |
2 | | //! |
3 | | //! `lyquor-cli` keeps cross-binary concerns out of the node and tooling crates. It owns tracing |
4 | | //! initialization, environment-driven log filtering, build-version display, and Cargo build-script |
5 | | //! helpers used by binaries that otherwise have separate command surfaces. Command-specific parsing |
6 | | //! and behavior remain in the crates that expose those binaries. |
7 | | |
8 | | use anyhow::Context as _; |
9 | | use tonic::transport::{Channel, Endpoint}; |
10 | | use url::Url; |
11 | | |
12 | | use std::io::IsTerminal as _; |
13 | | |
14 | | use tracing_subscriber::{Layer as _, filter::EnvFilter, fmt::format::FmtSpan, registry::LookupSpan}; |
15 | | |
16 | | /// Cargo build-script helpers shared by Lyquor binaries. |
17 | | pub mod script; |
18 | | |
19 | | #[macro_export] |
20 | | macro_rules! build_version { |
21 | | () => { |
22 | | env!("LYQUOR_BUILD_VERSION") |
23 | | }; |
24 | | } |
25 | | |
26 | | /// Install the process-wide tracing subscriber from Lyquor logging environment variables. |
27 | 456 | pub fn setup_tracing() -> anyhow::Result<()> { |
28 | | use tracing_subscriber::prelude::*; |
29 | | |
30 | 456 | let env_filter = EnvFilter::builder() |
31 | 456 | .with_default_directive("info".parse().unwrap()) |
32 | 456 | .with_env_var("LYQUOR_LOG") |
33 | 456 | .from_env_lossy() |
34 | 456 | .add_directive("foundry_compilers=warn".parse().unwrap()) |
35 | 456 | .add_directive("cranelift=info".parse().unwrap()) |
36 | 456 | .add_directive("wasmtime=info".parse().unwrap()); |
37 | | |
38 | 456 | let span_events = { |
39 | 456 | let mut span_events = FmtSpan::NONE; |
40 | | |
41 | | // Default to no span lifecycle events: spans decorate the events that fire inside them, |
42 | | // so info-level cause spans stay free at steady state (see developer/debugging.md). |
43 | 456 | let s = std::env::var("LYQUOR_LOG_SPAN_EVENTS") |
44 | 456 | .unwrap_or_else(|_| "none".into()) |
45 | 456 | .split(',') |
46 | 456 | .map(|s| s.trim().to_lowercase()) |
47 | 456 | .collect::<Vec<_>>(); |
48 | 456 | for fmt_span in s { |
49 | 456 | match fmt_span.as_str() { |
50 | 456 | "new" => span_events |= FmtSpan::NEW0 , |
51 | 456 | "close" => span_events |= FmtSpan::CLOSE0 , |
52 | 456 | "enter" => span_events |= FmtSpan::ENTER0 , |
53 | 456 | "exit" => span_events |= FmtSpan::EXIT0 , |
54 | 456 | "active" => span_events |= FmtSpan::ACTIVE0 , |
55 | 456 | "full" => span_events |= FmtSpan::FULL0 , |
56 | 456 | _ => (), |
57 | | } |
58 | | } |
59 | 456 | span_events |
60 | | }; |
61 | | |
62 | 456 | let registry = tracing_subscriber::registry(); |
63 | | |
64 | | #[cfg(feature = "tokio-console")] |
65 | | let registry = registry.with(console_subscriber::spawn()); |
66 | | |
67 | 456 | let format = match std::env::var("LYQUOR_LOG_FORMAT") |
68 | 456 | .unwrap_or_else(|_| "full".into()) |
69 | 456 | .to_lowercase() |
70 | 456 | .as_str() |
71 | | { |
72 | 456 | "compact" => LogFormat::Compact0 , |
73 | 456 | "pretty" => LogFormat::Pretty0 , |
74 | 456 | "json" => LogFormat::Json0 , |
75 | 456 | _ => LogFormat::Full, |
76 | | }; |
77 | 456 | let ansi = std::io::stderr().is_terminal() && std::env::var_os0 ("NO_COLOR").is_none_or0 (|value| value0 .is_empty0 ()); |
78 | 456 | registry |
79 | 456 | .with(format_layer(format, ansi, span_events, std::io::stderr, env_filter)) |
80 | 456 | .init(); |
81 | | |
82 | 456 | Ok(()) |
83 | 456 | } |
84 | | |
85 | | #[derive(Clone, Copy)] |
86 | | enum LogFormat { |
87 | | Full, |
88 | | Compact, |
89 | | Pretty, |
90 | | Json, |
91 | | } |
92 | | |
93 | 457 | fn format_layer<S, W>( |
94 | 457 | format: LogFormat, ansi: bool, span_events: FmtSpan, writer: W, env_filter: EnvFilter, |
95 | 457 | ) -> Box<dyn tracing_subscriber::Layer<S> + Send + Sync> |
96 | 457 | where |
97 | 457 | S: tracing::Subscriber + for<'lookup> LookupSpan<'lookup>, |
98 | 457 | W: for<'writer> tracing_subscriber::fmt::MakeWriter<'writer> + Send + Sync + 'static, |
99 | | { |
100 | 457 | let layer = tracing_subscriber::fmt::layer() |
101 | 457 | .with_thread_ids(true) |
102 | 457 | .with_writer(writer) |
103 | 457 | .with_span_events(span_events) |
104 | 457 | .with_ansi(ansi); |
105 | | |
106 | 457 | match format { |
107 | 0 | LogFormat::Compact => layer.compact().with_filter(env_filter).boxed(), |
108 | 0 | LogFormat::Pretty => layer.pretty().with_filter(env_filter).boxed(), |
109 | 1 | LogFormat::Json => layer |
110 | 1 | .json() |
111 | 1 | .with_current_span(true) |
112 | 1 | .with_span_list(true) |
113 | 1 | .with_filter(env_filter) |
114 | 1 | .boxed(), |
115 | 456 | LogFormat::Full => layer.with_filter(env_filter).boxed(), |
116 | | } |
117 | 457 | } |
118 | | |
119 | | /// Render the startup banner using the supplied build version string. |
120 | 0 | pub fn format_logo_banner(version: &str) -> String { |
121 | | const LOGO: &str = r" |
122 | | __ _ _ __ _ _ __ ____ _o/_ |
123 | | (..) (.\/.) / \ / )( \ / \( _ \ \##/ |
124 | | /.(_/\ )../ ( O )) \/ (( O )) / || |
125 | | \..../(../te \__\)\____/ \__/(__\_)um _||_"; |
126 | | |
127 | 0 | format!( |
128 | | "{LOGO} |
129 | | |
130 | | Version: {version:>33} |
131 | | =========================================\n", |
132 | | ) |
133 | 0 | } |
134 | | |
135 | | /// Converts a node websocket or HTTP endpoint into the base gRPC HTTP endpoint. |
136 | 7 | pub fn grpc_api_endpoint(endpoint: &str) -> anyhow::Result<String> { |
137 | 7 | let mut url = |
138 | 7 | Url::parse(endpoint).map_err(|err| anyhow::anyhow!0 ("Invalid node API endpoint `{endpoint}`: {err}"))?0 ; |
139 | 7 | let scheme = match url.scheme() { |
140 | 7 | "ws" | "http"5 => "http"4 , |
141 | 3 | "wss" | "https"2 => "https", |
142 | 0 | other => anyhow::bail!("Unsupported node API endpoint scheme `{other}`"), |
143 | | }; |
144 | 7 | url.set_scheme(scheme) |
145 | 7 | .map_err(|_| anyhow::anyhow!0 ("Failed to convert API endpoint scheme for `{endpoint}`"))?0 ; |
146 | 7 | url.set_path("/"); |
147 | 7 | url.set_query(None); |
148 | 7 | url.set_fragment(None); |
149 | 7 | Ok(url.to_string()) |
150 | 7 | } |
151 | | |
152 | | /// Builds the tonic endpoint for a node gRPC API endpoint. |
153 | 2 | pub fn grpc_api_channel_endpoint(endpoint: &str) -> anyhow::Result<(String, Endpoint)> { |
154 | 2 | let grpc_endpoint = grpc_api_endpoint(endpoint)?0 ; |
155 | 2 | if grpc_endpoint.starts_with("https://") { |
156 | 1 | // Tonic's rustls transport needs a process-level crypto provider. If another provider is |
157 | 1 | // already installed, keep it. |
158 | 1 | let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); |
159 | 1 | } |
160 | 2 | let endpoint = |
161 | 2 | Endpoint::new(grpc_endpoint.clone()).with_context(|| format!0 ("Invalid gRPC endpoint `{grpc_endpoint}`"))?0 ; |
162 | 2 | Ok((grpc_endpoint, endpoint)) |
163 | 2 | } |
164 | | |
165 | | /// Connects to a node gRPC API endpoint with tonic's HTTP and HTTPS transport support. |
166 | 1 | pub async fn connect_grpc_api_channel(endpoint: &str, service_name: &str) -> anyhow::Result<(String, Channel)> { |
167 | 1 | let (grpc_endpoint, endpoint) = grpc_api_channel_endpoint(endpoint)?0 ; |
168 | 1 | let channel0 = endpoint |
169 | 1 | .connect() |
170 | 1 | .await |
171 | 1 | .with_context(|| format!("Failed to connect to {service_name} at `{grpc_endpoint}`"))?; |
172 | 0 | Ok((grpc_endpoint, channel)) |
173 | 1 | } |
174 | | |
175 | | #[cfg(test)] |
176 | | mod tests { |
177 | | use tracing_subscriber::prelude::*; |
178 | | |
179 | | use super::*; |
180 | | |
181 | | #[test] |
182 | 1 | fn json_format_accepts_structured_event_in_span() { |
183 | 1 | let subscriber = tracing_subscriber::registry().with(format_layer( |
184 | 1 | LogFormat::Json, |
185 | | true, |
186 | | FmtSpan::NONE, |
187 | | std::io::sink, |
188 | 1 | EnvFilter::new("trace"), |
189 | | )); |
190 | | |
191 | 1 | tracing::subscriber::with_default(subscriber, || { |
192 | 1 | let span = tracing::info_span!("request", request_id = 7); |
193 | 1 | let _entered = span.enter(); |
194 | 1 | tracing::info!(answer = 42, "processed request"); |
195 | 1 | }); |
196 | 1 | } |
197 | | } |