Custom Reporting
Use custom reporting when you need a sink that is not built in and still want the normal LoadStrike sink lifecycle.
Matching docs
Search across docs titles, summaries, groups, and section headings.
Use Up and Down Arrow to move through results, then press Enter to open the active page.
No indexed docs matched that search. Try a broader term or open the docs hub.
What this page helps you do
What this page helps you do
Use custom reporting when you need a sink that is not built in and still want the normal LoadStrike sink lifecycle.
Who this is for
Teams exporting final run data and realtime metrics into supported observability backends.
Prerequisites
- A run result or sink destination you want to wire into the wider observability stack
By the end
A sink-specific setup path that stays tied to the same LoadStrike report model.
Use this page when
Use this page when the local report is not the only destination and you need to wire realtime or final export into a supported sink.
Visual guide
Sample Report Data Rows
Scope Scenario Result Count RPS LatencyP50Ms LatencyP80Ms LatencyP85Ms LatencyP90Ms LatencyP95Ms LatencyP99Ms
Scenario reports-demo OK 675 15.0 21.4 30.2 33.0 36.8 48.6 72.1
LatencyTable Scenario Result Count LatencyP50Ms LatencyP95Ms
LatencyTable reports-demo OK 675 21.4 48.6
LatencyTable reports-demo FAIL 12 35.9 79.2
StatusCode Result Percent
200 OK 97.48
500 FAIL 2.52
FailedStatus Scope Scenario Step StatusCode Count Percent
FailedStatus Scenario reports-demo 500 12 1.75
Reporting
Realtime reporting
Choose Portal Reporting when users should review runs in the customer portal, pick the built-in sink page that matches the backend your team already runs, or open Custom Reporting when you need to implement your own destination. Each tab opens a dedicated page with the settings, behavior, and lifecycle details for that reporting path.
Guide
When To Use Custom Reporting
Choose custom reporting when your team needs to push realtime updates and the final run result into an internal platform, a proprietary API, or a destination that is not covered by the built-in sinks.
Shared Sink Lifecycle
Implement the LoadStrikeReportingSink contract for your SDK and register it with WithReportingSinks(...). LoadStrike calls the base sink lifecycle in one fixed order: Init, Start, realtime callbacks, SaveRunResult, Stop, then Dispose.
Individual Attempt Capability
Implement the SDK's optional iteration-batch sink capability when the custom destination must retain each attempt rather than only periodic summaries. It receives bounded batches during the run and a completion record with captured, delivered, and dropped counts before the base sink shuts down. Percentiles should be calculated in the destination from those observations.
What The Final Callback Receives
SaveRunResult receives the same full LoadStrikeRunResult artifact that Run() returns. That includes final counts, timing, report files, sink errors, policy errors, detailed report rows, and correlation output when those features are enabled.
Failure And Cleanup Behavior
Every reporting-sink callback receives three retries by default after the first attempt, with delays of 250 ms, 500 ms, and 1 second. Recovered callbacks add no final sink error or warning. Only exhausted raw-observation delivery counts as dropped observations; other exhausted callbacks are recorded against that sink. Custom sinks use batch or observation identities to deduplicate replay because a destination that accepts a write before returning an error can receive the same logical batch again. Exception details remain in sanitized local logs rather than portable reports. Persistent lifecycle failures disable only that sink for the run. Stop and dispose remain best-effort cleanup, and an exhausted stop callback does not prevent dispose.
Plan Gate
Built-in vendor sinks and custom reporting are available on Enterprise. Custom sink registration still requires the extensions.reporting_sinks.custom entitlement.
Custom reporting setup
Implement the reporting sink contract for your SDK and its optional iteration-batch capability, register it with WithReportingSinks(...), and let LoadStrike send raw attempts, realtime snapshots, delivery completion, and the final LoadStrikeRunResult automatically.
If you run these examples locally, add a valid runner key before execution starts. Set it with WithRunnerKey("...") or the config key LoadStrike:RunnerKey.
HTML reports include a top-right Light/Dark theme toggle and responsive offline SVG charts. Use exact-value tooltips, legends, zoom, pan, reset, the accessible expanded view, chart-title search, and grid-density controls to inspect dense results. Light is the default report theme.
Custom Reporting
using LoadStrike;
using Microsoft.Extensions.Configuration;
public sealed class InternalOpsSink : LoadStrikeReportingSink, ILoadStrikeIterationBatchSink
{
public string SinkName => "internal-ops";
public Task Init(LoadStrikeBaseContext context, IConfiguration infraConfig) => Task.CompletedTask;
public Task Start(LoadStrikeSessionStartInfo sessionInfo) => Task.CompletedTask;
public Task SaveRealtimeStats(LoadStrikeScenarioStats[] stats)
{
foreach (var scenario in stats)
{
Console.WriteLine($"{scenario.ScenarioName}: {scenario.AllRequestCount64}");
}
return Task.CompletedTask;
}
public Task SaveRealtimeMetrics(LoadStrikeMetricStats metrics) => Task.CompletedTask;
public Task SaveIterationBatch(LoadStrikeIterationObservationBatch batch)
{
Console.WriteLine($"Raw attempts: {batch.Observations.Count}");
return Task.CompletedTask;
}
public Task CompleteIterationObservationStream(LoadStrikeIterationObservationStreamCompletion completion)
{
Console.WriteLine($"Delivered: {completion.DeliveredCount64}; dropped: {completion.DroppedBufferCount64}");
return Task.CompletedTask;
}
public Task SaveRunResult(LoadStrikeRunResult result)
{
Console.WriteLine($"Final requests: {result.AllRequestCount64}");
return Task.CompletedTask;
}
public Task Stop() => Task.CompletedTask;
public void Dispose()
{
}
}
var scenario = LoadStrikeScenario.CreateAsync("submit-orders", async _ =>
{
await Task.Delay(25);
LoadStrikeReply reply = LoadStrikeResponse.Ok(statusCode: "200");
return reply;
})
.WithLoadSimulations(LoadStrikeSimulation.Inject(rate: 10, interval: TimeSpan.FromSeconds(1), during: TimeSpan.FromSeconds(20)));
LoadStrikeRunner.RegisterScenarios(scenario)
.WithReportingInterval(TimeSpan.FromSeconds(5))
.WithReportingSinks(new InternalOpsSink())
.WithRunnerKey("rkl_your_local_runner_key")
.Run();
package main
import (
"fmt"
loadstrike "loadstrike.com/sdk/go"
)
type internalOpsSink struct {
loadstrike.LoadStrikeReportingSinkBase
}
func (internalOpsSink) SinkName() string { return "internal-ops" }
func (internalOpsSink) SaveIterationBatch(batch loadstrike.LoadStrikeIterationObservationBatch) loadstrike.LoadStrikeTask {
fmt.Printf("Raw attempts: %d\n", len(batch.Observations))
return loadstrike.CompletedTask()
}
func (internalOpsSink) CompleteIterationStream(completion loadstrike.LoadStrikeIterationStreamCompletion) loadstrike.LoadStrikeTask {
fmt.Printf("Delivered: %s; dropped: %s\n", completion.DeliveredCount64, completion.DroppedBufferCount64)
return loadstrike.CompletedTask()
}
func main() {
scenario := loadstrike.CreateScenario("custom-reporting", func(loadstrike.LoadStrikeScenarioContext) loadstrike.LoadStrikeReply {
return loadstrike.LoadStrikeResponse.Ok("200")
}).WithLoadSimulations(
loadstrike.LoadStrikeSimulation.Inject(10, loadstrike.DurationFromSeconds(1), loadstrike.DurationFromSeconds(20)),
)
loadstrike.RegisterScenarios(scenario).
WithReportingInterval(loadstrike.DurationFromSeconds(5)).
WithReportingSinks(internalOpsSink{}).
WithRunnerKey("rkl_your_enterprise_runner_key").
Run()
}
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeRunner;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeBaseContext;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeMetricStats;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeReportingSink;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeResponse;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeRunResult;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeScenario;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeScenarioStats;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeSessionStartInfo;
import com.loadstrike.runtime.LoadStrikeRuntime.LoadStrikeSimulation;
import com.loadstrike.runtime.LoadStrikeIterationBatchSink;
import com.loadstrike.runtime.LoadStrikeIterationObservations.LoadStrikeIterationObservationBatch;
import com.loadstrike.runtime.LoadStrikeIterationObservations.LoadStrikeIterationObservationStreamCompletion;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
final class InternalOpsSink implements LoadStrikeReportingSink, LoadStrikeIterationBatchSink {
@Override
public String sinkName() {
return "internal-ops";
}
@Override
public void init(LoadStrikeBaseContext context, Map<String, Object> infraConfig) {
}
@Override
public void start(LoadStrikeSessionStartInfo sessionInfo) {
}
@Override
public void saveRealtimeStats(LoadStrikeScenarioStats[] scenarioStats) {
for (var scenario : scenarioStats) {
System.out.println(scenario.scenarioName + ": " + scenario.allRequestCount);
}
}
@Override
public void saveRealtimeMetrics(LoadStrikeMetricStats metrics) {
}
@Override
public void saveIterationBatch(LoadStrikeIterationObservationBatch batch) {
System.out.println("Raw attempts: " + batch.observations().size());
}
@Override
public void completeIterationObservationStream(LoadStrikeIterationObservationStreamCompletion completion) {
System.out.println("Delivered: " + completion.deliveredCount64()
+ "; dropped: " + completion.droppedBufferCount64());
}
@Override
public void saveRunResult(LoadStrikeRunResult result) {
System.out.println("Final requests: " + result.allRequestCount);
}
@Override
public void stop() {
}
@Override
public void dispose() {
}
}
var scenario = LoadStrikeScenario
.createAsync("submit-orders", ignoredContext -> CompletableFuture.supplyAsync(() -> LoadStrikeResponse.ok("200")))
.withLoadSimulations(LoadStrikeSimulation.inject(10, 1d, 20d));
LoadStrikeRunner.registerScenarios(scenario)
.withReportingInterval(5)
.withReportingSinks(new InternalOpsSink())
.withRunnerKey("rkl_your_local_runner_key")
.run();
import asyncio
from loadstrike_sdk import LoadStrikeResponse, LoadStrikeRunner, LoadStrikeScenario, LoadStrikeSimulation
class InternalOpsSink:
@property
def sink_name(self) -> str:
return "internal-ops"
def init(self, context, infra_config) -> None:
pass
def start(self, session) -> None:
pass
def save_realtime_stats(self, scenario_stats) -> None:
for scenario in scenario_stats:
print(f"{scenario['scenarioName']}: {scenario['allRequestCount']}")
def save_realtime_metrics(self, metrics) -> None:
pass
def save_iteration_batch(self, batch) -> None:
wire_batch = batch.to_dict() if hasattr(batch, "to_dict") else dict(batch)
print(f"Raw attempts: {len(wire_batch['observations'])}")
def complete_iteration_observation_stream(self, completion) -> None:
wire_completion = completion.to_dict() if hasattr(completion, "to_dict") else dict(completion)
print(
f"Delivered: {wire_completion['deliveredCount64']}; "
f"dropped: {wire_completion['droppedBufferCount64']}"
)
def save_run_result(self, result) -> None:
print(f"Final requests: {result.all_request_count}")
def stop(self) -> None:
pass
def dispose(self) -> None:
pass
async def run_step(_):
await asyncio.sleep(0.025)
return LoadStrikeResponse.ok("200")
scenario = (
LoadStrikeScenario.create_async("submit-orders", run_step)
.with_load_simulations(LoadStrikeSimulation.inject(10, 1, 20))
)
LoadStrikeRunner.register_scenarios(scenario) \
.with_reporting_interval(5) \
.with_reporting_sinks(InternalOpsSink()) \
.with_runner_key("rkl_your_local_runner_key") \
.run()
import {
LoadStrikeResponse,
LoadStrikeRunner,
LoadStrikeScenario,
LoadStrikeSimulation
} from "@loadstrike/loadstrike-sdk";
import type { ILoadStrikeReportingSink } from "@loadstrike/loadstrike-sdk";
const sink: ILoadStrikeReportingSink = {
sinkName: "internal-ops",
saveRealtimeStats: async (scenarioStats) => {
for (const scenario of scenarioStats) {
console.log(`${scenario.scenarioName}: ${scenario.allRequestCount}`);
}
},
saveRealtimeMetrics: async () => {},
saveIterationBatch: async (batch) => {
console.log(`Raw attempts: ${batch.observations.length}`);
},
completeIterationObservationStream: async (completion) => {
console.log(`Delivered: ${completion.deliveredCount64}; dropped: ${completion.droppedBufferCount64}`);
},
saveRunResult: async (result) => {
console.log(`Final requests: ${result.allRequestCount}`);
},
stop: async () => {},
dispose: async () => {}
};
const scenario = LoadStrikeScenario
.createAsync("submit-orders", async () => {
await new Promise((resolve) => setTimeout(resolve, 25));
return LoadStrikeResponse.ok("200");
})
.withLoadSimulations(LoadStrikeSimulation.inject(10, 1, 20));
await LoadStrikeRunner
.registerScenarios(scenario)
.withReportingInterval(5)
.withReportingSinks(sink)
.withRunnerKey("rkl_your_local_runner_key")
.run();
const {
LoadStrikeResponse,
LoadStrikeRunner,
LoadStrikeScenario,
LoadStrikeSimulation
} = require("@loadstrike/loadstrike-sdk");
const sink = {
sinkName: "internal-ops",
async saveRealtimeStats(scenarioStats) {
for (const scenario of scenarioStats) {
console.log(`${scenario.scenarioName}: ${scenario.allRequestCount}`);
}
},
async saveRealtimeMetrics() {},
async saveIterationBatch(batch) {
console.log(`Raw attempts: ${batch.observations.length}`);
},
async completeIterationObservationStream(completion) {
console.log(`Delivered: ${completion.deliveredCount64}; dropped: ${completion.droppedBufferCount64}`);
},
async saveRunResult(result) {
console.log(`Final requests: ${result.allRequestCount}`);
},
async stop() {},
async dispose() {}
};
(async () => {
const scenario = LoadStrikeScenario
.createAsync("submit-orders", async () => {
await new Promise((resolve) => setTimeout(resolve, 25));
return LoadStrikeResponse.ok("200");
})
.withLoadSimulations(LoadStrikeSimulation.inject(10, 1, 20));
await LoadStrikeRunner
.registerScenarios(scenario)
.withReportingInterval(5)
.withReportingSinks(sink)
.withRunnerKey("rkl_your_local_runner_key")
.run();
})();
Custom reporting contract
Implement this contract in your SDK when you need to send LoadStrike data to a destination that is not covered by the built-in sinks.
Provide a stable sink name so LoadStrike can validate registration and record sinkErrors against the correct destination.
Resolve configuration, create clients, and prepare any sink-owned state before the run begins.
Receives session metadata such as the test suite, test name, and timestamps after sink initialization succeeds.
Called at the configured reporting interval with scenario statistics and projected metrics snapshots.
Receives the final run artifact automatically at the end of the run. You do not need to enable a separate finalizer hook.
Stop ends active sink work cleanly. Dispose is the final best-effort cleanup phase for sockets, writers, or client objects. Dispose failures are recorded in sinkErrors with phase=dispose.
Registers one or more sink instances on the runner or context. Custom reporting sinks require Enterprise.