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
28 changes: 15 additions & 13 deletions crates/bestool/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ Alias: t
* `download` — Download Tamanu artifacts
* `find` — Find Tamanu installations
* `greenmask-config` — Generate a Greenmask config file
* `logs` — Tail logs for a Tamanu service, in a supervisor-agnostic way.
* `logs` — Tail logs for tamanu services and (optionally) caddy.
* `meta-ticket` — Generate a meta-ticket for this Tamanu server
* `psql` — Connect to Tamanu's database
* `restart` — Rolling-restart all running tamanu services.
Expand Down Expand Up @@ -1688,31 +1688,33 @@ Generate a Greenmask config file

## `bestool tamanu logs`

Tail logs for a Tamanu service, in a supervisor-agnostic way.
Tail logs for tamanu services and (optionally) caddy.

On Linux this drives `journalctl -u`; on Windows it reads pm2's log files
directly. The service name is matched as a substring against the expected
service list, so `tamanu logs api` picks up
`tamanu-{central,facility}-api@*` on systemd and `tamanu-api` on pm2.
Each NAME is matched as a substring against the expected-Up service
list, so `tamanu logs api` picks up `tamanu-{central,facility}-api@*`
on systemd and `tamanu-api` on pm2. Multiple names combine: `tamanu
logs api fhir` tails both. With no names at all, every expected-Up
tamanu service is tailed alongside caddy.

The special name `caddy` tails the caddy service: from `journalctl -u
caddy.service` on Linux, and from `.log` files under `C:\Caddy\logs` (or
`C:\Caddy`) on Windows. Caddy emits JSON-per-line logs; bestool detects
these and applies opportunistic syntax highlighting.
The literal name `caddy` is recognised as a pseudo-service that
tails caddy: from `journalctl -u caddy.service` on Linux, and from
`.log` files under `C:\Caddy\logs` (or `C:\Caddy`) on Windows. Caddy
emits JSON-per-line logs; bestool detects these and applies
opportunistic syntax highlighting per line.

**Usage:** `bestool tamanu logs [OPTIONS] <NAME>`
**Usage:** `bestool tamanu logs [OPTIONS] [NAMES]...`

###### **Arguments:**

* `<NAME>` — Service name. Matched as a substring against the expected service list
* `<NAMES>` — Service names. Each is matched as a substring against the expected service list. `caddy` is a recognised pseudo-service. With no names, tails everything (every expected-Up tamanu service plus caddy)

###### **Options:**

* `-n`, `--lines <LINES>` — Number of trailing lines to print before tailing

Default value: `10`
* `-f`, `--follow` — Follow: keep printing new lines as they arrive. Equivalent to `tail -f`
* `-g`, `--grep <REGEX>` — Only print lines matching this regex. On Linux this is passed to `journalctl -g`; on Windows it's applied client-side after reading from the pm2 log files
* `-g`, `--grep <REGEX>` — Only print lines matching this regex. On Linux this is passed to `journalctl -g`; on Windows it's applied client-side after reading from the log files



Expand Down
97 changes: 0 additions & 97 deletions crates/bestool/src/actions/tamanu/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,43 +428,6 @@ fn is_running(supervisor: Supervisor, target: &str) -> bool {
}
}

/// Filter an expectation set by zero or more substring patterns.
///
/// - Empty `names`: returns every expectation unchanged.
/// - Otherwise: an expectation matches if **any** name in `names` is a
/// substring of the expectation's name.
///
/// Returns an error if any name in `names` matched zero expectations
/// (typo safety in multi-name invocations).
pub fn match_names<'a>(
expectations: &'a [Expectation],
names: &[&str],
) -> Result<Vec<&'a Expectation>> {
if names.is_empty() {
return Ok(expectations.iter().collect());
}

let unmatched: Vec<&str> = names
.iter()
.copied()
.filter(|name| !expectations.iter().any(|e| e.name.contains(name)))
.collect();
if !unmatched.is_empty() {
let available: Vec<&str> = expectations.iter().map(|e| e.name).collect();
bail!(
"no service matches: {}; available names are: {}",
unmatched.join(", "),
available.join(", "),
);
}

let matched: Vec<&Expectation> = expectations
.iter()
.filter(|e| names.iter().any(|name| e.name.contains(name)))
.collect();
Ok(matched)
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -479,66 +442,6 @@ mod tests {
}
}

#[test]
fn empty_names_returns_everything() {
let es = [exp("tamanu-api"), exp("tamanu-tasks"), exp("tamanu-sync")];
let m = match_names(&es, &[]).unwrap();
assert_eq!(m.len(), 3);
}

#[test]
fn single_name_substring_matches() {
let es = [exp("tamanu-central-api"), exp("tamanu-central-tasks")];
let m = match_names(&es, &["api"]).unwrap();
assert_eq!(m.len(), 1);
assert_eq!(m[0].name, "tamanu-central-api");
}

#[test]
fn multi_name_union() {
let es = [
exp("tamanu-central-api"),
exp("tamanu-central-tasks"),
exp("tamanu-central-fhir-resolve"),
];
let m = match_names(&es, &["api", "fhir"]).unwrap();
assert_eq!(m.len(), 2);
assert_eq!(
m.iter().map(|e| e.name).collect::<Vec<_>>(),
vec!["tamanu-central-api", "tamanu-central-fhir-resolve"],
);
}

#[test]
fn zero_match_name_bails() {
let es = [exp("tamanu-api"), exp("tamanu-tasks")];
let err = match_names(&es, &["nope"]).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("nope"), "error should name the bad pattern: {msg}");
assert!(msg.contains("tamanu-api"), "error should list available: {msg}");
}

#[test]
fn mixed_match_and_no_match_still_bails() {
// One typo in a multi-name invocation should bail rather than silently
// drop the bad pattern and process the rest.
let es = [exp("tamanu-api"), exp("tamanu-tasks")];
let err = match_names(&es, &["api", "nope"]).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("nope"), "error should name the bad pattern: {msg}");
}

#[test]
fn name_substring_can_match_multiple() {
let es = [
exp("tamanu-central-fhir-resolve"),
exp("tamanu-central-fhir-refresh"),
exp("tamanu-api"),
];
let m = match_names(&es, &["fhir"]).unwrap();
assert_eq!(m.len(), 2);
}

fn templated_exp(name: &'static str) -> Expectation {
Expectation {
name,
Expand Down
Loading
Loading