<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[SRE Diaries]]></title><description><![CDATA[Learn how we turned 3000+ chaotic Jenkins jobs into structured, actionable telemetry using Logstash, scikit-learn, and a read-only Grafana MCP server.]]></description><link>https://smahima.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 12 Nov 2026 09:16:56 GMT</lastBuildDate><atom:link href="https://smahima.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Ghost in the Apache: Debugging a Mystery Cron Job Across 250+ Legacy EC2 Instances]]></title><description><![CDATA[A war story about integrating a new RASP security tool, dealing with sudden third-party CDN blocks, and hunting down a ghost config-reverting cron.]]></description><link>https://smahima.hashnode.dev/the-ghost-in-the-apache-debugging-a-mystery-cron-job-across-250-legacy-ec2-instances</link><guid isPermaLink="true">https://smahima.hashnode.dev/the-ghost-in-the-apache-debugging-a-mystery-cron-job-across-250-legacy-ec2-instances</guid><category><![CDATA[Devops]]></category><category><![CDATA[SRE]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Security]]></category><category><![CDATA[incident response]]></category><category><![CDATA[Linux]]></category><category><![CDATA[cronjob]]></category><dc:creator><![CDATA[Mahima Singh]]></dc:creator><pubDate>Sun, 01 Nov 2026 11:42:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae6c6672c12b2614affb3e6/debf6292-4b54-4831-96ba-68cf85dc7b21.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every SRE or platform engineer has met the incident where the fix works, the ticket gets closed, and twenty minutes later the problem is back as if you had done nothing. This is the story of how a new security agent, a strict Content Security Policy, and a forgotten script hidden in a legacy Golden AMI broke <strong>250+ independent EC2 instances</strong>, and what we learned about finding automation nobody remembers writing.</p>
<p><strong>TL;DR</strong></p>
<ul>
<li><strong>Symptom:</strong> After a RASP/WAF agent was rolled out, customers lost buttons, dropdowns and styles in a legacy web app.</li>
<li><strong>Cause:</strong> A default Content-Security-Policy blocked third-party assets. Our fix then kept disappearing because a hidden cron script restored an old Apache config.</li>
<li><strong>Lesson:</strong> <code>crontab -l</code> shows one user's crontab, not the system. Audit <code>/etc/cron.*</code>, <code>/var/spool/cron/</code> and systemd timers, and inventory what you bake into base images.</li>
</ul>
<hr />
<h2>Incident Overview &amp; Impact</h2>
<ul>
<li><strong>Scale:</strong> 250+ dedicated EC2 instances running an un-migrated legacy Java monolith, one instance per customer environment.</li>
<li><strong>Impact:</strong> Intermittent UI failures, dead dropdowns and missing third-party assets for customers using the legacy portal during a migration period.</li>
<li><strong>My role:</strong> On-call engineer. I led triage, coordinated the fix across environments, verified browser-level behavior, and coordinated the fleet-wide remediation.</li>
</ul>
<table>
<thead>
<tr>
<th>Stage</th>
<th>What happened</th>
<th>Approx. time</th>
</tr>
</thead>
<tbody><tr>
<td>Initial outage</td>
<td>UI assets blocked by the new CSP until mitigation</td>
<td>~45 minutes</td>
</tr>
<tr>
<td>Recurrence</td>
<td>Apache config reverted after our fix, UI broke again</td>
<td>~20 minutes after the fix</td>
</tr>
<tr>
<td>Diagnosis</td>
<td>Tracing what was reverting the config</td>
<td>~35 minutes</td>
</tr>
</tbody></table>
<hr />
<h2>The Setup: The Monolith and the Security Injection</h2>
<p>The application was a legacy Java monolith made up of four core services bundled together. Each service powered a distinct part of the same product, with its own login flow and routing.</p>
<p>We were in the middle of a multi-year modernization, so deep refactoring was paused. The goal was to keep this environment stable and treat it as-is.</p>
<p>To improve our security posture, we rolled out a Web Application Firewall (WAF) and Runtime Application Self-Protection (RASP) solution, Rapid7 tCell. The agent was injected directly into the Java application's runtime.</p>
<p>Shortly after deployment, tickets started arriving: <strong>customers reported that core parts of the web application were behaving abnormally. Buttons were missing, dropdowns were dead, and UI assets were vanishing.</strong></p>
<hr />
<h2>Phase 1: The RASP &amp; CSP Culprit</h2>
<p>We opened an incident and went through the logs and browser dev tools. The root cause of the UI breakage was a classic modern web conflict:</p>
<ol>
<li><strong>The architecture:</strong> A browser requested the web app. The request passed through Apache, hit the Java application (where the tCell agent ran inside the JVM), and generated the response.</li>
<li><strong>The strict security header:</strong> Out of the box, the agent attached a restrictive Content-Security-Policy (CSP) header, along the lines of <code>default-src 'self'</code>. This tells the browser to load scripts, styles and assets only from the exact same origin.</li>
<li><strong>The third-party CDNs:</strong> The legacy app relied on JavaScript frameworks, font icons and styling components hosted on external CDNs and separate subdomains.</li>
<li><strong>The browser interdiction:</strong> Because those hosts were not allowed in the tCell CSP configuration, the browser blocked them. Visual components disappeared silently.</li>
</ol>
<h3>The Fix</h3>
<p>To solve this without turning security off, we did the following:</p>
<ul>
<li>Added the trusted external asset sources to the tCell CSP configuration in the tCell dashboard (for example <code>*.example.com</code> and <code>fonts.googleapis.com</code>, shown here as illustrative values).</li>
<li>Updated Apache's proxy configuration (<code>httpd.conf</code>) so the security headers generated by the Java agent passed through to the browser via <code>ProxyPassReverse</code> and were not overwritten or stripped by hardcoded Apache rules.</li>
<li>Temporarily switched tCell to <strong>Report-Only mode</strong> to catch any remaining missing-domain violations in the logs without breaking the production UI. After validating the policy in our staging environment, we moved back to <strong>enforcement mode</strong>.</li>
</ul>
<p>We applied the fixes to the primary environments, restarted Apache, verified that everything looked healthy, and closed the incident.</p>
<p>Or so we thought.</p>
<hr />
<h2>Phase 2: The Mystery of the Reverting Configuration</h2>
<p>About 20 minutes later, the alarms started again. The same UI failures were back.</p>
<p>We returned to the servers and found that <strong>the Apache configuration changes we had just made had been reverted to their original state.</strong></p>
<p>We did not have continuous configuration-drift correction (such as an always-on Ansible or Chef run) on these legacy nodes. Changes were pushed through Ansible Tower during deployments, and nothing was supposed to overwrite configuration files on a timer.</p>
<p>I went through the standard checklist:</p>
<ul>
<li>Did a deployment pipeline or automation job run? <em>No.</em></li>
<li>Was anyone else logged in or running scripts? <em>No.</em></li>
<li>Do the user crontabs show anything (<code>crontab -l</code>)? <em>Completely empty.</em></li>
<li>Is there unusual sudo activity in <code>/var/log/auth.log</code>? <em>Nothing out of the ordinary.</em></li>
</ul>
<p>The configuration was reverting like clockwork, yet no standard log or user crontab showed who or what was doing it. We were chasing a ghost.</p>
<hr />
<h2>Phase 3: Unmasking the Ghost in the System Cron Directories</h2>
<p>Since the user-level checks were empty, we widened the search to system-level scheduling and used our runtime audit tool, Twistlock, which records commands executed with elevated privileges. These were the discovery commands:</p>
<pre><code class="language-bash"># Check all cron directories and system cron tables
ls -la /etc/cron.*

# Search recursively across cron configuration for Apache references
grep -r "httpd" /etc/cron*

# Inspect systemd timers that might trigger background jobs
systemctl list-timers --all
</code></pre>
<p>The directory listing exposed the culprit: a script in <strong><code>/etc/cron.hourly/</code></strong>, completely invisible to <code>crontab -l</code>. The audit trail of root-executed commands lined up with it, showing Apache being restarted at the same moments the configuration reverted.</p>
<p>A redacted version of the script's logic:</p>
<pre><code class="language-bash">#!/bin/bash
# Legacy Apache "safety net": restore the baseline config if it drifted
if ! cmp -s /etc/httpd/conf/httpd.conf /etc/httpd/conf/httpd.conf.bak; then
  cp -f /etc/httpd/conf/httpd.conf.bak /etc/httpd/conf/httpd.conf
  systemctl restart httpd
fi
</code></pre>
<h3>Why the timing looked odd</h3>
<p>If the script runs hourly, why did the config revert about 20 minutes after our fix? Scripts in <code>/etc/cron.hourly/</code> are not triggered on the hour. A system cron entry (commonly <code>/etc/cron.d/0hourly</code> or <code>/etc/crontab</code>) runs <code>run-parts</code> over the directory at a fixed minute offset. On many RHEL-style systems that is a minute or so past the hour. Our fix landed roughly 40 minutes into the hourly cycle, so the next run came about 20 minutes later.</p>
<h3>Why it stayed invisible</h3>
<p>The script only acted when the live config differed from its stored baseline. For months the two files matched, so the script quietly did nothing. It stayed unnoticed until the first time anyone legitimately changed Apache, which was our CSP header work.</p>
<h3>Why Was It There? (A Blameless Perspective)</h3>
<p>Long before our team arrived, engineers had built a <strong>Golden AMI</strong> used to spin up new customer environments. It is easy to see the intent. Someone wanted a guarantee that if a developer or sysadmin accidentally corrupted an Apache configuration file, the system would self-heal within the hour and prevent extended downtime.</p>
<p>Because configuration changes were rare on this legacy product, the script sat unnoticed. It was a landmine waiting for the day we needed to change the web server rules.</p>
<hr />
<h2>Phase 4: Remediation at Scale</h2>
<p>Finding the script on one server was a victory. The real challenge was scale: <strong>the same Golden AMI artifact was baked into every one of our 250+ legacy instances, each running a customer's dedicated environment.</strong></p>
<p>I coordinated the fleet-wide remediation:</p>
<ol>
<li><strong>Stop the revert:</strong> We removed the script from <code>/etc/cron.hourly/</code> across the fleet using automated SSH and Ansible orchestration.</li>
<li><strong>Re-apply the fixes:</strong> We re-applied the tCell CSP configuration and the Apache header-passing rules.</li>
<li><strong>Verify:</strong> We confirmed that configuration drift had stopped, the audit logs were clean, and the tCell dashboard showed no unexpected CSP violations.</li>
</ol>
<hr />
<h2>Beyond <code>crontab -l</code></h2>
<p>Relying only on <code>crontab -l</code> gives a false sense of security, because it shows only the current user's crontab. When you chase recurring, unexplained behavior on a Linux host, widen the search:</p>
<ul>
<li><code>/var/spool/cron/</code>: per-user cron files on disk</li>
<li><code>/etc/crontab</code> and <code>/etc/cron.d/</code>: system-wide cron tables</li>
<li><code>/etc/cron.hourly/</code>, <code>/etc/cron.daily/</code>, <code>/etc/cron.weekly/</code>, <code>/etc/cron.monthly/</code>: scripts run on a schedule regardless of user crontabs</li>
<li><code>systemctl list-timers --all</code>: systemd timer units, which many modern distributions use instead of cron</li>
</ul>
<p>A quick one-shot check:</p>
<pre><code class="language-bash">ls -la /etc/cron.* /var/spool/cron/ 2&gt;/dev/null
grep -r "httpd" /etc/cron* /var/spool/cron/ 2&gt;/dev/null
systemctl list-timers --all
</code></pre>
<hr />
<h2>Preventing the Next Ghost</h2>
<p>An incident like this is cheap to prevent once you know what to look for:</p>
<ol>
<li><strong>Declare Apache configuration as code.</strong> Use Ansible or Chef so any local, unreviewed change to the config is visible and reversible.</li>
<li><strong>Inventory what goes into Golden AMIs.</strong> Review every script, cron job and background binary baked into a base image before promoting it, and remove this safety net from the image itself so new environments do not inherit it.</li>
<li><strong>Detect configuration drift.</strong> Run periodic scans that alert when a production config file diverges from its version-controlled baseline.</li>
<li><strong>Monitor critical directories.</strong> Use file integrity monitoring or <code>auditd</code> rules (or Falco/Twistlock) on <code>/etc/</code> and <code>/etc/cron.*</code> to alert on unexpected file creation or modification.</li>
</ol>
<hr />
<h2>Closing Thoughts</h2>
<p>Legacy systems will keep throwing curveballs. Tracking down a hidden cron script across 250 EC2 instances taught me to widen the search whenever the obvious tools come back empty, and to treat everything baked into a base image as code someone has to own.</p>
]]></content:encoded></item><item><title><![CDATA[Finding the Real Reasons Jenkins Jobs Fail: Elasticsearch, a Hybrid Classifier and a Claude Reporting Agent]]></title><description><![CDATA[Learn how we turned 3000+ chaotic Jenkins jobs into structured, actionable telemetry using Logstash, scikit-learn, and a read-only Grafana MCP server.]]></description><link>https://smahima.hashnode.dev/finding-the-real-reasons-jenkins-jobs-fail-elasticsearch-a-hybrid-classifier-and-a-claude-reporting-agent</link><guid isPermaLink="true">https://smahima.hashnode.dev/finding-the-real-reasons-jenkins-jobs-fail-elasticsearch-a-hybrid-classifier-and-a-claude-reporting-agent</guid><category><![CDATA[Devops]]></category><category><![CDATA[Jenkins]]></category><category><![CDATA[AI]]></category><category><![CDATA[elasticsearch]]></category><category><![CDATA[SRE]]></category><dc:creator><![CDATA[Mahima Singh]]></dc:creator><pubDate>Sun, 01 Nov 2026 10:30:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae6c6672c12b2614affb3e6/a857ac13-953f-4dc0-b7df-2f266ddd3df9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Nov 1, 2026 · @Mahima</p>
<p>Across 4 Jenkins instances and 3000+ jobs, we turned an unreadable pile of failures into a ranked, routable list of causes: unclassified failures fell from about 40% to 10-15%, 789 chronically failing jobs were identified, and peak-time load dropped 36%.</p>
<p><strong>TL;DR</strong></p>
<ul>
<li><strong>Problem:</strong> 4 Jenkins instances and 3000+ jobs, with no visibility into why jobs fail or which causes to fix first.</li>
<li><strong>Approach:</strong> failure analysis outside Jenkins (Logstash, Elasticsearch, Grafana), regex rules maintained through pull requests, an ML classifier for the long tail, and a weekly report written by a Claude agent through a read-only Grafana MCP server.</li>
<li><strong>Results:</strong> unclassified failures down from about 40% to 10-15%, 789 chronically failing jobs identified, and peak-time load down 36%.</li>
</ul>
<p><strong>My role.</strong> I proposed this project and built the pipeline, dashboards, classifier and reporting agent described below.</p>
<h2>The problem: failures everywhere, visibility nowhere</h2>
<p>Our CI estate is four Jenkins instances, each serving a different purpose, with more than 3000 jobs. About 600 of them run every day: maintenance, upgrades, integration tests per agent, per OS flavour and per supported version, promotions and acceptance tests.</p>
<p>The topology makes failures hard to explain. Controllers run in a data center, workers are provisioned on a GKE cluster, and test jobs request temporary VMs from vmpooler, which can land in the data center, AWS or GCP. Packages live in JFrog Artifactory, and other services such as ABS sit in the path. A single red build can come from:</p>
<ul>
<li>a network problem between any two of those pieces</li>
<li>a service being down (vmpooler, ABS, Artifactory)</li>
<li>an OS-specific issue</li>
<li>a job configuration error</li>
<li>a faulty build agent</li>
<li>memory exhaustion</li>
</ul>
<p>Nobody could say which of these caused the most failures, so triage was driven by whoever shouted loudest. Failures also cascade. A job that fails after provisioning leaves its VMs unused but still holding capacity, in some cases 300+ at a time, and automatic retries multiplied the damage. We needed to see why jobs fail before we could decide what to fix first.</p>
<h2>Starting point: the Build Failure Analyzer plugin and its limit</h2>
<p>We began with the <a href="https://plugins.jenkins.io/build-failure-analyzer/">Build Failure Analyzer</a> (BFA) plugin. You maintain a knowledge base of regex patterns, and BFA matches failed builds against it and shows the cause on the build page.</p>
<p>It worked, but it does all of its matching on the controller. That is the wrong place for it. When a major issue makes pipelines fail in bulk, every failed build triggers regex matching against dozens of patterns, adding load to a controller that is already struggling. A tool meant to diagnose an incident could make the incident worse.</p>
<p>So we set one rule: <strong>analysis happens outside Jenkins.</strong></p>
<p>BFA had a second problem. Anyone could add patterns, and over time developers piled up many overlapping regexes with no review. We wanted the knowledge base to stay clean as it grew.</p>
<h2>Architecture</h2>
<img src="https://raw.githubusercontent.com/smahima27/images/main/images/jfa_arch.png" alt="Architecture Diagram" style="display:block;margin:0 auto" />

<p>Jenkins only ships console logs. Logstash cleans them, tags known failures with regex and asks the classifier about the rest. Elasticsearch stores the result for Grafana dashboards, and a Claude agent reads the same data through Grafana's read-only MCP server to write the weekly report.</p>
<h2>Classification: regex first, ML for the long tail</h2>
<p>Regex alone has a ceiling: if no pattern matches, the failure is "unclassified", and about 40% of our failures landed there. We also wanted broader categories than individual patterns give, because different teams care about different failures:</p>
<ul>
<li>DevOps wants infrastructure failures.</li>
<li>IT wants SSL and network problems.</li>
<li>Developers want package and internal failures.</li>
<li>Job owners want to see job configuration issues.</li>
</ul>
<p><strong>Building the training data.</strong> We first wrote regex rules by hand and collected real failure logs labelled by them. We then used AI to propose failure reasons and categories for logs the rules missed, and a human reviewed these labels before any training. Low-confidence labels were filtered out so noisy examples did not teach the model the wrong thing.</p>
<p><strong>The model.</strong> A deliberately simple, CPU-friendly pipeline that can run inside a log pipeline:</p>
<ul>
<li>Logs are cleaned first: ANSI codes stripped, UUIDs and long hex strings redacted, and timestamps, paths, URLs, IPs, versions, hostnames and exit codes normalized so the model learns patterns, not one-off values.</li>
<li>Features are TF-IDF over word unigrams and bigrams (up to 5,000 features), plus binary keyword flags for known signals such as OOM, disk full, network, permissions, test failures, dependencies, containers and databases.</li>
<li>A scikit-learn Logistic Regression with balanced class weights predicts one of six categories: Infrastructure Failure, Job Failure, Memory Issue, Shell script exited with failure, Test Failure and Workspace/Env Failure.</li>
<li>Classes are capped during balancing so common failures do not drown out rare ones.</li>
</ul>
<p><strong>Serving.</strong> The model sits behind a small FastAPI service. Logstash sends each failed build's log to it and attaches the predicted category, confidence and processing time to the event before it reaches Elasticsearch. Fixed rules for unambiguous signals such as OOM, disk full and permission errors run before the model.</p>
<p><strong>Rollout.</strong> The classifier ran in a staging environment for a period of testing and verification before we promoted it to production.</p>
<h2>Making it easy for developers to contribute</h2>
<p>Asking a developer who has never used Logstash to edit a Logstash config is a nightmare, and we also wanted version control over every change. All server configuration is managed through Puppet, so we put the regex knowledge base behind a pull request flow:</p>
<ol>
<li>A developer proposes a pattern for a failure they keep seeing.</li>
<li>A GitHub Action checks whether an existing regex already covers it.</li>
<li>If it is covered, the PR is rejected as a duplicate. If not, the pattern is added.</li>
<li>Puppet rolls the updated rules out to the Logstash servers.</li>
</ol>
<p>Every rule has an author, a review and a history, and the regex sprawl that hurt us with BFA is contained by design.</p>
<h2>The AI agent: weekly reports instead of ever-longer retention</h2>
<p>Once the dashboards existed, people asked for longer retention. Keeping more raw console logs costs a lot of storage, so we looked for another way to preserve long-term insight.</p>
<p>We built an agent on Claude that produces a weekly report automatically. It highlights which failure categories rose, which Jenkins instances are healthy, and which incidents mattered most. The report goes to a Slack channel for the team and to SharePoint as a permanent reference. The weekly reports now carry the long-term trend, while detailed logs stay at 60 days.</p>
<p><strong>How the agent gets its data.</strong> The agent uses the Grafana MCP (Model Context Protocol) server, so it reads the same enriched Elasticsearch data our dashboards show, through Grafana's existing data source, instead of scraping logs.</p>
<p><strong>Read-only by design.</strong> The MCP server runs in read-only mode, so the agent can query and summarize but cannot change dashboards or data. Access uses a read-only Grafana token supplied through an environment variable, so no credentials live in code and the agent never holds Elasticsearch credentials.</p>
<p><strong>Why Grafana MCP, not Elasticsearch directly.</strong> Grafana is already the single place where our data sources meet. If we want the weekly report to include vmpooler uptime or ABS VM capacity problems, we add those as Grafana data sources and the agent can query them through the same MCP server and the same read-only token. Reports can grow with new signals without writing a new integration each time, and the capacity and outage data are exactly what we need to explain failures such as VMs being unavailable.</p>
<p><strong>Incident awareness.</strong> Failures carry metadata that links them, so a burst of hundreds of failures from one vmpooler outage can be recognized as one incident, not hundreds of separate problems.</p>
<h2>Results</h2>
<table>
<thead>
<tr>
<th>Outcome</th>
<th>Result</th>
</tr>
</thead>
<tbody><tr>
<td>Unclassified failures</td>
<td>About 40% before, 10-15% after the hybrid classifier</td>
</tr>
<tr>
<td>Chronically failing jobs found</td>
<td>789 jobs failing for at least one month, disabled until their owners fix them</td>
</tr>
<tr>
<td>Peak-time load</td>
<td>Reduced by 36% after scattering scheduled jobs</td>
</tr>
<tr>
<td>Top failure causes</td>
<td>1. Test failures. 2. Installer and pre-suite setup failures after new product versions. 3. VMs unavailable because of capacity limits</td>
</tr>
</tbody></table>
<p>Test failures leading the list is expected, since that is what CI exists to catch. The other two are more actionable: setup failures cluster around new product versions, and capacity shortages point to a platform limit, not a bug in any job.</p>
<p><strong>Scheduling.</strong> The data showed many teams' scheduled jobs requesting VMs at the same moment. Spreading them across time slots cut peak-time load by 36%.</p>
<p><strong>What it did not fix.</strong> Many jobs are long-running, so failure rates stayed a problem and wasted VMs were reduced, not eliminated. What we gained was knowing which jobs fail continuously while also consuming VMs, which let us stop the biggest repeat offenders.</p>
<h2>Lessons and what is next</h2>
<p>The system gave us visibility, and it also showed where it can go further:</p>
<ul>
<li><strong>Rank causes by cost, not count.</strong> Ingest vmpooler data so failures can be ranked by VM-hours wasted, which turns "300+ wasted VMs" into a measured metric.</li>
<li><strong>Report incidents and rates.</strong> Lead the weekly report with grouped incidents, and compare failure rates per instance instead of raw counts, since more builds can mean more failures without anything getting worse.</li>
<li><strong>Keep weekly aggregates as data.</strong> Store each week's rollup in a small structured index so quarter-over-quarter questions need no raw logs.</li>
<li><strong>Make uncertainty explicit.</strong> Add a calibrated confidence threshold so low-confidence predictions are reported as "uncertain" and routed to review, and use those logs to grow the regex rules and training set.</li>
<li><strong>Evaluate harder.</strong> Hold out a human-verified set, split train and test by job or time so repeat failures from one incident do not leak across, and monitor class share and confidence for drift.</li>
<li><strong>Stop waste at the source.</strong> Pre-flight health checks on vmpooler and Artifactory before provisioning, retry caps with a circuit breaker, and guaranteed VM release in post-build steps with a short lease TTL as a backstop.</li>
<li><strong>Harden the pipeline.</strong> Persistent queues in Logstash and an alert when an instance stops sending logs, so silence is never mistaken for success.</li>
</ul>
<h2>Closing thoughts</h2>
<p>The biggest win was not any single model or tool. It was being able to see failures by cause. Once we could, the next steps were practical: spread out scheduled jobs that competed for VMs, stop chronically failing jobs from consuming capacity, and point each failure at the team that can fix it.</p>
<p>Three choices carried the project: keep analysis off the system that is already failing, combine regex precision with ML coverage behind a reviewable pull request flow, and let a weekly report carry long-term trends instead of storing ever more raw logs.</p>
<p>If you run CI at scale and have tackled failure analysis a different way, I would like to hear about it in the comments.</p>
<h2>Tech stack</h2>
<p>Jenkins (4 instances, GKE agents) · Logstash with the Jenkins Logstash plugin and Grok · Elasticsearch (daily indices, 60-day retention) · Grafana Cloud with Private Data Source Connect · Python, scikit-learn and FastAPI · Claude with the Grafana MCP server (read-only) · Puppet and GitHub Actions · Slack and SharePoint · vmpooler, Artifactory</p>
]]></content:encoded></item></channel></rss>