grafana 0.1.3

Ergonomic Rust SDK for Grafana's HTTP API, with async and blocking clients.
Documentation
use serde_json::Value;

pub(crate) fn redact_body_snippet(bytes: &[u8], byte_limit: usize) -> Option<String> {
    if byte_limit == 0 || bytes.is_empty() {
        return None;
    }

    let value = serde_json::from_slice::<Value>(bytes).ok();
    if let Some(mut value) = value {
        redact_json_value(&mut value);
        let json = serde_json::to_string(&value).ok()?;
        return Some(truncate_utf8_bytes(&json, byte_limit));
    }

    let text = String::from_utf8_lossy(bytes);
    Some(truncate_utf8_bytes(&redact_text(&text), byte_limit))
}

fn redact_json_value(value: &mut Value) {
    match value {
        Value::Object(map) => {
            for (key, value) in map.iter_mut() {
                if should_redact_key(key) {
                    *value = Value::String("<redacted>".to_owned());
                } else {
                    redact_json_value(value);
                }
            }
        }
        Value::Array(items) => {
            for item in items.iter_mut() {
                redact_json_value(item);
            }
        }
        _ => {}
    }
}

fn should_redact_key(key: &str) -> bool {
    matches!(
        key.to_ascii_lowercase().as_str(),
        "authorization"
            | "cookie"
            | "password"
            | "secret"
            | "client_secret"
            | "token"
            | "access_token"
            | "refresh_token"
            | "api_key"
            | "apikey"
    )
}

fn redact_text(text: &str) -> String {
    let mut output = String::with_capacity(text.len());
    for line in text.lines() {
        if let Some(redacted) = redact_header_line(line, "authorization") {
            output.push_str(&redacted);
        } else if let Some(redacted) = redact_header_line(line, "cookie") {
            output.push_str(&redacted);
        } else {
            output.push_str(&redact_inline_token(line));
        }
        output.push('\n');
    }

    if output.ends_with('\n') && !text.ends_with('\n') {
        output.pop();
    }
    output
}

fn redact_header_line(line: &str, header_name: &str) -> Option<String> {
    let (name, value) = line.split_once(':')?;
    if name.trim().eq_ignore_ascii_case(header_name) {
        return Some(format!("{}: <redacted>", name.trim()));
    }
    let _ = value;
    None
}

fn redact_inline_token(line: &str) -> String {
    let line = redact_prefixed_token(line, "Bearer ");
    redact_prefixed_token(&line, "Basic ")
}

fn redact_prefixed_token(line: &str, prefix: &str) -> String {
    let Some(start) = line.find(prefix) else {
        return line.to_owned();
    };

    let token_start = start + prefix.len();
    let mut token_end = token_start;
    for (idx, ch) in line[token_start..].char_indices() {
        if ch.is_whitespace() {
            break;
        }
        token_end = token_start + idx + ch.len_utf8();
    }

    if token_end == token_start {
        return line.to_owned();
    }

    let mut out = String::with_capacity(line.len());
    out.push_str(&line[..token_start]);
    out.push_str("<redacted>");
    out.push_str(&line[token_end..]);
    out
}

fn truncate_utf8_bytes(input: &str, byte_limit: usize) -> String {
    if input.len() <= byte_limit {
        return input.to_owned();
    }

    let mut end = 0;
    for (idx, _) in input.char_indices() {
        if idx > byte_limit {
            break;
        }
        end = idx;
    }

    let mut truncated = input[..end].to_owned();
    truncated.push_str("...(truncated)");
    truncated
}