DEV Community

Avery Lin
Avery Lin

Posted on

Your Alerts Are Not a Story Yet: Turn Noisy Logs Into a Reviewable Timeline

The worst part of an incident is rarely the fix. It is the hour before the fix, when five people are staring at five different tools and everyone has a different version of what happened first.

Alerting systems are good at saying something crossed a threshold. They are bad at answering the question humans actually need during triage: what is the shortest defensible timeline we can put in front of the team right now? Not a postmortem. Not a root-cause claim. A working narrative with evidence pointers, uncertainty marks, and enough structure that another engineer can challenge it without rereading 40,000 log lines.

This is a workflow for that gap. It uses an AI model only for compression and phrasing, not for truth. The model proposes a timeline; deterministic checks decide whether the proposal is even allowed into the room. Nothing in this approach executes generated code, and nothing asks the model to be an oracle. It is closer to giving a very fast intern a pile of logs and a strict form: fill out the timeline, cite your evidence, and mark anything you are guessing.

The rule that makes it safe

The model is allowed to do three things:

  1. Group events that look related.
  2. Draft candidate timeline entries in a fixed JSON shape.
  3. Point to the exact log lines or alert IDs that support each entry.

It is not allowed to do three things:

  1. Invent timestamps, services, request IDs, or error messages.
  2. State root cause as fact.
  3. Produce free-form prose that cannot be validated.

That boundary matters because incident language has a strange failure mode: fluent nonsense sounds calmer than messy truth. A sentence like “the deploy likely triggered cascading cache misses” can soothe a room even when the only evidence is one timeout and a hunch. The workflow below treats every model sentence as a claim that must either cite evidence or be labeled hypothesis.

Artifact: a strict timeline envelope

Start with an envelope that forces separation between observed events and interpretation.

{
  "incident_window": {"start": "2026-08-12T14:03:00Z", "end": "2026-08-12T14:41:00Z"},
  "entries": [
    {
      "ts": "2026-08-12T14:05:11Z",
      "kind": "observation",
      "service": "checkout-api",
      "summary": "p99 latency alert fired for /pay",
      "evidence": ["alert:grafana:88f1", "log:checkout-api:req_9d2"],
      "confidence": "high"
    },
    {
      "ts": "2026-08-12T14:06:02Z",
      "kind": "hypothesis",
      "service": "inventory",
      "summary": "inventory retries may have amplified checkout latency",
      "evidence": ["log:inventory:retry_burst_excerpt_3"],
      "confidence": "medium"
    }
  ],
  "open_questions": ["Did the config rollout at 14:04 reach the checkout-api pods?"],
  "not_supported": ["database failover occurred"]
}
Enter fullscreen mode Exit fullscreen mode

The important fields are not summary. They are kind, evidence, and not_supported. kind prevents interpretation from cosplaying as fact. evidence forces every entry to carry receipts. not_supported is a pressure valve: it gives the model a place to put tempting claims that the data does not prove, so they are less likely to leak into the main narrative.

Redact before you summarize

Incident logs are full of things you do not want to send to any model endpoint: customer emails, auth headers, session tokens, internal hostnames, sometimes payment-adjacent identifiers. Redaction has to happen before prompt construction, not after the model answers.

A minimal pre-filter looks like this:

import re
from dataclasses import dataclass

PATTERNS = {
    "email": re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"),
    "bearer": re.compile(r"Bearer\s+[A-Za-z0-9._~+/=-]+", re.I),
    "cookie": re.compile(r"(?i)(cookie|set-cookie):[^\n]+"),
    "ipv4": re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"),
}

@dataclass
class RedactedChunk:
    text: str
    hits: dict[str, int]

def redact(raw: str) -> RedactedChunk:
    hits = {}
    text = raw
    for name, pat in PATTERNS.items():
        text, n = pat.subn(f"[{name.upper()}_REDACTED]", text)
        hits[name] = n
    return RedactedChunk(text=text, hits=hits)
Enter fullscreen mode Exit fullscreen mode

Keep the redaction counts. If a chunk redacts 300 emails and 40 bearer tokens, that is not necessarily a better chunk for the model; it may be a sign that this slice should be summarized locally or reviewed by a human first. Redaction is not only compliance hygiene. It changes the economics of attention: the model sees structure, not secrets.

Chunk by shape, not by line count

A common mistake is splitting logs every N lines. Incidents do not respect line counts. Better chunks are shaped around triage questions:

  • A deploy/config-change window: five minutes before and after any rollout marker.
  • A first-alert window: from the first page-worthy alert to ten minutes after.
  • A service-edge window: ingress and gateway logs around the user-visible symptom.
  • A dependency window: database, queue, cache, and third-party calls that share request IDs with failing edge requests.

Each chunk gets a small header with known facts: time range, services included, redaction counts, and the question the model should answer. Example header:

WINDOW: 2026-08-12T14:03:00Z..2026-08-12T14:12:00Z
SERVICES: checkout-api, inventory, payments-gateway
QUESTION: List only events that could plausibly precede the first latency alert.
REDACTIONS: email=12 bearer=3 cookie=1 ipv4=27
OUTPUT: JSON envelope v1; no root-cause claims; cite evidence IDs.
Enter fullscreen mode Exit fullscreen mode

This header is doing more than prompt decoration. It narrows the job so the model is less tempted to write a memoir. You are not asking “what happened?” You are asking “which evidence-bearing events belong in the timeline for this slice?”

Validate the envelope before anyone reads it

The model output should fail fast if it cannot keep the form. JSON Schema is enough for the shape; a second pass checks evidence references against the actual log index.

import json
from jsonschema import validate

ENVELOPE_SCHEMA = {
  "type": "object",
  "required": ["incident_window", "entries", "open_questions", "not_supported"],
  "properties": {
    "incident_window": {
      "type": "object",
      "required": ["start", "end"],
      "properties": {"start": {"type": "string"}, "end": {"type": "string"}}
    },
    "entries": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["ts", "kind", "service", "summary", "evidence", "confidence"],
        "properties": {
          "ts": {"type": "string"},
          "kind": {"enum": ["observation", "hypothesis", "correction"]},
          "service": {"type": "string"},
          "summary": {"type": "string", "maxLength": 220},
          "evidence": {"type": "array", "minItems": 1, "items": {"type": "string"}},
          "confidence": {"enum": ["low", "medium", "high"]}
        }
      }
    },
    "open_questions": {"type": "array", "items": {"type": "string"}},
    "not_supported": {"type": "array", "items": {"type": "string"}}
  }
}

def parse_envelope(text: str) -> dict:
    data = json.loads(text)  # strict: no markdown fences, no commentary
    validate(data, ENVELOPE_SCHEMA)
    return data

def evidence_exists(envelope: dict, known_ids: set[str]) -> list[str]:
    missing = []
    for e in envelope["entries"]:
        for ev in e["evidence"]:
            if ev not in known_ids:
                missing.append(f'{e["service"]}:{e["ts"]} cites unknown {ev}')
    return missing
Enter fullscreen mode Exit fullscreen mode

If validation fails, do not ask the same model to “try harder” in a loop until it passes. That quietly teaches the process to reward persistence over accuracy. Record the failure, inspect the chunk, and usually shrink the question. Most invalid outputs come from a chunk that asked for too much: too many services, too wide a window, or a question that invited causality.

Use disagreement as a feature

One model run gives you a draft. Two independent passes give you a map of disagreement, which is often more useful than either answer.

Run the same redacted chunk through two prompts with different stances:

  • Chronicler: list observations only; conservative; no causal language.
  • Skeptic: propose only hypotheses that could explain the symptom, each with the evidence that would falsify it.

Then diff the outputs mechanically:

Result Meaning Triage move
Same event, same evidence Strong candidate timeline entry Add to shared timeline
Same event, different timestamp Clock skew or bad parsing Check log source timezone before trusting either
Chronicler omits, Skeptic cites Possible hidden dependency Pull narrower request-ID trace
Skeptic hypothesis has no evidence Room rumor Move to not_supported unless someone finds a receipt
Both cite same alert but opposite order Sequencing ambiguity Sort by monotonic trace ID, not wall clock alone

This is where free model access can be genuinely useful rather than merely cheap. Timeline reconstruction wants lots of small, bounded calls across many chunks; it is exactly the kind of workload where people skip a second pass because each extra call feels like overhead. Disclosure: This article was prepared as part of MonkeyCode's product outreach. In this workflow, MonkeyCode’s operator-supplied free model access and free server option are relevant only as a place to run the redaction-plus-validation loop and the two-stance comparison without turning every extra chunk into a procurement thought. I am not assuming specific models, quotas, permanence, or performance; check current terms before wiring an incident process to any free tier.

The architectural point is independent of provider: keep the summarizer swappable, keep validation local and deterministic, and let disagreement drive human attention. If the free endpoint disappears next quarter, your envelope, redactor, schema, and evidence index should still work.

A rollout plan that will not annoy your team

Do not introduce this as “AI incident response.” Introduce it as a better first fifteen minutes.

Phase 1: shadow mode. After the next two incidents, run the workflow offline on saved logs. Compare the generated timeline with what the team actually believed at minute 10, minute 30, and after resolution. You are looking for one thing: does the envelope surface a sequencing mistake earlier than memory does?

Phase 2: one service, one alert type. Pick a noisy but well-instrumented path, like checkout latency or queue backlog. Keep the output read-only in chat: “draft timeline, challenge welcome.” No automatic paging, no auto-postmortem, no status-page text.

Phase 3: pre-incident hygiene. The workflow gets much better if evidence IDs already exist. Add stable alert IDs, propagate request IDs across service boundaries, and make deploy markers easy to grep. Boring instrumentation beats clever prompting.

Only after those steps should anyone discuss drafting customer-facing language, and even then the model should summarize approved facts, not decide what is approved.

Limitations worth saying out loud

  • It can formalize bias. If your logs mostly record the services you already suspect, the timeline will be beautifully cited and still wrong.
  • Clocks are liars. Cross-service ordering needs trace context or at least known skew. A schema cannot fix physics.
  • Redaction is not anonymization. Aggregated behavior can still identify customers in small populations. Sensitive incidents may belong entirely inside your boundary.
  • Free tiers are operationally soft. Do not make a paging path depend on any endpoint whose limits and availability you have not verified for your own account and region.
  • A validated timeline is not a cause. It is a better argument surface. The win is fewer confident wrong stories in the first hour, not automatic truth.

Who should not use this

Skip it if your incidents are rare, your logs are sparse, or your team cannot yet agree on basic evidence IDs. Skip it if leadership wants a machine to declare root cause; this workflow is intentionally bad at that. Skip it if sending any operational text off-box would violate policy, unless every part runs inside an approved boundary. And skip it for the first hour of a security incident unless your security team has explicitly blessed the data path.

The durable lesson is smaller than the hype: during triage, teams do not need more words. They need a shared, challengeable sequence of events with receipts. An AI model can help compress the noise, but only if the surrounding process treats fluency as a liability and evidence as the admission ticket. If you experiment with this, start by building the envelope and redactor before choosing any endpoint; the model is the replaceable part.

Top comments (0)