Skip to content

Producing an audit log

The analyser reads the event-audit log a Fluxtion EventProcessor emits. This page is the whole path from "my processor logs nothing" to a file the analyser opens. In a hurry, or just evaluating? The analyser ships a recorded run inside the jar — see Guided tour and sample project. Want a live producer instead of a recording — something you start, that writes a log while you watch? Download a prepared one: From playground to analyser in 10 minutes. Its audit configuration is a working example of everything below.

The shortest complete example

Three things have to be true, and missing any one of them produces a log that is empty, or short, or full of the wrong thing. This example is complete and runs:

package com.acme.hello;

public record Temperature(String sensor, double celsius) { }
package com.acme.hello;

import com.telamin.fluxtion.runtime.annotations.OnEventHandler;
import com.telamin.fluxtion.runtime.audit.EventLogNode;

public class Thermostat extends EventLogNode {          // (1) the node can audit
    private boolean heating;

    @OnEventHandler
    public boolean onTemperature(Temperature t) {
        heating = t.celsius() < 18.0;
        auditLog.info("sensor", t.sensor())
                .info("celsius", t.celsius())
                .info("heating", heating);
        return true;
    }
}
package com.acme.hello;

import com.telamin.fluxtion.Fluxtion;
import com.telamin.fluxtion.runtime.DataFlow;
import com.telamin.fluxtion.runtime.audit.EventLogControlEvent;
import java.nio.file.Files;
import java.nio.file.Path;

public class AuditLogHello {
    public static void main(String[] args) throws Exception {
        StringBuilder log = new StringBuilder();

        DataFlow flow = Fluxtion.compile(cfg -> {
            cfg.addNode(new Thermostat(), "thermostat");   // the name becomes the instanceId
            cfg.addEventAudit();                           // (2) install the audit auditor
        });
        flow.init();
        flow.setAuditLogLevel(EventLogControlEvent.LogLevel.INFO);
        // (3) the sink — and it writes the `---` separator itself
        flow.setAuditLogProcessor(r -> log.append("---").append(System.lineSeparator())
                .append(r.toString()).append(System.lineSeparator()));

        flow.onEvent(new Temperature("hall", 21.5));
        flow.onEvent(new Temperature("hall", 16.0));

        Files.writeString(Path.of("thermostat-audit.yaml"), log.toString());
    }
}

Running it writes thermostat-audit.yaml — two events in, two records out, each carrying the three keys the node logged under its instanceId:

---
eventLogRecord:
    eventTime: 1787696934848
    logTime: 1787696934848
    groupingId: null
    event: Temperature
    eventToString: Temperature[sensor=hall, celsius=21.5]
    thread: main
    nodeLogs:
        - thermostat: { sensor: hall, celsius: 21.5, heating: false}
    endTime: 1787696934849
---
eventLogRecord:
    eventTime: 1787696934849
    logTime: 1787696934849
    groupingId: null
    event: Temperature
    eventToString: Temperature[sensor=hall, celsius=16.0]
    thread: main
    nodeLogs:
        - thermostat: { sensor: hall, celsius: 16.0, heating: true}
    endTime: 1787696934849

That is the whole contract: thermostat is the name the graph gave the node, and sensor, celsius and heating are the keys it logged — so in the analyser you filter on thermostat and plot thermostat.celsius over time. Open the file with Audit log ▸ Open log…, or drag it onto the window.

The analyser will tell you before you even run it

Open your processor's .graphml and the analyser reads whether audit logging was installed at build time — the compiler puts an EventLogManager node on the graph when addEventAudit() was called. If it is missing, the Project panel's Graph section says so: this processor will write no audit log, however carefully its nodes narrate themselves. That check needs no log, no run and no export, which matters because this is the one mistake that leaves nothing behind to diagnose.

And if a log is wrong, it tells you that too

Opening a log runs checks on the file and reports what it finds in the status bar and in context: records run together with no --- between them, no nodeLogs anywhere, a log containing nothing but the framework's own control event, or a value whose line break broke its record — quote any value that can hold one, such as text an operator typed. Each names the cause and the fix. They are reported, never repaired — a mis-written log is a finding about the emitter.

Write the --- separator, or the analyser silently reads fewer records

A text audit log is a sequence of YAML documents separated by lines consisting of --- (Format specification §1). record.toString() does not include the separator — the sink adds it. Omit it and the file still opens, still looks like a log, and is read as one record however many it contains — which is why the analyser now checks for it. The example above writes it in the sink for exactly this reason.

Set the level before you attach the sink

setAuditLogLevel(...) dispatches an EventLogControlEvent through the graph, so it produces a record of its own. Set the level first, as above, and the sink never sees it. If your code attaches the sink first, drop the control record explicitly: if (record.toString().contains("event: EventLogControlEvent")) return;

The three numbered pieces are each load-bearing: without (1) the node has no auditLog; without (2) the EventLogManager auditor is never installed and nodeLogs is empty for every record; without (3) the records are produced and thrown away.

How a node appears in the log

A node that should show up in the audit log gets an audit logger (typically by extending EventLogNode) and calls auditLog.info(key, value) / auditLog.warn(key, value) during its callbacks. Each key/value becomes an entry under that node's instanceId in the cycle's nodeLogs:

public class VenueMonitorQuoteCalculator extends EventLogNode implements QuoteCalculator {

    @Override
    public boolean calculateQuote() {
        boolean connected = venueMonitor.isConnected();
        auditLog.info("connected", connected);            // → connected: true
        if (!connected) {
            auditLog.warn("quoteCalculationSkipped", venueMonitor.getVenueStatus());
        }
        // …
        return true;
    }
}

The Fluxtion EventLogManager auditor gathers every node's entries for the cycle into one eventLogRecord. So the log is a by-product of your nodes narrating what they did — no separate instrumentation pass.

Name your fields well

An instanceId is the node's field name in the generated processor. Those names are exactly what you see, filter and graph in the analyser, so give nodes meaningful field names and log stable key names — the analyser plots instanceId.key over time.

Turning it on

Audit logging is enabled on the processor / server and has a log level that controls verbosity (higher = more nodeLogs). In a Mongoose server you configure the audit log and point it at a file sink; that file is the one you open here.

  • Log level — raise it (e.g. DEBUG) while diagnosing to capture more per-node detail; lower it (INFO) in steady state.
  • File sink — the server writes records to a ----separated text file. Open it locally, or copy it to S3 and open s3://bucket/key.

Which audit sinks the analyser reads

EventLogManager takes a LogRecordListener, so a processor can write its audit stream to any back-end sink: a text file, the binary FLXA file, Chronicle, Kafka, JDBC and so on. The analyser reads the text file and the binary file. A Fluxtion binary audit log (FLXA) opens like a text one, recognised by its first four bytes rather than its file name. For any other sink, export to one of those, or run a file sink alongside it: a file sink and a high-performance sink can run side by side on the same processor.

A binary log differs from a text one in five ways:

  • It does not carry groupingId or thread, so the analyser shows neither, rather than inventing them.
  • It cannot be followed while it is still being written: open it again to see new records.
  • Only millisecond binary logs open. A header that states no time unit, or nanoseconds, is refused. For a file whose header states no unit, write a copy that declares it with the runtime's audit tool, AuditLogTool <file> --declare-unit millis --out <copy>, and open the copy.
  • Its byte offsets are not positions in the file, so an agent's read by byteOffset finds nothing; read by record index or time instead.
  • A cut-off tail is reported, not refused: the records before it open, and the analyser says how many bytes at the end formed no whole record.

The analyser's format specification is the text format's; the binary format's own specification is published with the Fluxtion runtime.

In an embedded processor the three pieces above are the whole configuration. In a Mongoose server the level and the sink are server configuration rather than code — see the Mongoose server documentation — but the same three things must be true, and the same --- rule applies to whatever writes the file.

What you get

Each cycle becomes one record — the triggering event, timing, and the ordered nodeLogs. See Log format for the record shape and a field reference. That deterministic, replayable record is what the whole analyser toolchain works from.