Skip to main content

Load-test scenario report schema

The report is the machine-readable output of a finalized scenario — the authoritative record of what nl6 sent, which an operator diffs against a monitor's received counts to localize missed or duplicated telemetry. It is built once at stop/abort, immutable thereafter, and served by GET /api/v1/scenarios/{id}/report (also returned by stop). See the API reference and the scenarios guide.

Shape

{
"summary": {
"id": "s-000001",
"phase": "stopped",
"protocol": "syslog",
"metadata": {
"config_sha256": "9b8d8c9c…969314",
"seed": 42,
"nl6_version": "v0.16.0",
"t0": "2026-07-18T09:00:05.000Z",
"t1": "2026-07-18T09:00:07.000Z",
"drain_end": "2026-07-18T09:00:07.500Z",
"sub_window_count": 10,
"sub_window_duration": "200ms",
"run_tags": {
"protocol": "syslog", "mechanism": "window_source_ip", "value": "",
"pen": 0, "pen_required": true, "degraded": true,
"note": "no PEN configured (-scenario-pen); SD-PARAM lever unavailable, isolate by source IP + [t0,t1)"
}
},
"duration": "2s",
"participants_armed": 2,
"participants_excluded": 1,
"emitted": 40, "in_window": 40, "drain": 0,
"suppressed_pre_window": 0, "send_failures": 0, "dropped": 0,
"informational": {"background_suppressed": 0},
"sub_windows": [4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
"excluded": [
{"device": "10.42.0.9", "reason": "device not found",
"remediation_hint": "create the device before arming, or remove it from the scenario"}
]
},
"counters": [
{"protocol": "syslog", "source_ip": "10.42.0.1", "collector": "10.0.0.9:514",
"emitted": 20, "sent": 20, "in_window": 20, "drain": 0,
"suppressed_pre_window": 0, "send_failures": 0, "dropped": 0,
"informational": {"background_suppressed": 0, "requested": 20, "deferred": 0},
"sub_windows": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2]}
]
}

The top-level summary block always serializes before counters so a streaming consumer sees the aggregate first.

summary

FieldTypeMeaning
idstringScenario ID (s-000001).
phasestringTerminal phase: stopped (window elapsed / explicit stop) or aborted (graceful shutdown).
protocolstringParticipating protocol.
metadataobjectThe reproducibility fingerprint + actual window timestamps (see below).
durationstringt1 − t0 as a Go duration, from the monotonic clock — the window that actually ran.
participants_armednumberDevices that armed and ran.
participants_excludednumberDeclared participants that did not arm (see excluded).
emitteddroppednumberFleet-wide sums of the per-device identity buckets below.
informationalobjectFleet-wide sum of the disclosure counters (see counters[].informational). Outside the identity.
sub_windowsarrayFleet-wide loss localization: in-window sends per time bucket — element-wise sum of the counters[].sub_windows rows. Length metadata.sub_window_count; sums to in_window. See Loss localization.
excludedarrayArm-time exclusions, each {device, reason, remediation_hint}.

summary.metadata

The reproducibility fingerprint plus the timestamps the run actually observed. Copy the (config_sha256, seed) back into a resubmit on the same nl6_version to re-run a scenario exactly.

FieldTypeMeaning
config_sha256stringSHA-256 over the canonicalized submit config.
seednumberThe seed that pinned every random draw.
nl6_versionstringSimulator version that produced the report — reproduction is guaranteed only on the same version.
t0RFC3339-msActual window open (emission start).
t1RFC3339-msActual window close — the planned T1, or the abort instant for an early abort (never later than planned T1).
drain_endRFC3339-msWhen the drain barrier finished and the report was finalized.
sub_window_countnumberLoss-localization granularity: the number of equal time buckets [T0,T1) is sliced into (currently 10).
sub_window_durationstringWidth of one bucket as a Go duration — the planned window / sub_window_count (the basis fires were bucketed against). Bucket i covers [T0 + i·d, T0 + (i+1)·d). For an aborted run the buckets after the abort instant are simply empty (bucketing uses the planned t1, not the shortened actual one).
run_tagsobjectRun tagging: how this run's traffic is isolated from background noise per its protocol's lever — {protocol, mechanism, value, pen, pen_required, degraded, note}. See Run tagging. mechanism is one of syslog_sd_param, snmp_enterprise_varbind, netflow9_source_id, ipfix_odid, sflow_sub_agent_id, gnmi_synthetic_path, window_source_ip. degraded=true means a PEN-dependent lever fell back to window_source_ip because no -scenario-pen was set.

counters[] — per participant

One row per participant, keyed by the join tuple (protocol, source_ip, collector) — the same tuple a collector groups its received counts by, so the two sides line up row-for-row. Every ledger field is always present (explicit zeros, never omitted), so a zero-valued row still diffs cleanly.

FieldMeaning
protocolParticipating protocol.
source_ipDevice management IP (the emitter).
collectorConfigured collector host:port for this device (empty if the device was deleted post-window).
emittedRecords generated: gate-passed fires + emission-suppressed pre-window fires.
sentin_window + drain — the loss denominator for reconciliation (convenience; derived).
in_windowRecords sent (write returned success) with the write-return timestamp in [T0, T1).
drainRecords sent during the post-T1 drain grace.
suppressed_pre_windowState-driven / on-demand fires that occurred before T0 — counted but not emitted on the wire.
send_failuresResolve / encode / write errors (nl6 could not send).
droppedRecords generated but never confirmed on the wire (straggler past the drain barrier, or a shutdown-race socket drop).
informational.background_suppressedInformational, quarantined in its own sub-object — background-cadence fires the gate suppressed for this participant during the scenario. Deliberately not a flat sibling of the identity buckets and not part of the ledger identity.
informational.informs_acked / informs_pendingSNMP INFORM ack settlement (best-effort, collector-side). An origination counts sent at first-transmit; informs_pending = originations − acked at report time (still-awaiting-ack). Zero for fire-and-forget traps and non-trap protocols. Outside the identity.
informational.requestedScheduler demand — every fire the scenario scheduler popped (pre-limiter). requested = sent + deferred + send_failures + dropped.
informational.deferredFires the shared global cap had no token for — throttled, not fired, NOT lost. Outside the identity and the loss denominator, so a cap throttle never masquerades as pipeline loss.
sub_windowsLoss localization: this participant's in-window sends per time bucket. Length metadata.sub_window_count; sums to in_window. See Loss localization.

The six identity fields (emitted + the five loss buckets) are flat siblings; the disclosure counter is the sole member of the nested informational object. A consumer computing the identity iterates the flat fields and never has to know which keys to exclude.

The ledger identity

Every counter row (and the summary) satisfies, exactly:

emitted = in_window + drain + send_failures + dropped + suppressed_pre_window
sent = in_window + drain

informational.background_suppressed sits outside this identity by design — it counts generation-suppressed background fires that were never generated as scenario records, which is exactly why it lives in its own sub-object rather than as a flat sibling. Use sent (in_window + drain) as the number to reconcile against a collector's received count.

Field / semver evolution policy

The report is a versioned contract. Consumers should tolerate unknown fields.

  • Patch / minor (backward-compatible): new fields may be added to summary or counters; a new top-level block may be added after counters. Existing field names, types, units, and the summary-before- counters order never change within a major.
  • Major (breaking): renaming/removing a field, changing a type or unit, or reordering the top-level blocks. Breaking changes are gated on a major nl6_version bump and called out in the changelog.
  • The ledger identity above is a stability guarantee — it holds in every version that ships these fields.
  • config_sha256 covers only the submit config, not the report shape; the report contract is tracked by nl6_version.

Future projections (additional protocols in counters, richer loss-localization blocks) are additive under this policy.

Loss localization

A fleet total answers how much was lost; sub_windows answers where — by device-set (the per-participant counters[] rows, keyed on the join tuple) and by time (the array within each row). The planned window [T0,T1) is sliced into metadata.sub_window_count equal buckets, each metadata.sub_window_duration wide; sub_windows[i] counts the in-window sends whose write-return time fell in bucket i ([T0 + i·d, T0 + (i+1)·d)). An early abort shortens the run but not the bucket basis — buckets past the abort instant are simply empty.

  • Bucketing choice. A fixed count (10), not a fixed duration — the report stays bounded and window-length-independent (a 10 s window → 1 s buckets; a 10 min window → 60 s buckets).
  • Scope. Localizes in-window sends only: sum(sub_windows) == in_window for every row and the summary. Drain-tail sends are post-T1 by definition and carry no sub-window (reconcile them via the drain total).
  • How to use it. Bucket your collector's received records the same way (receive-time relative to T0, same bucket width) and diff per bucket: "1,204 records lost, all from [T0+30s, T0+45s]" points at a time window, not just a fleet total. Combined with the per-row source_ip, loss narrows to a device-set and a time span.
  • JSON only. The flat CSV projection is unchanged, so index-keyed CSV parsers keep working; localization lives in the JSON report.

Additive under the semver policy — the policy explicitly anticipates "richer loss-localization blocks."

CSV projection

GET /api/v1/scenarios/{id}/report?format=csv serves a flat text/csv projection of counters[] — one header row plus one row per participant, join-ready on the first three columns:

protocol,source_ip,collector,emitted,in_window,drain,suppressed_pre_window,send_failures,dropped,background_suppressed
syslog,10.42.0.1,10.0.0.9:514,20,20,0,0,0,0,0
syslog,10.42.0.2,10.0.0.9:514,20,20,0,0,0,0,0

The informational.background_suppressed counter flattens to a trailing background_suppressed column. summary-level fields are not in the CSV — it is purely the per-device counter projection, so it joins directly against a collector's received-counts export on (protocol, source_ip, collector).