logs
@library("logs", "0.0.0");
Turns GreyCat’s own runtime log into queryable time series, and ships a dashboard that reads them. Pure GCL; no native code.
The runtime appends every record it emits to files/root/log.csv. This library
tails that file from a persisted byte offset, folds each record into a set of
series, and exposes them over @exposed endpoints. Schedule one function and
the rest happens on its own:
fn main() {
Scheduler::add(logs::ingest, FixedPeriodicity { every: 1min }, null);
}
What it stores
| Series | Type | Holds |
|---|---|---|
| entries | nodeTime<Entry> |
every record, keyed by the instant it was logged |
| rates | nodeTime<LevelCounts> |
per-minute counts per level |
| usage | nodeTime<DataUsage> |
per-minute store counters from perf records |
| sources | nodeIndex<String, SourceStats> |
record totals per module::function |
| usage by source | nodeIndex<String, UsageStats> |
store counters per module::function |
usage fills only when the runtime runs at --log=perf, which is what makes it
report store counters per transaction commit: read and write bytes, block and
cache hits, and read_wasted, the part of read_bytes pulled off disk and
never used. A busy server commits far more than once a minute, so those are
accumulated per minute rather than stored per commit - that bounds a range query
by its length instead of by how hard the server was working, and puts the series
on the same buckets as rates. At any lower log level it stays empty and
everything else still works.
At --log=perf the log never quite drains: ingest commits a transaction of
its own, so the runtime logs one perf record for it, which the next run picks
up. That is one record per run, not a growing backlog.
Querying
logs::extent() // oldest/newest held
logs::records(null, from, to, 500) // newest first in a slice
logs::records(runtime::LogLevel::error, null, null, 20)
logs::between(from, to, 500) // oldest first, capped
logs::rates_between(from, to)
logs::usage_between(from, to)
logs::source_stats() // records per function
logs::usage_stats() // store counters per function
logs::status() // cursor + what is held
logs::clear() drops the series and keeps the cursor; logs::rewind() moves
the cursor back to the start of the log. Both need admin.
The dashboard
The library ships a webapp under lib/logs/webroot/logs, which the server
serves at /logs for any project that depends on it. It draws every series as a
lane on one shared clock, so a spike in errors can be read against the disk and
cache traffic at the same instant: record volume, errors and warnings stacked,
disk reads split into used and wasted, bytes written, and the share of block
reads served from cache. The three store lanes are replaced by an instruction
when the server is not running at --log=perf. Nothing to mount and no
route to declare - install the library, start the server, open /logs. The
project’s own webroot/ is left alone, so / keeps serving whatever the host
application puts there.
The dashboard reads the session cookie and never asks for credentials: it is a
page inside your application, and your application owns signing in. With no
session it says so and links to /. For a bare local run, mint one against the
server first:
LOGS_PASSWORD=secret greycat serve --log=perf
curl -c cookies.txt -X POST -d '["root","secret"]' \
http://localhost:8080/runtime::Identity::login
Reading the log format
A record is seven columns, level,time,user_id,id,id2,src,data, matching
runtime::Log. Only the payload may contain commas, and nothing escapes it, so
a record can also span several physical lines when the payload contains a
newline. logs::ingest handles both: the payload is read as the greedy last
field and re-joined, and a line that parses as no record is appended to the one
above it.
As of std 8.3.31-dev, CsvReader<runtime::Log> reads the file directly and
this library uses it as-is. Two details still apply to anyone parsing it
themselves: CsvFormat.string_delimiter must be '\0', because the payload is
unescaped and a " in it would otherwise be eaten as a delimiter; and the
payload arrives as one cell per comma, to be rejoined with ,.
Demo
greycat run demo --log=perf
Runs a workload that logs at every level, throws real errors, writes a multi-line record and touches the store, then ingests it and prints what landed in each series.