8
8
Table of Contents

At CloudKeeper, our team ships a wide range of services — backend APIs, front-end apps, Kong API gateways, Fluent Bit and Fluentd stacks — through a single shared Jenkins library. Every run posts its outcome to a Slack channel: green for success, red for failure, yellow for aborted. It works, and the team relies on it.

But over time we noticed something. The red messages told us a build had failed. They never told us why. And answering that question — every single time — quietly consumed more engineering hours than the fixes themselves.

So we built a small AI layer into our failure notifications. Now, when a build fails, the Slack alert arrives with the likely root cause already written into it. This is the story of why and how.

The Problem: Diagnosis, Not Detection

Jenkins is very good at detection. The moment a pipeline breaks, it stops, marks the build red, and fires a notification. That part was never broken.

The problem was everything that happened after the notification. A failure message looked like this:

Jenkins Detection

Functional, but inert. It's a link, not an answer. To learn anything, an engineer had to leave Slack, open Jenkins, and start reading. Four things made that costly:

  • The signal is buried. A single failed deployment can produce thousands of lines of console output — Gradle dependency trees, Amazon ECS task-definition JSON, Git changeset warnings, stage-skip notices. The one line that actually mattered sat somewhere in the middle.
  • The noise looks like signal. Lines such as Stage "X" skipped due to earlier failure and Unable to retrieve changeset read like errors but aren’t. Engineers — and any naive keyword search — routinely latched onto the wrong line and chased a red herring for ten minutes before backing out.
  • Nothing accumulated. The same expired credential, the same missing dependency, the same repository returning 403 Forbidden would be re-diagnosed from scratch every time, often by someone who had never seen it before.
  • The cost repeated on every failure. It was a needle-in-a-haystack exercise, run again for each red build, by whoever happened to be on hand.

The bottleneck, in other words, was never detection. It was diagnosis. That is the part we set out to automate.

The Reframe: Put the Root Cause in the Alert

The instinct with a noisy failure channel is to add more — more log lines in the message, more links, more dashboards. But dumping fifty log lines into Slack just moves the haystack from Jenkins to Slack. It doesn't answer the question.

We reframed the goal. The alert shouldn't carry more information; it should carry the right information—a short, plain-language statement of what broke and why, in the same message that already told you the build failed.

That reframing set our constraints. To be useful, the system had to read unstructured logs the way an experienced engineer would, focus on the real error and skip the noise, remember what it had already seen so recurring failures were free and instant, never leak a secret to an external model, and — above all — never break the notification it was trying to improve.

Design Principles

Four principles shaped the build — and they map directly onto the order the analyzer actually runs each failure through.

a) Sanitize before you send

The analyzer first scrubs the log. Console output is full of credentials, so before a single byte leaves our environment, it redacts passwords, tokens, API keys, AWS access and secret keys, Bearer and Basic headers, database connection strings, and PEM private-key blocks. We get the benefit of a powerful external model without ever handing it a secret.

b) Extract the signal, ignore the noise

Next, rather than passing the whole log along, the analyzer isolates the section that actually matters and strips the lines that reliably mislead. Less noise in means a sharper diagnosis out.

c) Let the database remember, let AI reason

Only then does it consider the model — and even here, the model is the last resort, not the first. The extracted error is normalized, hashed, and looked up in a database of previously diagnosed failures. If it's been seen before, the answer comes straight from the cache. Only a genuinely new error reaches the model. Diagnose each problem once; serve every recurrence from memory.

d) Degrade gracefully — never lose the alert

Wrapping all of the above: the enhancement must never become a new single point of failure. If the model is unreachable, the database is down, or a dependency is missing, the system falls back to the standard failure notification. The enhancement is optional; the alert is not.

The AI Layer: Turning Logs Into a Root Cause

When an error is genuinely new, the analyzer sends its sanitized, focused context to a hosted large language model with a tightly scoped prompt. The model is told to act as an expert DevOps service provider and to return a 2–3 sentence root cause that fits cleanly in a Slack message, highlighting the specific dependency, error code, or class involved in backticks and skipping filler.

The design choice here mirrors our first principle: let code count, let AI narrate. We don't ask the model to decide which log lines are errors, remember anything, or count occurrences. Deterministic code handles extraction and caching. The model does only what it's uniquely good at — reading messy human text and explaining it in plain language.

A representative result:

The build failed due to missing dependencies and failed downloads. The com.example:app-dtos:x.y.z and com.example:app-core:x.y.z dependencies aren't downloading correctly, resulting in a 403 Forbidden error. The build is also unable to find symbol for classes like SomeResponseDTO.

Three sentences. A stage lead can read that in Slack and know, without opening Jenkins, that this is a repository-access problem, not a code bug.

Architecture: A Shared Library, a Cache, and a Model

The whole system is deliberately small. No new service to operate — it lives inside the Jenkins shared library every pipeline already imports, backed by one PostgreSQL table and one API call.

Jenkins Architecture
Figure 1 — End-to-end flow, from a failed pipeline stage to an enhanced Slack alert.

Where it hooks in

Our library tracks the failing stage by recording env.STAGE_NAME into a FAILED_STAGE variable in each stage's error handler. When the pipeline reaches its failure post-block, it calls the analyzer with the channel, the failed stage, and credentials pulled from the Jenkins credential store — never hard-coded:

Jenkins code snippet
Adopting the feature in a new pipeline is a one-line change to its failure block.

The cache: check the database before calling AI

This is the heart of the design, and the reason the system is fast and cheap rather than slow and costly. Every failure hits the database first. The AI is consulted only when the database has never seen the error before.

The trick that makes this work is normalization. Before looking anything up, the analyzer strips everything variable from the error text—timestamps, build numbers, file paths, workspace directories, task and container IDs, dependency version numbers, URLs—and hashes what remains. Two occurrences of the same underlying problem therefore produce the same hash, even when their raw log text differs from run to run. That hash is the key into a PostgreSQL table, build_failures.

Build failure check
Figure 2 — The cache decision: a hit answers instantly from PostgreSQL; only a miss reaches the model.
  • On a hit — the same failure has occurred before — the stored root cause is returned in milliseconds, no AI request is made at all, and the row's occurrence_count is incremented and its timestamp refreshed.
    On a miss — a new error — the model is consulted once, and its answer is written back keyed by that same normalized hash. The next occurrence, on any pipeline, for any engineer, is now a hit.

The effect compounds. Every table stores, per unique failure, the error_hash, a snapshot of the error pattern, the failed stage, the job name, the AI-generated root cause, a running occurrence_count, and timestamps. The longer the platform runs, the more failures resolve straight from memory — and that occurrence_count becomes a signal in its own right. A count that keeps climbing signals a chronic problem that needs a permanent fix.

Results: The Alert That Explains Itself

The change engineers actually feel is the failure alert itself. Same channel, same red banner — but now it carries the answer.

Slack Alert Jenkins
For our prod channel
non-prod Jenkins slack message

The transformation is easiest to see side by side.

From:

Jenkins code

To:
Jenkins code

What that buys the team:

  • The slowest step of triage disappears. The "what happened?" phase now takes seconds, not minutes, and for a large class of failures needs no trip to Jenkins.
  • Less context-switching. A glance at Slack is often enough to decide whether a failure is yours, a known flaky issue, or a real incident — no focus lost to log archaeology.
  • Recurring failures are free. Once diagnosed, a repeat failure is answered from PostgreSQL in milliseconds, and its climbing occurrence_count flags the systemic problems worth fixing for good.
  • Cost stays proportional to distinct problems, not total failures. Because the model is consulted only on a cache miss, AI spend tracks the number of new errors, not the raw volume of red builds.

Lessons Learned

A few things stood out once it was running.

  • Normalization is the whole game. A cache is only as good as its ability to recognize that two differently worded logs are the same failure. Almost all our tuning went into stripping variable noise—versions, paths, IDs—so that recognition actually holds.
  • The noise filter matters as much as the AI. The single biggest quality improvement came not from a better prompt but from removing the misleading lines before the model ever saw them. Give a model clean context and it reasons well; give it Amazon ECS config JSON, and it will confidently explain the wrong thing.
  • Guardrails belong in code, not in the prompt. Counting, caching, secret redaction, and error extraction are deterministic jobs. Leaving them to the model would have been slower, costlier, and less trustworthy. The model narrates; the code decides.
  • Never let the enhancement break the basics. The graceful fallback earned its keep. On the days the model or database hiccuped, the plain notification still went out, and no one noticed anything was wrong.

Conclusion: From Red to Root Cause

Jenkins will always tell you that a build failed. The gap has always been the distance between that red status and an actual understanding of why — and closing it, over and over, by hand, was costing us real engineering time.

The fix wasn't another alert rule or a bigger dashboard. It was to read the logs once, remember the answer, and put it where people already look. A little sanitization, a normalized cache, and a model used sparingly turned a bare failure ping into an alert that explains itself.

When a build goes red, the question was never whether something broke. It was always what. Now the answer is already in the channel, waiting to be read — not an investigation waiting to begin.

12
Let's discuss your cloud challenges and see how CloudKeeper can solve them all!
Meet the Author
  • Prerana
    DevOps Engineer

    Prerana is a tech enthusiast with a passion for building scalable and reliable cloud systems.

No Comments Yet
Leave a Comment
Certified. Trusted. Industry Recognized.

Stop paying for cloud tools. Start paying for outcomes.

Get Started with CloudKeeper