Skip to main content

Resilience and failure modes

SteamLogger's operating principle is simple: a session must still be logged when everything optional breaks. This page enumerates what can go wrong and exactly what happens.

Failure matrix​

FailureDetected inConsequenceRecovery
Config file missing / invalid TOML / missing required keyconfig::loadFatal: message on stderr, exit code 1Fix the config and restart
SIGINT handler cannot be installedmain::runFatal: exit code 1Rare; usually a platform restriction
Steam API unreachable, HTTP 5xx/429, timeoutSteamClient::current_gamepoll error: … on stderr. Tracker untouched: an open session stays open, a closed one stays closedNext tick
Steam returns valid JSON with no gameidsummary_to_current_gameTreated as "not playing" → the session is closedNext tick re-opens if you are in fact playing
A row has an unparseable steamid/gameidparse_player_summariesThat row is skipped; others are kept—
GetFriendList or batch summaries failenrich::analyzeWhole enrichment pass returns Enrichment::default(); apply_meta then clears map/server/friends for this writeNext tick
Game server does not answer A2S_INFOenrich::server_and_mapserver and map omitted; friends data unaffectedNext tick
A2S reply is malformed or a repeated challengea2s::parse_responseSame as above — error swallowed by server_and_mapNext tick
apply_meta index out of boundsmain::runenrichment failed: … on stderr, entry unchangedNext tick
Log file cannot be written (permissions, disk full)storage::write_logfailed to save log: … on stderr; in-memory log is still correctNext tick retries the full write
Log file unreadable/corrupt at startupmain::runSilently starts from an empty log — see belowManual
Process killed (SIGKILL, power loss)—At most one interval of state is lost; the open session is resumed on restartAutomatic

The three guarantees​

Atomic writes​

storage::write_log never writes into the target file. It serialises to <path>.tmp and then rename(2)s it over the destination, which is atomic within a filesystem on both Unix and Windows (MoveFileEx semantics via std::fs::rename). A reader therefore sees either the complete previous file or the complete new one.

write(steamlog.json.tmp) ← may be interrupted; target untouched
rename(.tmp → steamlog.json) ← atomic swap

:::note No fsync The implementation does not call File::sync_all() on the temp file or the directory. A kernel panic or power cut in the window between write and the filesystem's own flush can still lose the most recent write on some filesystems — but it cannot produce a half-written JSON file. For a session logger that rewrites every 30 seconds, that trade-off is intentional. :::

No fabricated data​

A failed poll never changes state. This matters more than it sounds: if a transient API error were treated as "no game running", every outage would split one session into two and insert a fake gap. Only an explicit Ok(None) — Steam saying "this profile is not in a game" — closes a session.

Degradation, not failure​

Every enrichment path returns Option/Vec rather than propagating an error:

// enrich.rs — a failed friend list is an empty Enrichment, not an Err
let Ok(friends) = client.friends(me) else { return Enrichment::default(); };

// enrich.rs — a failed A2S query is simply "no server, no map"
match a2s.query(addr) { Ok(info) => (Some(info.name), Some(info.map)), Err(_) => (None, None) }

Startup read failures are swallowed​

src/main.rs
let mut tracker = Tracker::new(storage::read_log(&config.output_file).unwrap_or_default());

read_log distinguishes "file does not exist" (Ok(empty)) from "file is corrupt" (Err), but main collapses both with unwrap_or_default(). The practical effect:

  • A missing file is normal — the first run creates it.
  • A corrupt or unparseable file is treated as empty, and the first successful write replaces it. Previous history is lost.

:::danger Validate before restarting If you hand-edit steamlog.json, run jq . steamlog.json first. If the file is already damaged, move it aside before starting SteamLogger so the daemon cannot overwrite your only copy. A library consumer can avoid the behaviour entirely by handling read_log's Err instead of defaulting — see Library usage. :::

Timeouts, not retries​

OperationBoundWhere
Any Steam HTTP request10 sSteamClient::new (configurable via with_timeout)
A2S_INFO UDP read3 sA2sClient::new (field is private; only tests change it)
Sleep between ticksmax(poll_interval_secs, 5)main::run

There is no retry logic, no exponential backoff and no circuit breaker. The poll interval is the retry schedule. If Steam rate-limits you (HTTP 429), the request fails, the error is printed, and the next tick tries again — so lower intervals do not recover faster, they just fail more often.

What is deliberately not handled​

  • No log rotation or pruning. The file grows with your history and is rewritten in full every tick. Thousands of sessions are fine (a few hundred KB); millions would not be.
  • No concurrent-writer protection. Two processes pointed at the same output_file will clobber each other. There is no lock file.
  • No SIGTERM handling. Only SIGINT is caught (ctrlc). Service managers should send SIGINT for the cleanest shutdown — see Running continuously.
  • No structured logging. Diagnostics go to stdout/stderr as plain text: poll error: …, enrichment failed: …, failed to save log: ….

Reading the diagnostics​

MessageStreamMeaning
SteamLogger running. Log file: … (Ctrl+C to stop)stdoutStartup succeeded
poll error: fetching player summaries: …stderrThe Steam request failed; state untouched
poll error: … request failedstderrSteam answered with a non-success HTTP status
enrichment failed: entry index N out of boundsstderrInternal inconsistency; the entry was not updated
failed to save log: …stderrFilesystem problem; the in-memory log is intact
failed to load config from …stderrFatal, exit 1
stoppedstdoutClean shutdown, exit 0

Context strings come from anyhow and compose: fetching player summaries → the underlying reqwest error → the underlying I/O error. Troubleshooting recipes for each are in Troubleshooting.