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
14 changes: 8 additions & 6 deletions .github/workflows/timbersh.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@ jobs:
# works; `--no-install-recommends` is the workstation that only
# ever talks to other machines.
#
# The version belongs there too, and only as a hint: the viewer is
# nothing but seeks and the bounded `from_chunk` one landed in
# 0.26.0. What a dpkg constraint cannot reach is the case that
# matters anyway — a REMOTE target, about which it says nothing —
# so the runtime diagnosis is the real guard.
# The version belongs there too, and only as a hint: `graph ...
# extracting` runs `timberfs tally --try` HERE and resolves an
# extractor name against the directory the timberfs package ships,
# both of which landed in 0.32.0. What a dpkg constraint cannot
# reach is the case that matters anyway — a REMOTE target, about
# which it says nothing — so the runtime diagnosis is the real
# guard.
- name: Build the .deb
run: |
V=$(tr -d '[:space:]' < tools/VERSION)
Expand All @@ -95,7 +97,7 @@ jobs:
Priority: optional
Architecture: all
Depends: python3, zstd
Recommends: timberfs (>= 0.26.0)
Recommends: timberfs (>= 0.32.0)
Suggests: gnuplot
Maintainer: Torstein Tauno Svendsen <torstei@gmail.com>
Homepage: https://github.com/torstei/timberfs
Expand Down
5 changes: 5 additions & 0 deletions packaging/timbergraph.1
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ Draw somewhere other than the terminal.
.B \-\-window
needs a display and gnuplot's qt or x11 terminal.
.TP
.BI \-\-width " N" ", " \-\-height " N"
The plot's size \(em CHARACTERS on the terminal, where it defaults to the
terminal's own, and PIXELS everywhere else (1000x500, or 900x500 for
.BR \-\-window ).
.TP
.BI \-\-tsv " FILE" ", " \-\-gnuplot " FILE"
The table behind the picture, and the script that drew it with its data
inline so it re-runs alone
Expand Down
4 changes: 3 additions & 1 deletion packaging/timbersh.1
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,9 @@ read and a plot of what came back, with no response kind of its own. ⚠
The HOST is a label like any other: a fleet answer that merged two of
them would be two series a reader takes for one, so
.B "by host"
keeps them apart and leaving it out sums them on purpose. .B "graph M from SRC extracting NAME"
keeps them apart and leaving it out sums them on purpose.
.PP
.B "graph M from SRC extracting NAME"
makes the numbers HERE, from a store that has NO TALLY at all \(em for
trying an extractor against real data before provisioning anything. ⚠ It
moves the RAW LOG, tens of megabytes an hour per host against kilobytes
Expand Down
19 changes: 19 additions & 0 deletions tests/timbergraph/test-timbergraph
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ def test_an_extractor_is_a_path_or_a_name():
check("by path", doc, g.resolve_extractor(doc))
check("the unit", "x", g.extractor_facts(["mine"])["m"]["unit"])
raises("neither", lambda: g.resolve_extractor("absent"), "no extractor")
raises("names where it looked", lambda: g.resolve_extractor("absent"),
d)
finally:
if old is None:
del os.environ["XDG_CONFIG_HOME"]
Expand All @@ -231,6 +233,23 @@ def test_an_extractor_is_a_path_or_a_name():
sh.rmtree(home, ignore_errors=True)


def test_a_name_that_resolves_nowhere_says_where_it_looked():
"""A directory is listed only if it exists, so where none does the
list is empty — and the failure then named nowhere at all, which says
nothing about where the file should go. The directories are replaced
rather than the run being skipped where a real one exists: a test
that runs on some machines and not others has told you nothing."""
was = g.EXTRACTOR_DIRS
g.EXTRACTOR_DIRS = ("/nonexistent/packaged", "/nonexistent/site",
"~/nonexistent/mine")
try:
check("none exist", [], g.extractor_dirs())
raises("names the packaged one", lambda: g.resolve_extractor("absent"),
"/nonexistent/packaged")
finally:
g.EXTRACTOR_DIRS = was


def test_the_time_axis_is_local_and_snaps_to_the_clock():
"""gnuplot renders a time axis as though every value were UTC, so
the shift is in the number — and a tic lands on the hour a reader
Expand Down
15 changes: 15 additions & 0 deletions tests/timbersh/test-timbersh
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,21 @@ def a_graph_makes_the_fleet_a_dimension_rather_than_merging_it():
s.cleanup()


@test
def a_graph_refuses_a_host_label_it_would_have_to_write_over():
"""The shell adds `host` itself, so a metric that already carries one
would be two meanings in one label. The refusal has to survive being
RAISED — it names the metric, and naming the wrong variable turns the
diagnosis into a NameError from inside the shell."""
labelled = "2026-09-06T13:37:00.000Z 60s heap host=one last=100\n"
s = Session({h: {"stores": [store("t", ID_A)], "loglines": labelled}
for h in ("a", "b")}, hosts=["a", "b"]).run(
"graph heap from [class=tally];")
assert "already carry" in s.out, s.out
assert "graph heap from" in s.out, s.out
s.cleanup()


@test
def a_graph_refuses_a_cumulative_ladder_rather_than_multiplying_it():
"""`le` series are cumulative, so stacking them multiplies the count
Expand Down
4 changes: 4 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,10 @@ Several metrics go on one graph, comma-separated — which is how two stores
answer at once, since `[class=tally]` covers both and the metric name is what
tells them apart.

`using <name>` names the extractor document the numbers came from, which is
where the **unit** is written down; it is `timbergraph --using` below, with the
same lookup. `svg 'FILE'` draws where `png 'FILE'` does.

### `extracting` — metrics from a store that has no tally

```
Expand Down
15 changes: 12 additions & 3 deletions tools/timbergraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,10 +520,18 @@ def resolve_extractor(name_or_path):
candidate = os.path.join(d, f"{name_or_path}.json")
if os.path.isfile(candidate):
return candidate
where = ", ".join(extractor_dirs()) or "no extractor directory that exists"
# ⚠ A directory is listed only if it EXISTS, so where none does the
# list is empty — and a failure naming nowhere says nothing about
# where the file should go.
found = extractor_dirs()
if not found:
raise Bad(
f"no extractor {name_or_path!r} — it is not a path that exists, and "
f"there is no extractor directory to search: none of "
f"{', '.join(EXTRACTOR_DIRS)} exists")
raise Bad(
f"no extractor {name_or_path!r} — neither a path that exists nor a "
f"document in {where}"
f"document in {', '.join(found)}"
)


Expand Down Expand Up @@ -812,7 +820,8 @@ def main(argv=None):
ap.add_argument("--svg", metavar="FILE")
ap.add_argument("--window", action="store_true",
help="an interactive window; needs a display and gnuplot-qt")
ap.add_argument("--width", type=int)
ap.add_argument("--width", type=int,
help="characters on the terminal, pixels in an image")
ap.add_argument("--height", type=int)
ap.add_argument("--tsv", metavar="FILE",
help="the table behind the picture")
Expand Down
7 changes: 4 additions & 3 deletions tools/timbersh
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ HELP = """
select <kind> from <source> [where ...] [limit N [chunks]] [into view];
tail <kind> from <source> [where ...];
graph <metric>[, <metric>...] from <source> [by LABEL] [where ...]
[quantile Q] [rate] [png 'FILE'];
[quantile Q] [rate] [using <name>] [png 'FILE'|svg 'FILE'];
graph <metric> against <metric> from <source> [...];
graph <metric> from <source> extracting <name> where ... ;

Expand Down Expand Up @@ -1676,7 +1676,8 @@ class Shell:
if at is None or at < 2 or at + 1 >= len(toks):
raise ValueError(
"graph <metric>[, <metric>...] from <source> [by <label>] "
"[where logline since 'T'] [quantile 0.95] [rate] [png 'FILE']")
"[where logline since 'T'] [quantile 0.95] [rate] "
"[using <name>] [png 'FILE'|svg 'FILE']")
conds, by, opts, i = [], [], {}, at + 2
head_words = [t[1] for t in toks[1:at]]
versus = next((n for n, w in enumerate(head_words)
Expand Down Expand Up @@ -1766,7 +1767,7 @@ class Shell:
raise ValueError(
f"these samples already carry a `host` label, so the "
f"fleet cannot be one too — read one host at a time "
f"(`graph {metric} from [host=...]`)")
f"(`graph {metrics[0]} from [host=...]`)")
if host:
sample.labels["host"] = host
samples += got
Expand Down
Loading