Custom Reporting
Use custom reporting when you need a sink that is not built in and still want the normal LoadStrike sink lifecycle.
Reporting
Realtime reporting
Choose 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 sinks in one fixed order: Init, Start, SaveRealtimeStats, SaveRealtimeMetrics, SaveRunResult, Stop, then Dispose.
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, plugin-table rows, and correlation output when those features are enabled.
Failure And Cleanup Behavior
Init, start, realtime export, and final export use retry and backoff. Persistent sink failures disable only that sink for the run. Dispose is best-effort cleanup; if it throws, LoadStrike records the issue in sinkErrors and continues shutdown.
Plan Gate
Custom reporting remains an Enterprise-only extensibility feature. Built-in vendor sinks are available on Business and Enterprise, while custom sink registration requires the custom reporting entitlement.
Custom reporting setup
Implement the reporting sink contract for your SDK, register it with WithReportingSinks(...), and let LoadStrike send realtime snapshots plus the final LoadStrikeRunResult automatically.
Licensing note: every runnable sample still needs a valid runner key. Set it with WithRunnerKey("...") or the config key LoadStrike:RunnerKey before you run the test.
HTML reports also include the top-right Light/Dark theme toggle. Light is the default report theme.
Custom Reporting
using LoadStrike;
using Microsoft.Extensions.Configuration;
public sealed class InternalOpsSink : LoadStrikeReportingSink
{
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.AllRequestCount}");
}
return Task.CompletedTask;
}
public Task SaveRealtimeMetrics(LoadStrikeMetricStats metrics) => Task.CompletedTask;
public Task SaveRunResult(LoadStrikeRunResult result)
{
Console.WriteLine($"Final requests: {result.AllRequestCount}");
return Task.CompletedTask;
}
public Task Stop() => Task.CompletedTask;
public void Dispose()
{
}
}
var scenario = LoadStrikeScenario.CreateAsync("submit-orders", async _ =>
{
await Task.Delay(25);
return LoadStrikeResponse.Ok("200");
})
.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();
import com.loadstrike.runtime.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 java.util.Map;
import java.util.concurrent.CompletableFuture;
final class InternalOpsSink implements LoadStrikeReportingSink {
@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 saveRunResult(LoadStrikeRunResult result) {
System.out.println("Final requests: " + result.allRequestCount);
}
@Override
public void stop() {
}
@Override
public void dispose() {
}
}
var scenario = LoadStrikeScenario
.createAsync("submit-orders", _ -> 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_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 {
LoadStrikeReportingSink,
LoadStrikeResponse,
LoadStrikeRunner,
LoadStrikeScenario,
LoadStrikeSimulation
} from "@loadstrike/loadstrike-sdk";
const sink: LoadStrikeReportingSink = {
sinkName: "internal-ops",
saveRealtimeStats: async (scenarioStats) => {
for (const scenario of scenarioStats) {
console.log(`${scenario.scenarioName}: ${scenario.allRequestCount}`);
}
},
saveRealtimeMetrics: async () => {},
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 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 remain Enterprise-only.