Skip to content

Commit 68c4778

Browse files
committed
oxlog: Glob match zone names
`oxlog logs` currently requires a precise match for the zone name. This is inconvenient when attempting to search across multiple zones with similar names. For example, when searching all Crucible downstairs, or using Pilot to query Nexus logs on multiple sleds. Use the `glob` crate to take a pattern to match against the zone name for the `services` and `logs` subcommands, potentially allowing multiple zones to be returned. If more than one zone is matched, then the logs and services are sorted separately. Zones with a large number of archived logs may take an extended period to walk and sort, and when matching against multiple zones this becomes uncomfortably slow. To claw back some performance, use Rayon to find files and sort in parallel. We limit the size of its thread pool to a maximum of eight threads to avoid soaking up all available threads on a sled. On smaller machines we fall back to the number of logical threads. Example usage: $ oxlog services 'oxz_crucible_[!p]*' # All non-pantry Crucible zones $ oxlog logs 'oxz_nexus_*' --current # The nexus zone
1 parent 267d3d8 commit 68c4778

File tree

4 files changed

+124
-77
lines changed

4 files changed

+124
-77
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dev-tools/oxlog/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ anyhow.workspace = true
1212
camino.workspace = true
1313
chrono.workspace = true
1414
clap.workspace = true
15+
glob.workspace = true
1516
jiff.workspace = true
17+
rayon.workspace = true
1618
sigpipe.workspace = true
1719
uuid.workspace = true
1820
omicron-workspace-hack.workspace = true

dev-tools/oxlog/src/bin/oxlog.rs

+71-35
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@
55
//! Tool for discovering oxide related logfiles on sleds
66
77
use clap::{ArgAction, Args, Parser, Subcommand};
8+
use glob::Pattern;
89
use jiff::civil::DateTime;
910
use jiff::tz::TimeZone;
1011
use jiff::{Span, Timestamp};
1112
use oxlog::{DateRange, Filter, LogFile, Zones};
1213
use std::collections::BTreeSet;
14+
use std::num::NonZeroUsize;
15+
use std::str::FromStr;
16+
17+
/// The number of threads to given to the Rayon thread pool.
18+
/// Use this limit to avoid consuming excessive CPU time on a sled.
19+
const MAX_THREADS: usize = 8;
1320

1421
#[derive(Debug, Parser)]
1522
#[command(version)]
@@ -25,8 +32,8 @@ enum Commands {
2532

2633
/// List logs for a given service
2734
Logs {
28-
/// The name of the zone
29-
zone: String,
35+
/// The glob pattern to match against zone names
36+
zone_glob: GlobPattern,
3037

3138
/// The name of the service to list logs for
3239
service: Option<String>,
@@ -49,15 +56,26 @@ enum Commands {
4956
after: Option<Timestamp>,
5057
},
5158

52-
/// List the names of all services in a zone, from the perspective of oxlog.
59+
/// List the names of all services in matching zones, from the perspective of oxlog.
5360
/// Use these names with `oxlog logs` to filter output to logs from a
5461
/// specific service.
5562
Services {
56-
/// The name of the zone
57-
zone: String,
63+
/// The glob pattern to match against zone names
64+
zone_glob: GlobPattern,
5865
},
5966
}
6067

68+
#[derive(Clone, Debug)]
69+
struct GlobPattern(Pattern);
70+
71+
impl FromStr for GlobPattern {
72+
type Err = glob::PatternError;
73+
74+
fn from_str(s: &str) -> Result<Self, Self::Err> {
75+
Pattern::new(s).map(GlobPattern)
76+
}
77+
}
78+
6179
#[derive(Args, Debug)]
6280
#[group(required = true, multiple = true)]
6381
struct FilterArgs {
@@ -109,6 +127,12 @@ fn parse_timestamp(
109127
fn main() -> Result<(), anyhow::Error> {
110128
sigpipe::reset();
111129

130+
let num_threads = std::thread::available_parallelism()
131+
.map(NonZeroUsize::get)
132+
.unwrap_or(MAX_THREADS)
133+
.min(MAX_THREADS);
134+
rayon::ThreadPoolBuilder::new().num_threads(num_threads).build_global()?;
135+
112136
let cli = Cli::parse();
113137

114138
match cli.command {
@@ -118,7 +142,14 @@ fn main() -> Result<(), anyhow::Error> {
118142
}
119143
Ok(())
120144
}
121-
Commands::Logs { zone, service, metadata, filter, before, after } => {
145+
Commands::Logs {
146+
zone_glob,
147+
service,
148+
metadata,
149+
filter,
150+
before,
151+
after,
152+
} => {
122153
let zones = Zones::load()?;
123154
let date_range = match (before, after) {
124155
(None, None) => None,
@@ -145,44 +176,46 @@ fn main() -> Result<(), anyhow::Error> {
145176
);
146177
};
147178

148-
let logs = zones.zone_logs(&zone, filter);
149-
for (svc_name, svc_logs) in logs {
150-
if let Some(service) = &service {
151-
if svc_name != service.as_str() {
152-
continue;
179+
let zones = zones.zone_logs(&zone_glob.0, filter);
180+
for logs in zones {
181+
for (svc_name, svc_logs) in logs {
182+
if let Some(service) = &service {
183+
if svc_name != service.as_str() {
184+
continue;
185+
}
153186
}
154-
}
155-
if filter.current {
156-
if let Some(current) = &svc_logs.current {
157-
if metadata {
158-
print_metadata(current);
159-
} else {
160-
println!("{}", current.path);
187+
if filter.current {
188+
if let Some(current) = &svc_logs.current {
189+
if metadata {
190+
print_metadata(current);
191+
} else {
192+
println!("{}", current.path);
193+
}
161194
}
162195
}
163-
}
164-
if filter.archived {
165-
for f in &svc_logs.archived {
166-
if metadata {
167-
print_metadata(f);
168-
} else {
169-
println!("{}", f.path);
196+
if filter.archived {
197+
for f in &svc_logs.archived {
198+
if metadata {
199+
print_metadata(f);
200+
} else {
201+
println!("{}", f.path);
202+
}
170203
}
171204
}
172-
}
173-
if filter.extra {
174-
for f in &svc_logs.extra {
175-
if metadata {
176-
print_metadata(f);
177-
} else {
178-
println!("{}", f.path);
205+
if filter.extra {
206+
for f in &svc_logs.extra {
207+
if metadata {
208+
print_metadata(f);
209+
} else {
210+
println!("{}", f.path);
211+
}
179212
}
180213
}
181214
}
182215
}
183216
Ok(())
184217
}
185-
Commands::Services { zone } => {
218+
Commands::Services { zone_glob } => {
186219
let zones = Zones::load()?;
187220

188221
// We want all logs that exist, anywhere, so we can find their
@@ -197,8 +230,11 @@ fn main() -> Result<(), anyhow::Error> {
197230

198231
// Collect a unique set of services, based on the logs in the
199232
// specified zone
200-
let services: BTreeSet<String> =
201-
zones.zone_logs(&zone, filter).into_keys().collect();
233+
let services: BTreeSet<String> = zones
234+
.zone_logs(&zone_glob.0, filter)
235+
.into_iter()
236+
.flat_map(|l| l.into_keys())
237+
.collect();
202238

203239
for svc in services {
204240
println!("{}", svc);

dev-tools/oxlog/src/lib.rs

+49-42
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
99
use anyhow::Context;
1010
use camino::{Utf8DirEntry, Utf8Path, Utf8PathBuf};
11+
use glob::Pattern;
1112
use jiff::Timestamp;
13+
use rayon::prelude::*;
1214
use std::collections::BTreeMap;
1315
use std::io;
1416
use uuid::Uuid;
@@ -186,8 +188,8 @@ impl SvcLogs {
186188
/// scattered across several different directories -- and we care more
187189
/// about filename than which directory they are in.
188190
pub fn sort_by_file_name(&mut self) {
189-
self.archived.sort_unstable_by(LogFile::file_name_cmp);
190-
self.extra.sort_unstable_by(LogFile::file_name_cmp);
191+
self.archived.par_sort_unstable_by(LogFile::file_name_cmp);
192+
self.extra.par_sort_unstable_by(LogFile::file_name_cmp);
191193
}
192194
}
193195

@@ -302,51 +304,56 @@ impl Zones {
302304
Ok(Zones { zones })
303305
}
304306

305-
/// Return log files organized by service name
307+
/// Return log files for all matching zone names organized by service name
306308
pub fn zone_logs(
307309
&self,
308-
zone: &str,
310+
zone_pattern: &Pattern,
309311
filter: Filter,
310-
) -> BTreeMap<ServiceName, SvcLogs> {
311-
let mut output = BTreeMap::new();
312-
let Some(paths) = self.zones.get(zone) else {
313-
return BTreeMap::new();
314-
};
315-
// Some rotated files exist in `paths.primary` that we track as
316-
// 'archived'. These files have not yet been migrated into the debug
317-
// directory.
318-
if filter.current || filter.archived {
319-
load_svc_logs(
320-
paths.primary.clone(),
321-
&mut output,
322-
filter.show_empty,
323-
filter.date_range,
324-
);
325-
}
312+
) -> Vec<BTreeMap<ServiceName, SvcLogs>> {
313+
let output: Vec<BTreeMap<String, SvcLogs>> = self
314+
.zones
315+
.par_iter()
316+
.filter(|(zone, _)| zone_pattern.matches(zone))
317+
.filter_map(|(_, paths)| {
318+
let mut local = BTreeMap::new();
319+
// Some rotated files exist in `paths.primary` that we track as
320+
// 'archived'. These files have not yet been migrated into the debug
321+
// directory.
322+
if filter.current || filter.archived {
323+
load_svc_logs(
324+
paths.primary.clone(),
325+
&mut local,
326+
filter.show_empty,
327+
filter.date_range,
328+
);
329+
}
326330

327-
if filter.archived {
328-
for dir in paths.debug.clone() {
329-
load_svc_logs(
330-
dir,
331-
&mut output,
332-
filter.show_empty,
333-
filter.date_range,
334-
);
335-
}
336-
}
337-
if filter.extra {
338-
for (svc_name, dir) in paths.extra.clone() {
339-
load_extra_logs(
340-
dir,
341-
svc_name,
342-
&mut output,
343-
filter.show_empty,
344-
filter.date_range,
345-
);
346-
}
347-
}
331+
if filter.archived {
332+
for dir in paths.debug.clone() {
333+
load_svc_logs(
334+
dir,
335+
&mut local,
336+
filter.show_empty,
337+
filter.date_range,
338+
);
339+
}
340+
}
341+
if filter.extra {
342+
for (svc_name, dir) in paths.extra.clone() {
343+
load_extra_logs(
344+
dir,
345+
svc_name,
346+
&mut local,
347+
filter.show_empty,
348+
filter.date_range,
349+
);
350+
}
351+
}
348352

349-
sort_logs(&mut output);
353+
sort_logs(&mut local);
354+
Some(local)
355+
})
356+
.collect();
350357

351358
output
352359
}

0 commit comments

Comments
 (0)