Audit & Logging Reference
DDS-Security 1.2 §8.6/§9.6 spezifiziert das LoggingPlugin-SPI als Audit-Surface: jeder Security-Event (Handshake, Permission-Denial, Crypto-Mismatch) durchläuft den Plugin und landet in Sinks. ZeroDDS liefert vier produktionsreife Sinks (Stderr · JsonLines · Syslog · Fan-Out) plus die Telemetrie-Schiene über zerodds-observability-otlp (Spans/Histogramme an OTLP-Collector). Konkrete Setup-Rezepte (logrotate, SIEM-Forwarder, Jaeger-compose, Sensitive-Data-Scrub) im Audit-Logging Cookbook.
Überblick
Audit & Logging in ZeroDDS hat zwei getrennte Schichten:
- Security-Audit (DDS-Security §8.6) — diskrete Events mit Severity, Participant-GUID und Kategorie. Pflicht-Surface des Standards. Jeder Auth-Handshake-Bruch, jede Permission-Verweigerung, jede Crypto-Mismatch landet hier.
- Observability-Telemetrie (OTLP) — kontinuierliche Spans, Histogramme und Events für Performance-Tracing und SLO-Monitoring. Optional, aber empfohlen für Produktions-Deployments. Geht an OpenTelemetry-Collector (Jaeger, Tempo, Honeycomb, …).
Die zwei Schichten sind orthogonal — Security-Audit ist die compliance-relevante Stream, OTLP ist die operative Telemetrie. Beide können parallel laufen ohne Interaktion.
Spec-Coverage: DDS-Security 1.2 Coverage →
LoggingPlugin Trait
Das SPI ist minimal — eine einzige Methode:
pub trait LoggingPlugin: Send + Sync {
fn log(
&self,
level: LogLevel,
participant: [u8; 16], // Participant-GUID-Bytes
category: &str, // z.B. "auth.handshake.failed"
message: &str, // Free-form Detail
);
}
▶ Runnable example: rust-audit-loggingplugin
Quelle: crates/security/src/logging.rs
Trait-Eigenschaften — Send + Sync erforderlich (Plugin wird von Tick-Loop und User-Threads gleichzeitig aufgerufen). &self nicht &mut self — interne Mutability via Mutex/RwLock wenn nötig. Best-effort: Fehler im Sink dürfen den Sample-Pfad nicht killen — Implementierungen swallowen I/O-Fehler.
LogLevel-Skala (RFC 5424)
ZeroDDS folgt der RFC-5424-Severity-Skala — 8 Stufen, niedrigerer Wert = höhere Priorität:
| Wert | Name | Typische Auslöser | Production-Default |
|---|---|---|---|
| 0 | Emergency | Auth-Handshake-Bruch, Crypto-Key-Exchange gescheitert | ✓ alarmieren |
| 1 | Alert | Permission-Denial (Endpoint-Match blockiert) | ✓ alarmieren |
| 2 | Critical | CRL-Revocation getroffen, Cert-abgelaufen | ✓ alarmieren |
| 3 | Error | Crypto-MAC-Mismatch, Sample-Drop | ✓ loggen |
| 4 | Warning | Cert-Soft-Expiry <30 Tage, PSK-Fallback aktiv | ✓ loggen (Default-Cutoff) |
| 5 | Notice | Participant-Match Success | opt-in |
| 6 | Informational | Plugin-Init, Cert-Reload-Success | opt-in |
| 7 | Debug | Pro-Sample-Crypto-Trace | nur Dev |
Sinks filtern: StderrLoggingPlugin::with_level(LogLevel::Warning) verwirft alles ≥ Notice (höherer Zahlwert). Default ist Warning — Emergency..Warning passieren, Notice..Debug werden gedroppt.
Event-Kategorien
Die category-Strings sind nicht standardisiert vom OMG; ZeroDDS verwendet eine konsistente Punkt-Notation:
| Category-Prefix | Bedeutung | Typischer Severity-Range |
|---|---|---|
auth.* | Authentication-Plugin Events (Handshake, Token-Validation) | Emergency–Notice |
access.* | AccessControl-Permission-Checks | Alert–Notice |
crypto.* | Cryptographic-Plugin (Sample-Encrypt/MAC, Key-Derivation) | Error–Debug |
pki.* | X.509-PKI Events (CRL-Reload, OCSP-Stapling) | Critical–Informational |
tagging.* | DataTagging-Filter-Treffer | Warning–Debug |
plugin.* | Plugin-Lifecycle (Init, Reload, Shutdown) | Informational–Debug |
Konvention: <subsystem>.<action>.<outcome> — z.B. auth.handshake.failed, access.endpoint.denied, crypto.sample.mac_mismatch. Filter-tauglich für JSON-Aggregator (jq, Loki LogQL).
Audit-Sinks (zerodds-security-logging)
Vier produktionsreife LoggingPlugin-Implementations. Alle Send + Sync, alle mit konfigurierbarem min_level.
| Sink | Format | Ziel | Wann nutzen | Quelle |
|---|---|---|---|---|
StderrLoggingPlugin | Plain-text mit ISO-8601-Timestamp | stderr | Development, Container mit stdout/stderr-Collector (Loki, Vector, Fluentd) | stderr_sink.rs |
JsonLinesLoggingPlugin | NDJSON (eine Zeile = ein Event) | Datei (append) | Compliance-Audit, externe Rotation via logrotate(8) oder systemd | jsonl.rs |
SyslogLoggingPlugin | RFC-5424 syslog (UDP) | Syslog-Daemon (Facility LOCAL0) | SIEM-Forwarding (Splunk, Graylog, rsyslog → SIEM) | syslog.rs |
FanOutLoggingPlugin | Broadcast an N Sinks | mehrere | Dual-Destination (z.B. stderr für Operator + JSONL für Auditor) | fanout.rs |
NDJSON-Format (JsonLinesLoggingPlugin)
Jede Zeile ist ein self-contained JSON-Objekt — direkt jq-/Loki-/Splunk-tauglich:
{"ts":"2026-05-15T10:22:33Z","level":"CRITICAL","participant":"0102030405060708...","category":"auth.handshake.failed","message":"cert chain rejected: untrusted root"}
{"ts":"2026-05-15T10:22:34Z","level":"NOTICE","participant":"aabbccddeeff...","category":"access.endpoint.granted","message":"writer for topic 'Telemetry'"}
Felder: ts (UTC ISO-8601), level (RFC-5424 Name uppercase), participant (Hex-encoded 16-Byte-GUID), category (Dot-Notation), message (free-form, JSON-escaped). Schema ist stabil ab v1.0 — neue Felder werden additiv hinzugefügt.
Keine Log-Rotation built-in. JsonLinesLoggingPlugin appended an die Datei ohne Größen-Limit. Rotation gehört zur Infrastruktur: logrotate.d/zerodds-audit, journald-Forwarding, oder SIGHUP-getriggerter Reopen-Pattern. Cookbook hat das Recipe.
OTLP-Telemetrie (zerodds-observability-otlp)
Separater Layer für strukturierte Telemetrie — Spans, Histogramme, Events. POSTet an OTel-Collector via OTLP/HTTP (Port 4318 Default).
let cfg = OtlpConfig {
host: "otel-collector.internal".into(),
port: 4318,
service_name: "zerodds-publisher".into(),
service_version: env!("CARGO_PKG_VERSION").into(),
timeout: Duration::from_secs(5),
};
let exporter = OtlpExporter::new(cfg);
exporter.add_span(Span { /* ... */ });
exporter.add_histogram(Histogram { /* ... */ });
exporter.add_event(Event { /* ... */ });
exporter.flush()?; // POST an Collector
▶ Runnable example: rust-audit-otlp
API-Surface
| Methode | Pfad | Bedeutung |
|---|---|---|
add_span | Buffer → /v1/traces | Distributed-Trace-Span (Discovery-Phase, Reader-Match, …) |
add_histogram | Buffer → /v1/metrics | Latenz/Throughput-Histogramm (Publish-Latenz, Sample-Size) |
add_event | Buffer → /v1/logs | Strukturierter Log-Event (Component, Severity) |
flush | POST × 3 | Schreibt alle drei Buffer auf den Collector; clearert lokal |
Quelle: crates/observability-otlp/src/lib.rs
Beziehung zu Security-Audit. OTLP-Events können Security-Events spiegeln (Plugin-Init, Permission-Reload), aber das Audit-Mandate des Standards wird über LoggingPlugin abgedeckt. OTLP ist die operative Telemetrie-Schiene; mische sie nicht mit dem compliance-relevanten Audit-Stream — Auditor will sortierte NDJSON-Files, nicht Jaeger-Spans.
Plugin-Wiring
Das LoggingPlugin wird in der DcpsRuntimeBuilder über das Security-Bundle eingehängt:
use zerodds_security_logging::{StderrLoggingPlugin, JsonLinesLoggingPlugin, FanOutLoggingPlugin};
use zerodds_security::logging::LogLevel;
let stderr_sink = StderrLoggingPlugin::with_level(LogLevel::Warning);
let audit_sink = JsonLinesLoggingPlugin::open("/var/log/zerodds/audit.ndjson", LogLevel::Notice)?;
let fanout = FanOutLoggingPlugin::new()
.with(stderr_sink)
.with(audit_sink);
let security_bundle = SecurityBundle::builder()
.logging_plugin(Box::new(fanout))
// ... auth, access, crypto Plugins ...
.build();
let participant = DomainParticipantFactory::create(0)
.with_security(security_bundle)
.build()?;
▶ Runnable example: rust-audit-factory-chain
Die Reihenfolge im Wire-Pfad: bei jedem Security-Event (in zerodds-security-runtime) wird der Plugin-Handle synchron aufgerufen. Sinks sind Send + Sync und dürfen blockieren — aber blockieren sie zu lange, blockiert der Tick. Daher: I/O-heavy Sinks (Syslog-UDP, JsonLines-File) sind OK für 100-1000 Events/s; Syslog-TCP oder OTLP-on-LoggingPlugin wären zu langsam (deswegen ist OTLP separater Layer).
Audit & Logging Reference
DDS-Security 1.2 §8.6/§9.6 specifies the LoggingPlugin SPI as the audit surface: every security event (handshake, permission denial, crypto mismatch) runs through the plugin and lands in sinks. ZeroDDS provides four production-ready sinks (stderr · JsonLines · syslog · fan-out) plus the telemetry track via zerodds-observability-otlp (spans/histograms to an OTLP collector). Concrete setup recipes (logrotate, SIEM forwarder, Jaeger compose, sensitive-data scrub) in the Audit Logging Cookbook.
Overview
Audit & logging in ZeroDDS has two separate layers:
- Security audit (DDS-Security §8.6) — discrete events with severity, participant GUID and category. The standard's mandatory surface. Every auth-handshake break, every permission denial, every crypto mismatch lands here.
- Observability telemetry (OTLP) — continuous spans, histograms and events for performance tracing and SLO monitoring. Optional, but recommended for production deployments. Goes to an OpenTelemetry collector (Jaeger, Tempo, Honeycomb, …).
The two layers are orthogonal — the security audit is the compliance-relevant stream, OTLP is operational telemetry. Both can run in parallel without interaction.
Spec coverage: DDS-Security 1.2 coverage →
LoggingPlugin trait
The SPI is minimal — a single method:
pub trait LoggingPlugin: Send + Sync {
fn log(
&self,
level: LogLevel,
participant: [u8; 16], // participant GUID bytes
category: &str, // e.g. "auth.handshake.failed"
message: &str, // free-form detail
);
}
▶ Runnable example: rust-audit-loggingplugin
Source: crates/security/src/logging.rs
Trait properties — Send + Sync required (the plugin is called by the tick loop and user threads simultaneously). &self not &mut self — internal mutability via Mutex/RwLock when needed. Best-effort: a sink error must not kill the sample path — implementations swallow I/O errors.
LogLevel scale (RFC 5424)
ZeroDDS follows the RFC 5424 severity scale — 8 levels, lower value = higher priority:
| Value | Name | Typical triggers | Production default |
|---|---|---|---|
| 0 | Emergency | Auth-handshake break, crypto key exchange failed | ✓ alert |
| 1 | Alert | Permission denial (endpoint match blocked) | ✓ alert |
| 2 | Critical | CRL revocation hit, cert expired | ✓ alert |
| 3 | Error | Crypto MAC mismatch, sample drop | ✓ log |
| 4 | Warning | Cert soft expiry <30 days, PSK fallback active | ✓ log (default cutoff) |
| 5 | Notice | Participant match success | opt-in |
| 6 | Informational | Plugin init, cert-reload success | opt-in |
| 7 | Debug | Per-sample crypto trace | dev only |
Sinks filter: StderrLoggingPlugin::with_level(LogLevel::Warning) drops everything ≥ Notice (higher numeric value). The default is Warning — Emergency..Warning pass, Notice..Debug are dropped.
Event categories
The category strings are not standardised by the OMG; ZeroDDS uses a consistent dot notation:
| Category prefix | Meaning | Typical severity range |
|---|---|---|
auth.* | Authentication plugin events (handshake, token validation) | Emergency–Notice |
access.* | AccessControl permission checks | Alert–Notice |
crypto.* | Cryptographic plugin (sample encrypt/MAC, key derivation) | Error–Debug |
pki.* | X.509 PKI events (CRL reload, OCSP stapling) | Critical–Informational |
tagging.* | DataTagging filter hits | Warning–Debug |
plugin.* | Plugin lifecycle (init, reload, shutdown) | Informational–Debug |
Convention: <subsystem>.<action>.<outcome> — e.g. auth.handshake.failed, access.endpoint.denied, crypto.sample.mac_mismatch. Filterable for a JSON aggregator (jq, Loki LogQL).
Audit sinks (zerodds-security-logging)
Four production-ready LoggingPlugin implementations. All Send + Sync, all with a configurable min_level.
| Sink | Format | Target | When to use | Source |
|---|---|---|---|---|
StderrLoggingPlugin | plain text with ISO-8601 timestamp | stderr | Development, containers with a stdout/stderr collector (Loki, Vector, Fluentd) | stderr_sink.rs |
JsonLinesLoggingPlugin | NDJSON (one line = one event) | file (append) | Compliance audit, external rotation via logrotate(8) or systemd | jsonl.rs |
SyslogLoggingPlugin | RFC 5424 syslog (UDP) | syslog daemon (facility LOCAL0) | SIEM forwarding (Splunk, Graylog, rsyslog → SIEM) | syslog.rs |
FanOutLoggingPlugin | broadcast to N sinks | multiple | Dual destination (e.g. stderr for the operator + JSONL for the auditor) | fanout.rs |
NDJSON format (JsonLinesLoggingPlugin)
Each line is a self-contained JSON object — directly jq/Loki/Splunk-ready:
{"ts":"2026-05-15T10:22:33Z","level":"CRITICAL","participant":"0102030405060708...","category":"auth.handshake.failed","message":"cert chain rejected: untrusted root"}
{"ts":"2026-05-15T10:22:34Z","level":"NOTICE","participant":"aabbccddeeff...","category":"access.endpoint.granted","message":"writer for topic 'Telemetry'"}
Fields: ts (UTC ISO-8601), level (RFC 5424 name uppercase), participant (hex-encoded 16-byte GUID), category (dot notation), message (free-form, JSON-escaped). The schema is stable from v1.0 — new fields are added additively.
No log rotation built in. JsonLinesLoggingPlugin appends to the file with no size limit. Rotation belongs to the infrastructure: logrotate.d/zerodds-audit, journald forwarding, or a SIGHUP-triggered reopen pattern. The Cookbook has the recipe.
OTLP telemetry (zerodds-observability-otlp)
A separate layer for structured telemetry — spans, histograms, events. POSTs to an OTel collector via OTLP/HTTP (port 4318 default).
let cfg = OtlpConfig {
host: "otel-collector.internal".into(),
port: 4318,
service_name: "zerodds-publisher".into(),
service_version: env!("CARGO_PKG_VERSION").into(),
timeout: Duration::from_secs(5),
};
let exporter = OtlpExporter::new(cfg);
exporter.add_span(Span { /* ... */ });
exporter.add_histogram(Histogram { /* ... */ });
exporter.add_event(Event { /* ... */ });
exporter.flush()?; // POST to the collector
▶ Runnable example: rust-audit-otlp
API surface
| Method | Path | Meaning |
|---|---|---|
add_span | buffer → /v1/traces | distributed-trace span (discovery phase, reader match, …) |
add_histogram | buffer → /v1/metrics | latency/throughput histogram (publish latency, sample size) |
add_event | buffer → /v1/logs | structured log event (component, severity) |
flush | POST × 3 | writes all three buffers to the collector; clears locally |
Source: crates/observability-otlp/src/lib.rs
Relationship to the security audit. OTLP events can mirror security events (plugin init, permission reload), but the standard's audit mandate is covered via LoggingPlugin. OTLP is the operational telemetry track; don't mix it with the compliance-relevant audit stream — the auditor wants sorted NDJSON files, not Jaeger spans.
Plugin wiring
The LoggingPlugin is hooked into the DcpsRuntimeBuilder via the security bundle:
use zerodds_security_logging::{StderrLoggingPlugin, JsonLinesLoggingPlugin, FanOutLoggingPlugin};
use zerodds_security::logging::LogLevel;
let stderr_sink = StderrLoggingPlugin::with_level(LogLevel::Warning);
let audit_sink = JsonLinesLoggingPlugin::open("/var/log/zerodds/audit.ndjson", LogLevel::Notice)?;
let fanout = FanOutLoggingPlugin::new()
.with(stderr_sink)
.with(audit_sink);
let security_bundle = SecurityBundle::builder()
.logging_plugin(Box::new(fanout))
// ... auth, access, crypto plugins ...
.build();
let participant = DomainParticipantFactory::create(0)
.with_security(security_bundle)
.build()?;
▶ Runnable example: rust-audit-factory-chain
The order in the wire path: on every security event (in zerodds-security-runtime) the plugin handle is called synchronously. Sinks are Send + Sync and may block — but if they block too long, the tick blocks. So: I/O-heavy sinks (syslog UDP, JsonLines file) are OK for 100-1000 events/s; syslog TCP or OTLP-on-LoggingPlugin would be too slow (which is why OTLP is a separate layer).