<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohanad Abu-Nayla</title>
    <description>The latest articles on DEV Community by Mohanad Abu-Nayla (@hannody).</description>
    <link>https://dev.to/hannody</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F725203%2F18ac32ab-8f50-4575-947c-ba86505f68e6.png</url>
      <title>DEV Community: Mohanad Abu-Nayla</title>
      <link>https://dev.to/hannody</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hannody"/>
    <language>en</language>
    <item>
      <title>Spec-driven development with AI agents: constitutions, checkpoints, and handoffs</title>
      <dc:creator>Mohanad Abu-Nayla</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:09:19 +0000</pubDate>
      <link>https://dev.to/hannody/spec-driven-development-with-ai-agents-constitutions-checkpoints-and-handoffs-2o8l</link>
      <guid>https://dev.to/hannody/spec-driven-development-with-ai-agents-constitutions-checkpoints-and-handoffs-2o8l</guid>
      <description>&lt;p&gt;AI coding agents are strangely lopsided. Hand one a well-scoped task and it writes better code, faster, than I do. Hand it a &lt;em&gt;large&lt;/em&gt; feature, the kind that takes days and touches a dozen files across several layers, and the failure mode changes. The context window fills up and quality quietly degrades (AKA &lt;strong&gt;context rot&lt;/strong&gt;). A fresh session, missing the history, confidently invents an API that never existed. And even when the code is good, you're left staring at a 3,000-line diff you can't meaningfully review.&lt;/p&gt;

&lt;p&gt;Those failures are not solved by the model alone. They are largely a &lt;em&gt;process&lt;/em&gt; problem.&lt;/p&gt;

&lt;p&gt;The backbone I work inside isn't mine and isn't novel: a constitution, specs, and deliberately stabilized plans. It's spec-driven development, which has been around in various shapes for years. What I added from running it day to day were two operational mechanisms that made it survive long features: explicit &lt;strong&gt;checkpoints&lt;/strong&gt; and structured &lt;strong&gt;handoffs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The mental model I use: &lt;strong&gt;treat the agent like a brilliant but forgetful senior engineer.&lt;/strong&gt; Sharp in the moment, no long-term memory, occasionally overconfident. You wouldn't hand that person a two-week feature and walk away. You'd give them the ground rules, a spec, and a plan, then review their work in slices. That's the whole idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop: constitution → spec → plan → implement
&lt;/h2&gt;

&lt;p&gt;Standard stuff, so I'll keep it short.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constitution.&lt;/strong&gt; A short, binding document of principles the project may never violate: dependency direction, the testing bar, security rules, "spec before code." It's the highest authority; when a request conflicts with it, the conflict gets &lt;em&gt;surfaced&lt;/em&gt;, not silently coded around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec.&lt;/strong&gt; WHAT and WHY only. User value, functional requirements, acceptance criteria. No tech, no code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan.&lt;/strong&gt; HOW. Architecture, contracts, and an ordered list of tiny, test-first tasks, each justified against the constitution. Crucially, the plan is &lt;strong&gt;frozen&lt;/strong&gt; once approved: it's the source of truth, and the agent doesn't get to quietly rewrite it mid-flight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives the agent rails. But rails aren't enough for long work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checkpoints: break the feature into slices you can actually review
&lt;/h2&gt;

&lt;p&gt;A plan might have 25 tasks. Executing all 25 in one shot lands you right back at the unreviewable-diff problem. So I slice the plan into &lt;strong&gt;checkpoints&lt;/strong&gt;, review-sized groups of tasks, and the agent works one checkpoint at a time, then &lt;strong&gt;stops&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At each stop it has to: get the gate green (lint, types, architecture checks, tests), tick off the tasks, write a short note recording &lt;em&gt;every deviation&lt;/em&gt; from the plan, and hand off for review.&lt;/p&gt;

&lt;p&gt;The review itself is two-stage. The coding agent doesn't send its work straight to me. It writes a brief for a separate &lt;strong&gt;reviewer agent&lt;/strong&gt;, stating what changed and why. I read that brief first, because what the coder chooses to explain (and what it leaves out) tells me where to look. Then the reviewer agent goes to work on the diff, and I read its findings against my own. The slice is a few hundred lines, one coherent unit, so this is actually tractable. Approval is mine either way; nothing continues on an agent's say-so.&lt;/p&gt;

&lt;p&gt;Two things fall out of this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complexity stays bounded.&lt;/strong&gt; I'm never reviewing "a feature"; I'm reviewing a checkpoint. Drift and bad assumptions get caught at checkpoint 2, not discovered at checkpoint 7.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification is real.&lt;/strong&gt; Because each checkpoint ends on a green gate &lt;em&gt;with evidence&lt;/em&gt;, "done" means something. I run the commands and read the output before I believe it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The idea of pausing between steps isn't new. What I've made repeatable is the artifact: a shared checkpoint tracker that both the human and the agent write to, so stopping-for-review is the default, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handoffs: kill context rot and hallucination
&lt;/h2&gt;

&lt;p&gt;Long features outlive a single agent session. The context window fills, or I come back the next day, or I switch models. A fresh session starts with &lt;em&gt;zero&lt;/em&gt; memory of the last three days of decisions. Left alone, it will re-litigate settled choices or hallucinate the current state.&lt;/p&gt;

&lt;p&gt;I learned this the tedious way. Fresh sessions reintroduced an API we'd deleted two days earlier, reopened architectural decisions that were already settled, quietly dropped an invariant because the reasoning behind it had scrolled out of the window. After the third time reviewing the same decision, I stopped waiting for it to happen.&lt;/p&gt;

&lt;p&gt;So I don't wait for a session to degrade. I retire it on purpose and have it hand the work to a fresh agent on my terms.&lt;/p&gt;

&lt;p&gt;Every feature carries a &lt;strong&gt;HANDOFF&lt;/strong&gt; document, a living page that tells the next session exactly what it needs and nothing it doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orientation: which docs to read, in order (constitution, spec, the frozen plan, the checkpoint tracker).&lt;/li&gt;
&lt;li&gt;The hard rules it must not break.&lt;/li&gt;
&lt;li&gt;Current state: branch, commit, which checkpoints are done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decisions already made, with the *why&lt;/strong&gt;*, so they don't get reopened.&lt;/li&gt;
&lt;li&gt;The next task, and the items consciously deferred.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fresh agent reads the handoff and the frozen plan, and it's grounded again. No rot, no invention, because the ground truth lives in the documents rather than in a context window that's about to overflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Six sessions, one document
&lt;/h3&gt;

&lt;p&gt;On a recent AI-system backend feature, six different agent sessions touched the code across its seven checkpoints. Each started from a fresh context, some were different models, and none of them needed me to re-explain a decision that had already been made. I read every handoff as it was written, and tightened the template whenever a fresh session asked a question the document should have already answered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tiering models by leverage
&lt;/h2&gt;

&lt;p&gt;One more lever: not every step deserves the same horsepower. Planning and code review are the highest-leverage, hardest-to-reverse steps, and a bad plan poisons everything downstream. Implementation against a &lt;em&gt;frozen, detailed&lt;/em&gt; plan is comparatively mechanical.&lt;/p&gt;

&lt;p&gt;So I tier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Top reasoning tier for plans and the reviewer agent&lt;/strong&gt; (currently Fable 5), where judgment compounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tier down for the actual coding&lt;/strong&gt; (Opus 5, or Sonnet 5 on the mechanical stretches), where the plan and the checkpoint gate are already the guardrails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human review at each checkpoint is the safety net that makes the cheaper coding pass safe. It's a nice cost/quality trade: spend where thinking matters, save where the rails are already in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing, on five lines
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request → Constitution → Spec → Frozen Plan
    │
    └─&amp;gt; ( Checkpoint → Gate → Review ) × N
              ↑                    │
              └───── HANDOFF ◀─────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it buys me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reviewable diffs.&lt;/strong&gt; Always a checkpoint, never a monolith.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grounded agents.&lt;/strong&gt; The handoff plus the frozen plan beat context rot and hallucination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real verification.&lt;/strong&gt; A green gate and evidence at every stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost control.&lt;/strong&gt; Heavy models where they earn it, a tier down where the rails already hold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuity.&lt;/strong&gt; The feature survives any single session ending.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The surprising lesson: as agents get better at &lt;em&gt;writing code&lt;/em&gt;, the bottleneck moves to &lt;strong&gt;keeping them grounded and keeping yourself in the loop&lt;/strong&gt;. A constitution, checkpoints, and handoffs are how I do both, and honestly, they've made me a more disciplined engineer even on the parts I write by hand.&lt;/p&gt;

&lt;p&gt;I've templated the whole thing (constitution, spec/plan templates, the checkpoint tracker, and the handoff doc) as a starter kit I drop into new projects: &lt;strong&gt;&lt;a href="https://github.com/hannody/spec-kit" rel="noopener noreferrer"&gt;hannody/spec-kit&lt;/a&gt;&lt;/strong&gt;. It's language- and domain-agnostic; fork it and tune the constitution to your stack.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>On-Demand Namespace Error Log Watcher in a Locked-Down Kubernetes Environment</title>
      <dc:creator>Mohanad Abu-Nayla</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:03:37 +0000</pubDate>
      <link>https://dev.to/hannody/on-demand-namespace-error-log-watcher-in-a-locked-down-kubernetes-environment-5fji</link>
      <guid>https://dev.to/hannody/on-demand-namespace-error-log-watcher-in-a-locked-down-kubernetes-environment-5fji</guid>
      <description>&lt;p&gt;During a large-scale stress test, the last thing you want is to be hunting errors across 150 pods manually.&lt;/p&gt;

&lt;p&gt;So I built an on-demand error log watcher for a complex Kubernetes namespace under real-world constraints, without touching anything outside the namespace, and without installing a single binary on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we are building:&lt;/strong&gt; a Kubernetes Deployment, scaled to zero by default, that runs &lt;a href="https://github.com/stern/stern" rel="noopener noreferrer"&gt;Stern&lt;/a&gt; as a container inside the cluster. It watches all pods in a target namespace, filters output to errors only, and can be started or stopped with a single &lt;code&gt;kubectl scale&lt;/code&gt; command. The entire setup RBAC, ServiceAccount, and Deployment lives within the targeted namespace it monitors and leaves no trace when removed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Environment
&lt;/h2&gt;

&lt;p&gt;The namespace in question that I was stress-testing, &lt;code&gt;ironlight-ai&lt;/code&gt;, runs on a private GPU cluster. The infrastructure is completely private cloud environment, Node-level access is restricted, and installing tools server-wide is not permitted — and frankly, not something I would do even if I could. Any solution I introduce must be contained, reversible, and have zero footprint beyond its own namespace.&lt;/p&gt;

&lt;p&gt;The namespace itself is not small. It runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On-Prem LLM inference services&lt;/li&gt;
&lt;li&gt;Vision-Language Model (VLM) pipelines&lt;/li&gt;
&lt;li&gt;Classical ML classification workers&lt;/li&gt;
&lt;li&gt;Deep learning document processors&lt;/li&gt;
&lt;li&gt;OCR pipelines with pre- and post-processing stages&lt;/li&gt;
&lt;li&gt;Backend orchestration services&lt;/li&gt;
&lt;li&gt;File conversion, image processing, and rule evaluation workers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At peak stress test load, this namespace runs around &lt;strong&gt;150 pod replicas&lt;/strong&gt;. The goal of the stress tests is to burst large numbers of concurrent users through the full pipeline to expose weak points in the software stack and infrastructure before they become production incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Observability Gap
&lt;/h2&gt;

&lt;p&gt;I had already set up a full Grafana observability stack for this environment: Alloy for log collection, Prometheus for metrics, and Loki for log aggregation, all configured and functional. However, during high volume stress tests, log visibility for this namespace became inconsistent, but functional for other namespaces and services. Rather than interrupt the test to debug the collection pipeline, I needed a solution I could reach for immediately; something that would give me error visibility in minutes, not hours.&lt;/p&gt;

&lt;p&gt;The observability stack will be revisited and fixed properly. Since the stress-test uncovered this gap in it. However, this stern tool exists for the time between now and then and as a backup for future incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Install Stern the Normal Way
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/stern/stern" rel="noopener noreferrer"&gt;Stern&lt;/a&gt; is the obvious tool for this. It tails logs across multiple pods and containers simultaneously, supports regex filtering, and produces clean, readable output.&lt;/p&gt;

&lt;p&gt;The challenge is getting it running in this environment.&lt;/p&gt;

&lt;p&gt;I tried the straightforward approaches first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local install with SSH tunnel:&lt;/strong&gt; Installed Stern on my local machine and attempted to tunnel access through SSH and VPN to reach the cluster API. It did not work. The network topology between my machine and the cluster API server under VPN made this unreliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python helper script:&lt;/strong&gt; I wrote a helper to establish and maintain the connection programmatically. It did not work reliably either. At this point I had spent more time on the tooling problem than on the actual debugging I needed to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Following the official Kubernetes and GitHub documentation:&lt;/strong&gt; I checked the standard installation paths. They assumed a level of access to the nodes and the cluster that I simply do not have them readily available in this environment.&lt;/p&gt;

&lt;p&gt;I also considered the Kubernetes native alternatives before settling on this approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;kubectl logs&lt;/code&gt; with label selectors:&lt;/strong&gt; Works well for a handful of pods. At 150 replicas it becomes unwieldy, and it offers no log filtering — every line from every container comes through regardless of severity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel &lt;code&gt;kubectl logs&lt;/code&gt; with &lt;code&gt;xargs&lt;/code&gt;:&lt;/strong&gt; A common pattern for watching many pods at once looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; ironlight-ai &lt;span class="nt"&gt;-o&lt;/span&gt; name &lt;span class="se"&gt;\&lt;/span&gt;
  | xargs &lt;span class="nt"&gt;-n1&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; 10 kubectl logs &lt;span class="nt"&gt;-n&lt;/span&gt; ironlight-ai &lt;span class="nt"&gt;--all-containers&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100 &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clever, but it has the same problems as the label selector approach: no filtering, pure log volume from 150 replicas. It also batches with &lt;code&gt;-P 10&lt;/code&gt;, so at 150 pods you are waiting for processes to cycle rather than watching the full namespace simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ephemeral debug pod:&lt;/strong&gt; A &lt;code&gt;kubectl debug&lt;/code&gt; or temporary utility pod could run tools interactively, I have tried this approach, for another scenarios, e.g. use 'curl' commands for connectivity checks as most of my microservices images are minimal images optimized for production with no extra tooling, but it still requires either installing Stern inside the pod at runtime or building a custom image — both of which reintroduce the installation problem in a different form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix the Loki ingestion issue first:&lt;/strong&gt; The correct long-term answer. Not the right call during an active stress test.&lt;/p&gt;

&lt;p&gt;I stopped pursuing these paths when I recognised a pattern: every approach either required access I did not have, depended on local connectivity I could not trust, or introduced more complexity than the problem warranted.&lt;/p&gt;

&lt;p&gt;That is the wrong direction. The solution should make my day easier, not more complicated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Design Criteria
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of YAML, I defined what an acceptable solution looked like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Namespace-contained.&lt;/strong&gt; No resources, no side effects, nothing outside &lt;code&gt;ironlight-ai&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No server-wide installation.&lt;/strong&gt; Nothing touches the nodes or the cluster outside my namespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-demand only.&lt;/strong&gt; It should run when I need it and consume zero cluster resources when I do not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal RBAC surface.&lt;/strong&gt; Only the permissions strictly necessary — nothing broader.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable output.&lt;/strong&gt; During a stress test I need to see errors fast. The output must be clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reversible.&lt;/strong&gt; I can remove the entire thing with one command and leave no trace.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The solution that met all of these criteria: &lt;strong&gt;run Stern as a container inside the cluster itself&lt;/strong&gt;, deployed as a Kubernetes Deployment scaled to zero by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a Deployment Scaled to Zero
&lt;/h2&gt;

&lt;p&gt;A standalone Pod would work, but it has no scaling controller. Once created it runs until deleted.&lt;/p&gt;

&lt;p&gt;A Deployment with &lt;code&gt;replicas: 0&lt;/code&gt; solves this cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The configuration lives in the cluster permanently&lt;/li&gt;
&lt;li&gt;No pod runs, no log streams are open, no API calls are made&lt;/li&gt;
&lt;li&gt;When I need it, I scale to 1 with a single command&lt;/li&gt;
&lt;li&gt;When I am done, I scale back to 0&lt;/li&gt;
&lt;li&gt;The Deployment itself stays, ready for next time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the operational model I wanted: &lt;strong&gt;always available, never running unless explicitly started.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  RBAC Design
&lt;/h2&gt;

&lt;p&gt;Because Stern runs as a pod inside the cluster, it needs permission to read pod logs. I created a &lt;code&gt;ServiceAccount&lt;/code&gt;, &lt;code&gt;Role&lt;/code&gt;, and &lt;code&gt;RoleBinding&lt;/code&gt; scoped exclusively to the &lt;code&gt;ironlight-ai&lt;/code&gt; namespace.&lt;/p&gt;

&lt;p&gt;Note the use of &lt;code&gt;Role&lt;/code&gt;, not &lt;code&gt;ClusterRole&lt;/code&gt;. A &lt;code&gt;ClusterRole&lt;/code&gt; would grant permissions across the entire cluster. A namespace-scoped &lt;code&gt;Role&lt;/code&gt; limits access to only the namespace it is defined in which is exactly the boundary I wanted.&lt;/p&gt;

&lt;p&gt;This also means &lt;strong&gt;the entire setup must be deployed into the same namespace you want to monitor&lt;/strong&gt;. The &lt;code&gt;ServiceAccount&lt;/code&gt;, &lt;code&gt;Role&lt;/code&gt;, &lt;code&gt;RoleBinding&lt;/code&gt;, and &lt;code&gt;Deployment&lt;/code&gt; all carry &lt;code&gt;namespace: ironlight-ai&lt;/code&gt;. If you are adapting this for your own use, replace every occurrence of &lt;code&gt;ironlight-ai&lt;/code&gt; with your target namespace. Stern will then watch only that namespace, with permissions that cannot reach anything outside it.&lt;/p&gt;

&lt;p&gt;The Role grants only what Stern needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pods&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pods/log&lt;/span&gt;

&lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;get&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;watch&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing else. No cluster-wide permissions. No write access.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling logging for 150 Replicas
&lt;/h2&gt;

&lt;p&gt;Stern has a default limit of 50 concurrent log streams. With 150 pod replicas in &lt;code&gt;ironlight-ai&lt;/code&gt;, it exits immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: stern reached the maximum number of log requests (50), use --max-log-requests to increase the limit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--max-log-requests&lt;/span&gt; 200 &lt;span class="c"&gt;# HALT! Higher numbers could risk throttling or stressing the Kubernetes API server.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives enough headroom for the full namespace at peak replica count.&lt;/p&gt;




&lt;h2&gt;
  
  
  Filtering for What Matters
&lt;/h2&gt;

&lt;p&gt;During a stress test, I do not want to see every log line from 150 pods. I want errors, and only errors.&lt;/p&gt;

&lt;p&gt;Different services in this namespace log at different severity levels and with different casing conventions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR / Error / error
CRITICAL / Critical / critical
FATAL / Fatal / fatal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A case-insensitive regex covers all of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(?i)error|critical|fatal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also exclude the Stern pod itself from its own log watch to avoid a feedback loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--exclude-pod&lt;/span&gt; stern.&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I use &lt;code&gt;--tail 0&lt;/code&gt; so Stern does not replay historical logs on startup — only new lines from the moment it starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Output Template
&lt;/h2&gt;

&lt;p&gt;The default Stern output includes full pod names and container metadata. That is useful in some contexts, but during a stress test it is noise due to replicas.&lt;/p&gt;

&lt;p&gt;I used a minimal Go template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{printf "%s %s\n" .ContainerName .Message}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;backend 2026-05-22 13:23:25,094 ERROR streams.document.views 1619 ...
ocr 2026-05-22 13:23:31,201 CRITICAL pipeline.processor 88 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Service name, timestamp, severity, location, message. Everything I need, nothing I do not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full YAML 🫡
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ironlight-ai&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Role&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ironlight-ai&lt;/span&gt;
&lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pods"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pods/log"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;watch"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RoleBinding&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ironlight-ai&lt;/span&gt;
&lt;span class="na"&gt;subjects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
&lt;span class="na"&gt;roleRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Role&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
  &lt;span class="na"&gt;apiGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rbac.authorization.k8s.io&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ironlight-ai&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stern&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/stern/stern:v1.34.0&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-n"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ironlight-ai"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--include"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(?i)error|critical|fatal"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--exclude-pod"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stern.*"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--tail"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--max-log-requests"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--only-log-lines"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--template"&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{{printf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"%s&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;%s\n"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.ContainerName&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.Message}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Applying and Using It
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before applying:&lt;/strong&gt; make sure every &lt;code&gt;namespace:&lt;/code&gt; field in the YAML matches the namespace you want to monitor. All resources, the ServiceAccount, Role, RoleBinding, and Deployment must live in the same namespace as the pods you are watching. The image &lt;code&gt;ghcr.io/stern/stern:v1.34.0&lt;/code&gt; is publicly available; this solution was tested against this version at time of writing. In a restricted deployment environment, substitute your internal registry path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Apply once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; stern-error-watcher.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start watching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment &lt;span class="nt"&gt;-n&lt;/span&gt; ironlight-ai stern &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
kubectl logs &lt;span class="nt"&gt;-n&lt;/span&gt; ironlight-ai &lt;span class="nt"&gt;-f&lt;/span&gt; deployment/stern
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment &lt;span class="nt"&gt;-n&lt;/span&gt; ironlight-ai stern &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deployment &lt;span class="nt"&gt;-n&lt;/span&gt; ironlight-ai stern
kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; ironlight-ai &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;stern
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What This Is and What It Is Not
&lt;/h2&gt;

&lt;p&gt;This is not a replacement for a properly functioning Grafana/Loki observability stack. That stack exists, it covers the full environment, and it will be fixed properly.&lt;/p&gt;

&lt;p&gt;This is a deliberate tool built for a specific scenario: active stress testing, high replica counts, and a need for immediate error visibility scoped to one namespace.&lt;/p&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entirely self-contained within the namespace&lt;/li&gt;
&lt;li&gt;Zero-cost when not running&lt;/li&gt;
&lt;li&gt;Removable without side effects&lt;/li&gt;
&lt;li&gt;Scoped to the minimum necessary RBAC permissions&lt;/li&gt;
&lt;li&gt;Fast to start and fast to stop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a constrained environment, the right solution is not always the one the documentation recommends. Sometimes it is the one that fits cleanly inside the boundaries you are given 🙄 and leaves no mess behind.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/blog/2016/10/tail-kubernetes-with-stern/" rel="noopener noreferrer"&gt;Tail Kubernetes with Stern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/stern/stern" rel="noopener noreferrer"&gt;Stern — Kubernetes log tailing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/concepts/security/service-accounts/" rel="noopener noreferrer"&gt;Kubernetes ServiceAccounts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/" rel="noopener noreferrer"&gt;Kubernetes RBAC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kubernetes.io/docs/tasks/run-application/scale-deployment/" rel="noopener noreferrer"&gt;Kubernetes Deployment Scaling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>cloudnative</category>
      <category>backend</category>
      <category>gpu</category>
    </item>
  </channel>
</rss>
