Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install Playwright Chromium
run: npx playwright install --with-deps chromium

- name: Typecheck
run: npx tsc --noEmit

- name: Lint
run: npm run lint

- name: Build
run: npm run build
run: npm run build
195 changes: 195 additions & 0 deletions content/posts/enrichments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
---
id: enrichments
title: It Is 2 A.M. and Your Alert Says "Disk Full"
excerpt: How OpsiMate Enrichments turn bare alerts into context-rich action items automatically, without changing your source monitoring tools.
date: 2026-10-20
tags:
- alerts
- enrichments
- devops
- on-call
author:
name: Michael Uzukwu
avatarSrc: /images/logo.png
draft: false
cover: /images/enrichments-cover.png
---


It is late, and an alert fires. It says one thing: **"Disk full."**

No hostname. No runbook. No owner. Just the bare minimum, and now someone on-call has to open three tabs, find the runbook, look up which team owns the service, and figure out what to do next. The clock is ticking the whole time.

This is the part of on-call that never shows up in a dashboard: the gap between knowing something is wrong and knowing what to do about it. It is a small gap on a good night. On a bad night, it is the difference between a five-minute fix and a thirty-minute scramble.

OpsiMate has a feature that closes that gap. It is called **Enrichments**, and it is worth five minutes of your time.

## What Enrichments Do

An enrichment is a rule that runs when an alert arrives. When the alert matches a pattern you define, the rule attaches extra information to it, so that by the time anyone opens the alert, the context is already there.

You decide what gets attached. It can be:

- **Labels** to categorize the alert, like the owning team or the environment
- **Links** to runbooks, dashboards, or the source of the alert
- **A rewritten summary** that adds context to whatever the source sent

The rule runs automatically. Nobody has to click anything or fill anything in. The next time an alert is fetched, it simply arrives with more context than it had before.

```mermaid
flowchart TB
subgraph Control[" "]
style Control fill:none,stroke:none;

kgw["<b>Enrichment Engine</b><br/>(runs at fetch time)"]
end

subgraph Actions[" "]
style Actions fill:#f8fafc,stroke:#e2e8f0,stroke-width:2px,stroke-dasharray: 5 5;
C["<b>Add label</b><br/>owning_team=platform"]
D["<b>Add link</b><br/>Runbook"]
E["<b>Rewrite summary</b><br/>with team note"]
end

subgraph Result[" "]
style Result fill:none,stroke:none;
F["<b>Enriched alert</b><br/>ready to act on"]
end

A["Alert arrives<br/>(Disk full)"]
B{"Matches rule?"}

A --> B
B -->|Yes| kgw
kgw --> C
kgw --> D
kgw --> E
C & D & E --> F
B -->|No| G["Alert stays as-is"]

%% OpsiMate Blue Styling
classDef blueBox fill:#3b82f6,stroke:#2563eb,color:#ffffff,stroke-width:2px;
classDef darkBox fill:#1e293b,stroke:#0f172a,color:#ffffff,stroke-width:2px;
classDef whiteBox fill:#ffffff,stroke:#3b82f6,color:#1e293b,stroke-width:2px;

class kgw,C,D,E blueBox;
class F darkBox;
class A,B,G whiteBox;
```

*Figure 1: How an enrichment rule turns a raw alert into an enriched one.*

## A Real Example

Let us walk through what this looks like in practice.

### Enriching a Disk Full Alert

An alert comes in from a monitoring source. It says:

> **Disk full on web-01**

That is it. Nothing else.

Now let us say you have created an enrichment rule that matches any alert whose name contains "disk full." The rule does three things:

1. Adds a label called `owning_team` with the value of the alert's `team` tag
2. Attaches a link to the disk-full runbook
3. Rewrites the summary to append a note about paging the platform team

The next time that alert is fetched, it looks like this:

> **Disk full on web-01**
> Owner: platform
> Summary: Disk usage at 98 percent on web-01 — follow the runbook and page the platform team
> Links: Runbook

The alert is the same alert. Same source, same severity, same urgency. But the person who opens it now knows exactly what to do and who to talk to.

```mermaid
flowchart LR
subgraph Before["BEFORE"]
style Before fill:#ffffff,stroke:#3b82f6,stroke-width:2px,color:#1e293b,rx:8px,ry:8px;
B1["Disk full on web-01"]
end

subgraph After["AFTER"]
style After fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b,rx:8px,ry:8px;
A1["Disk full on web-01"]
A2["owner: platform"]
A3["links: Runbook"]
A4["summary: Disk usage at 98 percent<br/>follow the runbook and page the platform team"]

A1 ~~~ A2 ~~~ A3 ~~~ A4
end

Before -->|"enrichment rule applies"| After

%% OpsiMate Blue Styling
classDef whiteBox fill:#ffffff,stroke:#3b82f6,color:#1e293b,stroke-width:2px;
classDef blueBox fill:#3b82f6,stroke:#2563eb,color:#ffffff,stroke-width:2px;

class B1 whiteBox;
class A1,A2,A3,A4 blueBox;
```

*Figure 2: The same alert, before and after the enrichment rule applies*.

Here is what that looks like in the OpsiMate interface:


**After enrichment:**
![Alert after enrichment](/images/alert-after.png)


### Known Issues, Without Silencing the Alert

Not every alert needs action right now.

Sometimes you know about an issue, you know it is being fixed, and you know the fix date. But you do not want to turn off the alert in its source. That hides the issue from everyone and might let a real problem slip through.

Here is a case I see often in teams.

Kafka starts sending alerts: **Unable to connect to Kafka**. The team already knows about it. The broker is being replaced next week. The issue is being worked on. But the alert fires a dozen times a day, and every time it does, someone has to figure out whether this is the known issue or a new one.

An enrichment rule can match that alert and add a note to the summary:

> **Unable to connect to Kafka** — Known issue. Broker replacement scheduled for November 12, 2026.

The alert is still visible. It still fires. It is still in the pipeline. But everyone who sees it now knows it is already being tracked, and the on-call engineer does not have to ask in Slack whether this one is the known issue or something new.

That is a small feature doing a real job. It is not changing the source. It is not silencing the alert. It is not editing the monitoring tool. It is adding context, so everyone sees the same thing.

## Who This Is For

Enrichments are useful for any team that runs on-call. But they are especially helpful for:

- **NOC teams** that triage a high volume of alerts from many sources
- **Small teams** where the same person handles the alert, finds the runbook, and pages the right people
- **Platform teams** that want a consistent view of alerts, no matter which monitoring tool sent them

The feature is also good at one specific problem: **normalizing alert noise across tools.** Different monitoring sources use different words for the same severity. One says `Disaster`. Another says `error`. Another says `P1`. OpsiMate already maps these onto a fixed scale, and an enrichment rule can make sure every alert lands on the same three levels, no matter which source it came from.

## Why It Works

Three design decisions make Enrichments different from the alert-routing rules you may already have used elsewhere:

- **Rules run at read time, not at ingestion.** The original alert source is not modified. You can add, change, or remove a rule at any time, and the next fetch reflects the change. That means experimenting is safe.
- **Rules apply in priority order.** When several rules match the same alert, the higher-priority rule wins on conflicting fields. Summary templates chain, so a lower-priority rule can append to what a higher-priority rule already wrote.
- **Rules can match on labels added by other rules.** This is how you build layered rules. One rule classifies an alert, and a second rule reacts to that classification. The result is a small rule engine, not a flat list of matchers.

## How to Set It Up

Setting up an enrichment takes about five minutes. The OpsiMate docs have a full step-by-step walkthrough with screenshots, a worked example, and reference sections for every field.

If you are running OpsiMate locally, or on a team instance, you can start with the disk full example above and adapt it to your own alerts.

[Read the Enrichments guide in the OpsiMate docs →](https://docs.opsimate.dev/docs/alerts/enrichments)

## Try It and Tell Us What You Think

Enrichments was built to solve a real problem: the small gap between an alert firing and someone knowing what to do about it. If you try it and it saves you a few minutes on the next incident, we would love to hear about it.

If you find a rule that works especially well for your setup, share it. If something is unclear in the docs, tell us. OpsiMate is a community project, and the people using it are the ones who make it better.

You can find us on [GitHub](https://github.com/OpsiMate/OpsiMate) and in the OpsiMate community Slack.
8 changes: 8 additions & 0 deletions lib/posts.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import remarkRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
import rehypeStringify from "rehype-stringify";
import rehypeMermaid from "rehype-mermaid";
import { normalizeFrontMatter } from "@/lib/api-utils";
import type { Post } from "@/lib/posts";

Expand All @@ -28,12 +29,19 @@ export async function getPostById(id: string): Promise<Post | null> {
const raw0 = fs.readFileSync(fullPath, "utf8");
const raw = normalizeFrontMatter(raw0);
const parsed = fm<any>(raw);

const processed = await remark()
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
// 1. Sanitize the normal HTML from the markdown first
.use(rehypeSanitize, defaultSchema)
// 2. THEN render the Mermaid SVGs using the default theme
.use(rehypeMermaid, {
strategy: "inline-svg"
})
.use(rehypeStringify)
.process(parsed.body);

const contentHtml = processed.toString();

const data = parsed.attributes || {};
Expand Down
Loading