Coverage Report

Created: 2026-09-11 07:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}