Skip to content
ConsoleMonster

Incident workbench

Click a sample and the chart follows, drag the split, zoom with the wheel, then work the runbook, all with the mouse or the keys.

Demos Terminal apps Showcase

Use it when someone needs to read a spike, find the moment it started and work a checklist without leaving the terminal.

  • Live
  • 110×28
  • PowerShell 7.4+
  • .NET 8.0+
Incident workbench 110×2824-bitFull

How it's built

The whole program in one file, with its sample data at the top. It drew the recording above; all three styles produce the same frame bytes.

PowerShellincident-workbench.ps1View on GitHub
# An incident workbench: samples linked to their chart, a resizable split and a runbook.

Import-Module -Name ConsoleMonster
# All data is synthetic and inline; no services are contacted or changed.
$p50 = @(
    41, 43, 40, 44, 42, 45, 47, 46, 52, 88, 131, 164
    158, 120, 84, 61, 52, 48, 46, 44, 45, 43, 42, 44
)
$p95 = @(
    96, 98, 95, 101, 99, 104, 108, 112, 140, 262, 410, 486
    472, 355, 230, 160, 128, 117, 110, 104, 103, 100, 98, 101
)
$errors = @(0, 0, 0, 1, 0, 0, 1, 0, 3, 18, 47, 62, 58, 31, 12, 4, 1, 0, 0, 1, 0, 0, 0, 0)
$samples = for ($i = 0; $i -lt $p50.Count; $i++) {
    $minutes = $i * 5
    [pscustomobject]@{
        Time   = '{0:D2}:{1:D2}' -f (13 + [int][math]::Floor($minutes / 60)), ($minutes % 60)
        P50    = $p50[$i]
        P95    = $p95[$i]
        Errors = $errors[$i]
    }
}

# Layout: construct the widgets shown on screen.
$chart = New-ConsoleChart -Type Line -Unit ' ms' -Series @(
    [pscustomobject]@{ Key = 'p50'; Values = $p50 }
    [pscustomobject]@{ Key = 'p95'; Values = $p95 }
)
$latency = New-ConsoleSplitter -Key Panes -Ratio 0.4 -Fill -First {
    New-ConsoleDataGrid -Key Samples -InputObject $samples -LinkKey Sample
} -Second {
    New-ConsoleChartView -Key Latency -Chart $chart -PointLabels $samples.Time -LinkKey Sample
}
$done = @{ OnText = 'Done'; OffText = 'To do' }
$check = New-ConsoleCollapsible -Key Check -Expanded -Content {
    New-ConsoleToggle -Key Dashboards -Label 'Error rate confirmed' -Value $true @done
    New-ConsoleToggle -Key Deploys -Label 'Deploy at 13:40 matches the spike' -Value $true @done
}
$mitigate = New-ConsoleCollapsible -Key Mitigate -Content {
    New-ConsoleToggle -Key Rollback -Label 'Roll back api-gateway to 4.12.1' @done
    New-ConsoleToggle -Key Drain -Label 'Drain the eu-west-2 pool' @done
}
$verify = New-ConsoleCollapsible -Key Verify -Content {
    New-ConsoleToggle -Key P95Normal -Label 'p95 back under 150 ms for 15 minutes' @done
}
$steps = New-ConsoleOutline -Key Steps -SelectedIndex 0 -Items @(
    [pscustomobject]@{ Title = 'Check'; Target = $check }
    [pscustomobject]@{ Title = 'Mitigate'; Target = $mitigate }
    [pscustomobject]@{ Title = 'Verify'; Target = $verify }
)
$runbook = New-ConsoleSplitter -Ratio 0.22 -Fill -First { $steps } -Second {
    New-ConsoleAccordion -Content { $check; $mitigate; $verify }
}
$content = {
    New-ConsoleTabs -Key View -Content {
        @{ Title = 'Latency'; Content = $latency }
        @{ Title = 'Runbook'; Content = $runbook }
    }
}

# Behaviour: click or arrow through samples and the chart follows; drag the divider, scroll the
# wheel over the chart to zoom, click a legend entry to hide its series, right-click for the
# keys that work there, and click a tab, an outline entry or a toggle. Escape exits.
Show-ConsoleApp -Title 'Incident 4127 · api-gateway latency' -Content $content
C#Program.csView on GitHub
// An incident workbench: samples linked to their chart, a resizable split and a runbook.
// All data is synthetic and inline; no services are contacted or changed.
using ConsoleMonster;
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;

double[] p50 =
{
    41, 43, 40, 44, 42, 45, 47, 46, 52, 88, 131, 164,
    158, 120, 84, 61, 52, 48, 46, 44, 45, 43, 42, 44
};
double[] p95 =
{
    96, 98, 95, 101, 99, 104, 108, 112, 140, 262, 410, 486,
    472, 355, 230, 160, 128, 117, 110, 104, 103, 100, 98, 101
};
int[] errors = { 0, 0, 0, 1, 0, 0, 1, 0, 3, 18, 47, 62, 58, 31, 12, 4, 1, 0, 0, 1, 0, 0, 0, 0 };
string[] times = new string[p50.Length];
for (int i = 0; i < p50.Length; i++) { times[i] = $"{13 + (i * 5 / 60):D2}:{i * 5 % 60:D2}"; }

// Layout: construct the widgets shown on screen.
LineChart chart = new LineChart { Unit = " ms" };
for (int i = 0; i < p50.Length; i++) { chart.Add("p50", p50[i]); }
for (int i = 0; i < p95.Length; i++) { chart.Add("p95", p95[i]); }
Grid samples = new Grid("Samples")
    .AddColumn("Time")
    .AddColumn("P50")
    .AddColumn("P95")
    .AddColumn("Errors");
for (int i = 0; i < p50.Length; i++)
{
    samples.AddRow(times[i], times[i], p50[i], p95[i], errors[i]);
}
ChartView latencyChart = new ChartView("Latency", chart) { LinkKey = "Sample" };
foreach (string time in times) { latencyChart.PointLabels.Add(time); }
IWidget latency = new Splitter(
    new DataGrid("Samples", samples) { LinkKey = "Sample" },
    latencyChart)
{
    Key = "Panes",
    Ratio = 0.4
}.Fill();
Collapsible check = new Collapsible("Check", new Rows(
    new Toggle("Dashboards", true)
    {
        Label = "Error rate confirmed", OnText = "Done", OffText = "To do"
    },
    new Toggle("Deploys", true)
    {
        Label = "Deploy at 13:40 matches the spike", OnText = "Done", OffText = "To do"
    }))
{
    Expanded = true
};
Collapsible mitigate = new Collapsible("Mitigate", new Rows(
    new Toggle("Rollback", false)
    {
        Label = "Roll back api-gateway to 4.12.1", OnText = "Done", OffText = "To do"
    },
    new Toggle("Drain", false)
    {
        Label = "Drain the eu-west-2 pool", OnText = "Done", OffText = "To do"
    }));
Collapsible verify = new Collapsible("Verify", new Toggle("P95Normal", false)
{
    Label = "p95 back under 150 ms for 15 minutes", OnText = "Done", OffText = "To do"
});
Outline steps = new Outline("Steps")
{
    Items =
    {
        new OutlineItem("Check") { Target = check },
        new OutlineItem("Mitigate") { Target = mitigate },
        new OutlineItem("Verify") { Target = verify }
    },
    SelectedIndex = 0
};
IWidget runbook = new Splitter(steps, new Accordion(check, mitigate, verify)) { Ratio = 0.22 }
    .Fill();
Rows layout = new Rows(
    new HeaderBar("Incident 4127 · api-gateway latency"),
    new Tabs("View").Add("Latency", latency).Add("Runbook", runbook));

// Behaviour: click or arrow through samples and the chart follows; drag the divider, scroll the
// wheel over the chart to zoom, click a legend entry to hide its series, right-click for the
// keys that work there, and click a tab, an outline entry or a toggle. Escape exits.
ConsoleApp.Run(layout);
C# fluentProgram.Fluent.csView on GitHub
// An incident workbench: samples linked to their chart, a resizable split and a runbook.
// All data is synthetic and inline; no services are contacted or changed.
using ConsoleMonster;
using ConsoleMonster.Interactive;
using ConsoleMonster.Layout;
using ConsoleMonster.Widgets;

double[] p50 =
{
    41, 43, 40, 44, 42, 45, 47, 46, 52, 88, 131, 164,
    158, 120, 84, 61, 52, 48, 46, 44, 45, 43, 42, 44
};
double[] p95 =
{
    96, 98, 95, 101, 99, 104, 108, 112, 140, 262, 410, 486,
    472, 355, 230, 160, 128, 117, 110, 104, 103, 100, 98, 101
};
int[] errors = { 0, 0, 0, 1, 0, 0, 1, 0, 3, 18, 47, 62, 58, 31, 12, 4, 1, 0, 0, 1, 0, 0, 0, 0 };
string[] times = new string[p50.Length];
for (int i = 0; i < p50.Length; i++) { times[i] = $"{13 + (i * 5 / 60):D2}:{i * 5 % 60:D2}"; }

// Layout: construct the widgets shown on screen.
LineChart chart = (LineChart)Ui.Chart().Type(ChartType.Line).Unit(" ms").Build();
for (int i = 0; i < p50.Length; i++) { chart.Add("p50", p50[i]); }
for (int i = 0; i < p95.Length; i++) { chart.Add("p95", p95[i]); }
Grid samples = Ui.Grid()
    .Title("Samples")
    .AddColumn("Time")
    .AddColumn("P50")
    .AddColumn("P95")
    .AddColumn("Errors");
for (int i = 0; i < p50.Length; i++)
{
    samples.AddRow(times[i], times[i], p50[i], p95[i], errors[i]);
}
IWidget latency = Ui.Splitter()
    .First(c => c.DataGrid(grid => grid.Key("Samples").Grid(samples).LinkKey("Sample")))
    .Second(c => c.ChartView(view => view.Key("Latency")
        .Chart(chart)
        .LinkKey("Sample")
        .PointLabels(times)))
    .Key("Panes")
    .Ratio(0.4)
    .Fill();
Collapsible check = ((Collapsible)Ui.Collapsible().Key("Check").Content(Ui.Rows().Content(c =>
{
    c.Toggle(toggle => toggle.Key("Dashboards")
        .Value(true)
        .Label("Error rate confirmed")
        .OnText("Done")
        .OffText("To do"));
    c.Toggle(toggle => toggle.Key("Deploys")
        .Value(true)
        .Label("Deploy at 13:40 matches the spike")
        .OnText("Done")
        .OffText("To do"));
})).Expanded().Build());
Collapsible mitigate = ((Collapsible)Ui.Collapsible().Key("Mitigate").Content(Ui.Rows().Content(c =>
{
    c.Toggle(toggle => toggle.Key("Rollback")
        .Value(false)
        .Label("Roll back api-gateway to 4.12.1")
        .OnText("Done")
        .OffText("To do"));
    c.Toggle(toggle => toggle.Key("Drain")
        .Value(false)
        .Label("Drain the eu-west-2 pool")
        .OnText("Done")
        .OffText("To do"));
})).Build());
Collapsible verify = ((Collapsible)Ui.Collapsible()
    .Key("Verify")
    .Content(((Toggle)Ui.Toggle()
        .Key("P95Normal")
        .Value(false)
        .Label("p95 back under 150 ms for 15 minutes")
        .OnText("Done")
        .OffText("To do")
        .Build()))
    .Build());
Outline steps = ((Outline)Ui.Outline()
    .Key("Steps")
    .Build()).AddItem(new OutlineItem("Check").Target(check))
    .AddItem(new OutlineItem("Mitigate").Target(mitigate))
    .AddItem(new OutlineItem("Verify").Target(verify))
    .SelectedIndex(0);
IWidget runbook = Ui.Splitter().First(steps).Second(Ui.Accordion().Content(c =>
{
    c.Add(check);
    c.Add(mitigate);
    c.Add(verify);
})).Ratio(0.22).Fill();
Rows layout = Ui.Rows().Content(c =>
{
    c.HeaderBar(headerBar => headerBar.Title("Incident 4127 · api-gateway latency"));
    c.Tabs(tabs => tabs.Key("View").Add("Latency", latency).Add("Runbook", runbook));
});

// Behaviour: click or arrow through samples and the chart follows; drag the divider, scroll the
// wheel over the chart to zoom, click a legend entry to hide its series, right-click for the
// keys that work there, and click a tab, an outline entry or a toggle. Escape exits.
ConsoleApp.Run(layout);

Run it yourself

Three steps in PowerShell, four in C#. Every command copies with one click.

  1. Install the module

    Once. Windows PowerShell 5.1 or PowerShell 7 on Windows, macOS or Linux.

    Install-Module ConsoleMonster -Scope CurrentUser
  2. Save the program

    Download it, or copy it from above into incident-workbench.ps1.

  3. Run it

    From the folder you saved it in.

    ./incident-workbench.ps1
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n IncidentWorkbench; cd IncidentWorkbench
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run
  1. Create a project

    A plain console app; .NET 8 or later.

    dotnet new console -n IncidentWorkbench; cd IncidentWorkbench
  2. Save the project

    The project selects one C# style at a time and restores ConsoleMonster.

  3. Save both programs

    Replace Program.cs and save Program.Fluent.cs beside it.

  4. Run it

    dotnet run --property:DemoStyle=Fluent

Or skip the copying. Run the bundled program or create it from the project template.

Show-ConsoleDemo incident-workbench
$source = (Get-ConsoleDemo incident-workbench).Source
[IO.File]::WriteAllText("$PWD/incident-workbench.ps1", $source, [Text.UTF8Encoding]::new($true))
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo incident-workbench -n IncidentWorkbench
dotnet new install ConsoleMonster.Templates
dotnet new consolemonster --demo incident-workbench --DemoStyle Fluent -n IncidentWorkbench

Make it yours

Start with these, then read the rest of the file.

  1. Load your own samples: a service's latency, a queue's depth, a build's duration.

  2. Turn your team's runbook into outline entries, with a toggle for each step.

  3. Give the grid and the chart the same link key and keep their rows in the same order.

More terminal apps

All demos