Skip to main content

slack_rs/
debug.rs

1//! Debug logging helpers.
2//!
3//! This crate primarily prints user-facing progress messages to stdout.
4//! Any verbose diagnostics should be gated behind an environment variable
5//! and must never leak secrets (tokens, client secrets, etc.).
6
7use serde_json::Value;
8
9/// Debug level for output control
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum DebugLevel {
12    /// No debug output
13    Off,
14    /// Basic debug output (--debug)
15    Debug,
16    /// Verbose trace output (--trace)
17    Trace,
18}
19
20/// Get the current debug level from environment variable or flags
21///
22/// Priority:
23/// 1. --trace flag → Trace
24/// 2. --debug flag → Debug
25/// 3. SLACK_RS_DEBUG env → Debug
26/// 4. Default → Off
27pub fn get_debug_level(args: &[String]) -> DebugLevel {
28    // Check for --trace flag
29    if args.iter().any(|arg| arg == "--trace") {
30        return DebugLevel::Trace;
31    }
32
33    // Check for --debug flag
34    if args.iter().any(|arg| arg == "--debug") {
35        return DebugLevel::Debug;
36    }
37
38    // Check environment variable
39    if enabled() {
40        return DebugLevel::Debug;
41    }
42
43    DebugLevel::Off
44}
45
46/// Returns true when debug logging is enabled.
47///
48/// Enable with `SLACK_RS_DEBUG=1` (also accepts: true/yes/on).
49pub fn enabled() -> bool {
50    match std::env::var("SLACK_RS_DEBUG") {
51        Ok(v) => {
52            let v = v.trim().to_ascii_lowercase();
53            matches!(v.as_str(), "1" | "true" | "yes" | "on")
54        }
55        Err(_) => false,
56    }
57}
58
59/// Print a debug line to stderr when enabled.
60pub fn log(msg: impl AsRef<str>) {
61    if enabled() {
62        eprintln!("DEBUG: {}", msg.as_ref());
63    }
64}
65
66/// Print debug information for API call context
67///
68/// Outputs to stderr when debug level is Debug or higher.
69/// Never outputs secrets (tokens, client_secret).
70pub fn log_api_context(
71    level: DebugLevel,
72    profile_name: Option<&str>,
73    token_store_backend: &str,
74    token_type: &str,
75    method: &str,
76    endpoint: &str,
77) {
78    if level >= DebugLevel::Debug {
79        eprintln!("DEBUG: Profile: {}", profile_name.unwrap_or("<none>"));
80        eprintln!("DEBUG: Token store: {}", token_store_backend);
81        eprintln!("DEBUG: Token type: {}", token_type);
82        eprintln!("DEBUG: API method: {}", method);
83        eprintln!("DEBUG: Endpoint: {}", endpoint);
84    }
85}
86
87/// Print trace-level debug information
88///
89/// Only outputs when debug level is Trace.
90pub fn log_trace(level: DebugLevel, msg: impl AsRef<str>) {
91    if level >= DebugLevel::Trace {
92        eprintln!("TRACE: {}", msg.as_ref());
93    }
94}
95
96/// Log Slack error code if present in response
97///
98/// Outputs to stderr when debug level is Debug or higher.
99pub fn log_error_code(level: DebugLevel, response: &Value) {
100    if level >= DebugLevel::Debug {
101        if let Some(ok) = response.get("ok").and_then(|v| v.as_bool()) {
102            if !ok {
103                if let Some(error_code) = response.get("error").and_then(|v| v.as_str()) {
104                    eprintln!("DEBUG: Slack error code: {}", error_code);
105                }
106            }
107        }
108    }
109}
110
111/// Returns a safe, non-reversible hint for a token.
112///
113/// Never returns any part of the token value.
114pub fn token_hint(token: &str) -> String {
115    let kind = if token.starts_with("xoxb-") {
116        "xoxb"
117    } else if token.starts_with("xoxp-") {
118        "xoxp"
119    } else if token.starts_with("xoxa-") {
120        "xoxa"
121    } else if token.starts_with("xoxr-") {
122        "xoxr"
123    } else if token.starts_with("xoxs-") {
124        "xoxs"
125    } else {
126        "token"
127    };
128
129    format!("{} (len={})", kind, token.len())
130}
131
132/// Redact token-like values from a JSON string.
133///
134/// This is intentionally conservative: any string that looks like a Slack token
135/// (starts with "xox") is replaced.
136pub fn redact_json_secrets(json: &str) -> String {
137    let Ok(mut v) = serde_json::from_str::<Value>(json) else {
138        return "<non-json body>".to_string();
139    };
140
141    redact_value_in_place(&mut v);
142    serde_json::to_string(&v).unwrap_or_else(|_| "<unserializable json>".to_string())
143}
144
145fn redact_value_in_place(v: &mut Value) {
146    match v {
147        Value::Object(map) => {
148            for (_k, child) in map.iter_mut() {
149                redact_value_in_place(child);
150            }
151        }
152        Value::Array(items) => {
153            for child in items.iter_mut() {
154                redact_value_in_place(child);
155            }
156        }
157        Value::String(s) => {
158            let trimmed = s.trim();
159            if trimmed.starts_with("xox") {
160                *s = "<redacted>".to_string();
161            }
162        }
163        _ => {}
164    }
165}