Skip to content

Self-describing logs

The experimental self-describing-logs feature records a payload catalog during application startup. Offline tools use it to decode recorded payloads and inspect their fields, types, and units. This workflow is introduced by the startup catalog PR and standalone tools PR, following metadata-aware rollover.

Application setup

Both project templates expose an application feature:

[features]
self-describing-logs = ["cu29/self-describing-logs"]

Enable it with cargo run --features self-describing-logs. Keep the ordinary builder and log path:

```rust,ignore let app = App::builder() .with_log_path("logs/app.copper", Some(32 * 1024 * 1024))? .build()?;


The existing `cu29_build::setup()` call is sufficient. The runtime macro obtains
the catalog's output types from the RON graph, including types declared in other
crates. Payloads keep their existing `Encode` derives. Cargo enables the codec's
companion descriptions, and typed references follow nested and recursive fields.
Reusable payload authors may use `#[bincode(describe)]` to provide descriptions
independently of feature forwarding.

A manually implemented encoder supplies a matching `ValueDecode` recipe. For
example, a wrapper encoding four float components can reuse the array recipe:

```rust,ignore
impl cu29::bincode::ValueDecode for Orientation {
    const DECODE: &'static cu29::bincode::ValueDecodeSpec =
        <[f32; 4] as cu29::bincode::ValueDecode>::DECODE;
}

Copper units carry their coherent storage units in the catalog, and Copper time records nanoseconds. A length created from centimetres still records metres. Captured custom logging codecs need a native recipe for their chosen representation; startup errors identify the task, output type, and codec.

Typed quantity metadata

Each type attaches metadata through ValueDecode::METADATA, a static slice of Copper-owned ValueMetadata values. The allocation-free cu29-value-types crate supplies the shared vocabulary, re-exported through cu29-value and the Copper prelude. QuantityMetadata::coherent(Quantity::Velocity) describes coherent SI storage; QuantityMetadata::time(TimeStorageUnit::Nanosecond) describes Copper clock storage. Quantity and storage choices are checked by the type system.

Portable metadata uses permanent numeric IDs and length-delimited bodies. Readers retain unknown kinds, quantities, and storage alternatives while exposing the raw decoded payload value. Known metadata supplies conventional symbols such as V, N·m, and m·s⁻¹ for display. JSON and RON exports retain the IDs and include readable quantity and unit information.

Embedded startup

self-describing-logs supports no_std. The generated builder serializes static encoding recipes and compresses them with Heatshrink before initializing the runtime. The serializer and compressor allocate no heap memory on success. Their working memory is bounded: 256 reachable native type references, a 2 KiB compressor buffer, and small output buffers. The logger sizes one catalog section before writing it directly to backend storage. Exceeding the type limit returns a startup error.

Metadata, append and rollover

The application produces and saves its catalog only during construction, before resource initialization. One shared schema graph covers all compiled missions; each mission supplies its ordered CopperList slot map. Application identity, canonical RON configuration and sorted mission names occupy a separate static metadata section. Matching appended runs compare and reuse both sections.

Each catalog is a single bincode value compressed with Heatshrink, including its ordinary version-2 field. Its footer records uncompressed length and CRC32. A section can span backing files. Readers bound decompression and validate the complete catalog. Payload recording keeps its ordinary native encoding pass.

The metadata sections remain outside the rotating data region. Rollover reclaims closed data sections using byte-offset links, while retaining application metadata and the catalog. Every data section identifies its run, instance and mission. Readers can decode surviving CopperLists even when their startup lifecycle markers have rotated out; missing start times remain unknown. Mmap and SD/eMMC share this format and allocator. Append requires a cleanly closed log with matching metadata.

Lifecycle records mark successful construction, start, stop and shutdown, with separate failure and panic records. CopperList, keyframe and lifecycle streams retain their construction context. Structured text keeps the process-global sink: interleaved instances send text to the most recently installed sink.

Reading a recorded log

From a Copper checkout:

just logextract logs/app.copper catalog
just logextract logs/app.copper extract-copperlists --export-format jsonl
just logextract logs/app.copper fsck --deep

Appended logs require selecting a run with --run N; use list-runs to find its zero-based index. The standalone reader selects that run's mission map from the shared static catalog. The Python catalog result includes every compiled mission map.

Host Rust tools enable cu29/decode-catalog; cu29-export/self-describing-logs selects it automatically. Reader features use std and allocate value trees.

With python and self-describing-logs enabled on cu29-export:

import libcu29_export as cu

catalog = cu.value_decode_catalog_unified("logs/app.copper", run=0)
for cl in cu.copperlist_value_iterator_unified("logs/app.copper", run=0):
    print(cl["msgs"][0]["payload"])

See the single-crate wheel example for complete application setup and the format reference for wire details and offline limits.