Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
See [Keep a Changelog](https://keepachangelog.com/) for details.
This project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added

- Tiered diagnostic logging via `-v` / `-vv` / `-vvv` (or `verbose = 0..=3` in the config file). All logs go to stderr,
so they don't pollute `--format=json` output piped through `jq` or similar tools.
- `-v` shows warnings: when a provider fails and we fall back to the next one, when OpenUV is unavailable, or when
a translation key is missing.
- `-vv` adds one-line summaries of every HTTP request (provider, operation, status, latency) and geocoding cache
hits/misses. URLs and API keys are not shown at this level.
- `-vvv` adds full request URLs and truncated response bodies for debugging. URLs at this level include API keys —
don't share captured output verbatim.
- In live mode, log lines produced inside the alternate screen are buffered and flushed to stderr after you exit, so
the UI stays clean during the session and you still see what happened on quit.

### Fixed

- A transient OpenUV failure no longer aborts the weather fetch. UV index is silently omitted (with a `-v` warning if
enabled) and the rest of the data still displays normally.

## [0.5.0] - 2026-05-06

### Added
Expand Down
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,18 @@ use_geocoding_cache = false

#### Verbosity level

Verbosity level can be set with `verbose` option (0 - no verbose, 1 - show errors)
`rustormy` writes diagnostic logs to stderr. The level is set via `verbose` in the config file or by repeating `-v`
on the command line:

| Level | Flag | What you'll see |
|-------|---------|------------------------------------------------------------------------------|
| 0 | (none) | Silent (only fatal errors). |
| 1 | `-v` | Warnings: provider fallback, OpenUV failures, missing translations. |
| 2 | `-vv` | + Per-request summaries (provider, operation, HTTP status, latency) and cache hits/misses. |
| 3 | `-vvv` | + Full request URLs (including API keys) and truncated response bodies. |

Logs always go to stderr, so `2>/dev/null` keeps stdout clean for piping. In live mode, logs produced inside the
alternate screen are buffered and flushed to stderr after you exit, so the UI stays clean during the session.

```toml
verbose = 0
Expand Down Expand Up @@ -302,6 +313,15 @@ Options:
![Night mode icons](.github/assets/night.png)
![JSON output: `rustormy -c Ajax -o json`](.github/assets/json.png)

### Diagnostic output

```sh
rustormy -c London -v # warnings only (e.g. when a provider fails and we fall back)
rustormy -c London -vv # one-line per-request summaries and cache hits/misses
rustormy -c London -vvv # full request URLs and response bodies (for debugging only)
rustormy -c London -o json -vv 2>/dev/null | jq . # logs go to stderr; stdout stays pure JSON
```

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
Expand Down
9 changes: 5 additions & 4 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ pub struct App {
impl App {
pub fn new() -> Result<App, RustormyError> {
let mut config = Config::new(Cli::new())?;
if !config.live_mode() {
crate::logging::init(config.verbose(), config.format().use_colors);
}
let client = Client::builder()
.user_agent(concat!("rustormy/", env!("CARGO_PKG_VERSION")))
.timeout(Duration::from_secs(config.connect_timeout()))
Expand All @@ -43,15 +46,13 @@ impl App {
loop {
match self.provider.get_weather(&self.client, &self.config) {
Ok(mut weather) => {
enrich(&mut weather, &self.client, &self.config)?;
enrich(&mut weather, &self.client, &self.config);
return Ok(weather);
}
Err(error) => match error {
RustormyError::ApiReturnedError(_) | RustormyError::HttpRequestFailed(_) => {
let p: Provider = (&self.provider).into();
if self.config.verbose() >= 1 {
eprintln!("Provider {p:?} failed: {error:?}");
}
crate::warn!("Provider {p:?} failed: {error}");
let Some(next) = self.config.take_next_provider() else {
return Err(error);
};
Expand Down
2 changes: 2 additions & 0 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ pub fn get_cached_location(

if cache_path.exists() {
let location: Location = serde_json::from_reader(File::open(cache_path)?)?;
crate::info!("cache hit \"{city}\"");
Ok(Some(location))
} else {
crate::info!("cache miss \"{city}\"");
Ok(None)
}
}
Expand Down
49 changes: 44 additions & 5 deletions src/display/translations.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::models::Language;
use std::collections::HashMap;
use std::sync::LazyLock;
use std::collections::{HashMap, HashSet};
use std::sync::{LazyLock, Mutex, OnceLock};

macro_rules! translations {
($($key:expr => {
Expand Down Expand Up @@ -415,10 +415,49 @@ static TRANSLATIONS: LazyLock<HashMap<&'static str, HashMap<&'static str, &'stat
},
};

fn missing_keys_set() -> &'static Mutex<HashSet<&'static str>> {
static SET: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
SET.get_or_init(|| Mutex::new(HashSet::new()))
}

pub fn ll(lang: Language, key: &'static str) -> &'static str {
TRANSLATIONS
if let Some(translated) = TRANSLATIONS
.get(lang.code())
.and_then(|translations| translations.get(key))
// TODO: Add logging for missing translations
.unwrap_or(&key)
{
return translated;
}
let mut seen = missing_keys_set()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if seen.insert(key) {
crate::warn!("missing translation for key: {key}");
}
key
}

#[cfg(test)]
pub(crate) fn missing_keys_seen(key: &'static str) -> bool {
missing_keys_set().lock().is_ok_and(|s| s.contains(key))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn missing_keys_are_logged_only_once() {
let key = "test_missing_key_dedup";
let _ = ll(Language::English, key);
let _ = ll(Language::English, key);
let _ = ll(Language::English, key);
assert!(super::missing_keys_seen(key));
}

#[test]
fn known_keys_do_not_register_as_missing() {
let key = "Clear";
let _ = ll(Language::English, key);
assert!(!super::missing_keys_seen(key));
}
}
10 changes: 9 additions & 1 deletion src/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,20 @@ pub fn render(
}

pub fn run(app: &mut App) -> Result<(), RustormyError> {
let level = app.config().verbose();
let use_colors = app.config().format().use_colors;
let _capture = crate::logging::init_with_capture(level, use_colors);

// First fetch runs before entering the alt screen so the user's
// existing terminal contents stay visible during the initial API call.
let mut weather = app.fetch_with_fallback()?;
let mut now = Local::now();

let _guard = TerminalGuard::enter()?;
// Drain any logs produced during the first fetch directly to stderr
// so they remain in the user's scrollback after the alt-screen exits.
crate::logging::flush_capture();

let _terminal = TerminalGuard::enter()?;
let mut stdout = io::stdout();

loop {
Expand Down
Loading
Loading