Logb is a format. The specification is the normative document, and an implementation is a claim to conform to it — where the two disagree, the specification wins and the implementation is the bug.
One implementation exists today. If you write another, in any language, it belongs on this page: open an issue or a pull request on the repository.
A conforming reader is roughly a thousand lines plus a decompressor. There is no XML, no external schema registry, and no runtime dependency that must still exist in 2050. The things worth knowing before you start are on the conformance page — in particular that the bit-numbering vectors in §6.2, not the prose around them, are what your extractor has to reproduce.
A writer is smaller still, and needs only a byte sink: nothing points forward, so it never seeks back to patch a field it already emitted. That is what makes the format practical on an embedded logger.
`github.com/rveen/logb` — MIT, covering the specification as well as the code.
go get github.com/rveen/logb
A reader that needs only an `io.Reader`, a writer that needs only an `io.Writer`, bit extraction and the full conversion set, plus `Resync` — design rule 3 as an API: hand it a byte slice starting anywhere and it scans forward to the next sync frame and returns a reader positioned there.
API reference is on pkg.go.dev.
go run ./cmd/logbgen -o /tmp/x.logb # write the example file go run ./cmd/logbdump /tmp/x.logb # frames, schemas, records go run ./cmd/logbdump -resync /tmp/x.logb # resynchronise, from the CLI
in half and read what is left:
head -c 7000 /tmp/x.logb > /tmp/cut.logb go run ./cmd/logbdump /tmp/cut.logb # 302 records, TRUNCATED
No repair step, no recovery mode — the truncated file is simply a shorter valid file.
The schema declares the record layout, the axis, and the stream's identity. The UUID says which logical stream this is across segments and files.
func schema() *logb.Schema {
return &logb.Schema{
UUID: uuid.NewSHA1(uuid.NameSpaceOID, []byte("example/EngineData")),
Name: "EngineData",
RecordBits: 24, // EngineSpeed(16) + CoolantTemp(8)
// One record every 10 ms, derived from the record index: the axis costs // zero bytes per record. AxisExp is the tick size as a power of ten, so // -9 means one tick is a nanosecond. AxisKind: logb.AxisTime, AxisMode: logb.AxisImplicit, AxisExp: -9, AxisUnit: "s", AxisStep: logb.TickVal(10_000_000),
// Raw is what the bus produced; Conv derives the physical value. Fields: []logb.Field{ {Name: "EngineSpeed", BitOffset: 0, BitWidth: 16, Type: logb.TypeUint, Unit: "rpm", Conv: logb.Linear{A: 0, B: 0.25}}, {Name: "CoolantTemp", BitOffset: 16, BitWidth: 8, Type: logb.TypeUint, Unit: "degC", Conv: logb.Linear{A: -40, B: 1}}, }, } }
Writing needs only an `io.Writer`. A segment restates its schemas, which is what lets a reader start there:
lw, _ := logb.NewWriter(w)
lw.AddStream(s)
lw.BeginSegment(1773480413000000000) // wall clock, ns
lw.WriteMeta("vehicle.vin", "WVWZZZ1JZXW000001")
lw.WriteData(s, logb.TickVal(0), 0, n, rec)
lw.Close() // writes the index and end frames — both optional by construction
Reading yields one batch per DATA frame:
r, _ := logb.NewReader(bytes.NewReader(data))
for {
b, err := r.Next()
if err == io.EOF {
break
}
for i := 0; i < int(b.Count); i++ {
axis, _ := b.Axis(i) // b.Raw(i, f) gives the stored bits instead
rpm, _ := b.Value(i, 0) // conversion applied
degC, _ := b.Value(i, 1)
_, _, _ = axis, rpm, degC
}
}
// Truncated means the scan stopped at damage rather than at a clean end. // Every batch returned before it was set is intact and trustworthy. if r.Truncated { fmt.Println("file is truncated; batches above are still intact") }
Recovering a cut file is one call:
r, off, err := logb.Resync(data[7000:])
The full worked example — multiple segments, mixed byte orders, variable-length fields, compression — is in `internal/example`.
None yet. The specification is complete enough to implement from, the conformance vectors will tell you whether you got the hard part right, and there is nobody to ask for permission.