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
3 changes: 2 additions & 1 deletion packaging/timberfs-tally.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ APPLY=timberfs-apache-combined timberfs-volume
# Read a log through a document and print the tally it produces, writing no
# store at all:
#
# timberfs tally --try timberfs-apache-combined < /var/log/apache2/access.log
# timberfs tally --try --extractor timberfs-apache-combined \
# < /var/log/apache2/access.log
#
# That is also how a document of your own is developed: a metric that
# claimed lines and matched none says so, which a store quietly holding no
Expand Down
7 changes: 7 additions & 0 deletions packaging/timberfs.1
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,13 @@ later shadowing earlier \(em so a document can be tried without root, and
shadow a shipped one while it is.
.RS
.PP
⚠ The NAME here is the FILE's,
.IB name .json \fR,
where a provisioning's
.B APPLY
names the DOCUMENT. Keeping the two the same is what everything shipped
does and what a fork of one should.
.PP
⚠ That last one is a READER's. A
.B \-\-provision
never looks in a home directory: it runs as a service, creating stores
Expand Down
11 changes: 4 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,13 +689,10 @@ enum Command {
/// the follower whose command this is
#[arg(long, value_name = "SET", conflicts_with_all = ["extractors", "fold", "try_it", "provision"])]
run: Option<String>,
/// Where the provisioning and the site's extractors live
#[arg(
long,
value_name = "DIR",
default_value = "/etc/timberfs",
requires = "provision"
)]
/// Where the provisioning and the site's extractors live. Also
/// read by --try and --check, which resolve an extractor NAME
/// against it
#[arg(long, value_name = "DIR", default_value = "/etc/timberfs")]
etc: PathBuf,
/// Forests to resolve the selection against; default every
/// configured one
Expand Down
63 changes: 61 additions & 2 deletions src/tally.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1433,10 +1433,38 @@ pub fn resolve_extractors(args: &[PathBuf], etc: &Path) -> anyhow::Result<Vec<Pa
{
Some(p) => out.push(p),
None => {
// ⚠ A directory is listed only if it EXISTS, so on a host
// where none does the list is empty — and a resolution
// failure naming nowhere tells the reader nothing about
// where to put the file.
if dirs.is_empty() {
bail!(
"no extractor {name:?} — it is not a path that exists, and there \
is no extractor directory to search: none of \
{PACKAGED_EXTRACTORS} (the timberfs package), {} or \
~/.config/timberfs/{EXTRACTOR_DIR} exists",
etc.join(EXTRACTOR_DIR).display(),
);
}
// ⚠ What is listed is the FILE STEM, because that is
// what this lookup takes — a provisioning's APPLY names
// the DOCUMENT instead, and the two can differ on a
// site's own file. Naming the document here would print
// a word that does not resolve.
let known = load_extractors(&dirs)
.map(|docs| {
docs.iter()
.map(|(_, d)| d.name.clone())
.map(|(p, d)| {
let stem = p
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_default();
if stem == d.name {
stem
} else {
format!("{stem} (the document {:?})", d.name)
}
})
.collect::<Vec<_>>()
.join(", ")
})
Expand All @@ -1449,7 +1477,7 @@ pub fn resolve_extractors(args: &[PathBuf], etc: &Path) -> anyhow::Result<Vec<Pa
.collect::<Vec<_>>()
.join(", "),
if known.is_empty() {
String::new()
", which hold none".to_string()
} else {
format!(", which hold {known}")
}
Expand Down Expand Up @@ -3345,6 +3373,37 @@ mod tests {
std::fs::remove_dir_all(&site).ok();
}

#[test]
fn a_name_that_resolves_nowhere_says_where_it_looked() {
// A directory is listed only if it EXISTS, so on a host with
// none the list is empty — and the failure then named nowhere at
// all, which tells a reader nothing about where to put the file.
let empty = tempdir();
let err = resolve_extractors(&[PathBuf::from("nope")], &empty)
.unwrap_err()
.to_string();
assert!(err.contains(PACKAGED_EXTRACTORS), "{err}");
assert!(err.contains(EXTRACTOR_DIR), "{err}");

let site = empty.join(EXTRACTOR_DIR);
std::fs::create_dir_all(&site).unwrap();
std::fs::write(
site.join("x.json"),
doc(r#"{"name":"m","measure":[{"count":true}]}"#),
)
.unwrap();
let err = resolve_extractors(&[PathBuf::from("nope")], &empty)
.unwrap_err()
.to_string();
assert!(err.contains(&site.display().to_string()), "{err}");
// The FILE STEM, because that is what this lookup takes. The
// document is named `t`, and printing that would print a word
// that does not resolve.
assert!(err.contains("which hold x "), "{err}");
assert!(err.contains(r#"the document "t""#), "{err}");
std::fs::remove_dir_all(&empty).ok();
}

/// Every extractor this repository SHIPS, run against a fixture and
/// compared to a committed answer.
///
Expand Down
4 changes: 3 additions & 1 deletion tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,9 @@ timberfs query my-app-tally | timbergraph -m requests --using my-app

A name is looked up in `/usr/lib/timberfs/tally.extractors.d`, then
`/etc/…`, then `~/.config/…`, later shadowing earlier — so a document can be
written and tried without root, and can shadow a shipped one while it is.
written and tried without root, and can shadow a shipped one while it is. ⚠ The
name here is the **file's**, `<name>.json`; a provisioning's `APPLY` names the
**document**. Keep the two the same, as everything shipped does.

⚠ That last directory is a **reader's**. `timberfs tally --provision` never
looks in a home: it runs as a service, creating stores and registering
Expand Down
Loading