<?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: Oleksandr</title>
    <description>The latest articles on DEV Community by Oleksandr (@arsenalzp).</description>
    <link>https://dev.to/arsenalzp</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%2F4192380%2F85be4348-9bfa-4085-abfb-c101b8e704c2.jpg</url>
      <title>DEV Community: Oleksandr</title>
      <link>https://dev.to/arsenalzp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arsenalzp"/>
    <language>en</language>
    <item>
      <title>Why did Reconcile() run again? Easy event tracing in controller-runtime</title>
      <dc:creator>Oleksandr</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:16:41 +0000</pubDate>
      <link>https://dev.to/arsenalzp/why-did-reconcile-run-again-easy-event-tracing-in-controller-runtime-33gb</link>
      <guid>https://dev.to/arsenalzp/why-did-reconcile-run-again-easy-event-tracing-in-controller-runtime-33gb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhjv9ean8n8bcego72cmx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhjv9ean8n8bcego72cmx.png" alt="Gopher explores Kubernetes events"&gt;&lt;/a&gt;&lt;br&gt;
If you build Kubernetes operators in Go using &lt;a href="https://github.com/kubernetes-sigs/controller-runtime" rel="noopener noreferrer"&gt;&lt;code&gt;controller-runtime&lt;/code&gt;&lt;/a&gt; (Kubebuilder or Operator SDK), you probably know this pain.&lt;/p&gt;

&lt;p&gt;You update one small field in your Custom Resource, look at the logs, and see that &lt;code&gt;Reconcile()&lt;/code&gt; was called &lt;strong&gt;4 or 5 times in a row&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Why? Was it your Spec update? A Status update from a child Deployment? An external ConfigMap sync? Or just a standard requeue?&lt;/p&gt;

&lt;p&gt;By default, &lt;code&gt;controller-runtime&lt;/code&gt; gives you a very simple method signature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MyReconciler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Reconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at &lt;code&gt;ctrl.Request&lt;/code&gt; - it contains only &lt;code&gt;Namespace&lt;/code&gt; and &lt;code&gt;Name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The actual Kubernetes events that triggered this execution are lost inside the internal &lt;code&gt;workqueue&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does this happen? (Workqueue Deduplication)
&lt;/h3&gt;

&lt;p&gt;To protect the Kubernetes API Server from too many requests, &lt;code&gt;controller-runtime&lt;/code&gt; uses a deduplicating &lt;code&gt;workqueue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If 5 events happen almost at the same time for &lt;code&gt;default/my-cr&lt;/code&gt; (for example, Spec change + Deployment status update + Pod status change), the &lt;code&gt;workqueue&lt;/code&gt; compresses them &lt;strong&gt;into a single &lt;code&gt;Reconcile()&lt;/code&gt; call&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is great for cluster performance, but it makes debugging really hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't see which resource actually triggered the call.&lt;/li&gt;
&lt;li&gt;You don't see what changed (Spec, Status, or Metadata).&lt;/li&gt;
&lt;li&gt;It's hard to trace child resources (Deployments, Services) back to the main CR loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My solution: whyreconcile
&lt;/h3&gt;

&lt;p&gt;To solve this problem for my own projects, I built &lt;strong&gt;&lt;code&gt;whyreconcile&lt;/code&gt;&lt;/strong&gt; - a simple, lightweight Go package for &lt;code&gt;controller-runtime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It doesn't change your business logic. Instead, it hooks into event handlers, compares old and new object states (shallow diff), saves the cause metadata, and prints a clear summary right before your &lt;code&gt;Reconcile()&lt;/code&gt; starts.&lt;/p&gt;

&lt;p&gt;How it looks in your logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reconcile causes controller=my-controller target=default/my-cr causes=3 summary=PrimaryResourceSpecUpdate:1, SecondaryResourceStatusOrMetadata:2 sources=2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In just one log line, you immediately see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target&lt;/strong&gt;: Which resource is being reconciled (&lt;code&gt;default/my-cr&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Causes count&lt;/strong&gt;: 3 events happened since the last run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summary&lt;/strong&gt;: 1 Spec update on the Primary CR and 2 Status/Metadata updates on secondary resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How it works under the hood
&lt;/h3&gt;

&lt;p&gt;The idea is very straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Catch events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For Primary resources (&lt;code&gt;For&lt;/code&gt;), it intercepts events using predicates and shallow diffs.&lt;/li&gt;
&lt;li&gt;For Secondary/External resources (&lt;code&gt;Watches&lt;/code&gt;), it uses a custom &lt;code&gt;TypedEventHandler&lt;/code&gt; to map child objects (like Deployments) to the parent CR.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Store causes: As events flow in, whyreconcile saves cause info in a thread-safe in-memory store under the target resource key (&lt;code&gt;Namespace/Name&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Print before Reconcile: When &lt;code&gt;controller-runtime&lt;/code&gt; takes a request from the &lt;code&gt;workqueue&lt;/code&gt;, &lt;code&gt;whyreconcile&lt;/code&gt; extracts all stored causes for this key and prints them before calling your original &lt;code&gt;Reconcile()&lt;/code&gt; code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Quick Setup (takes 2 minutes)
&lt;/h3&gt;

&lt;p&gt;You don't need to change anything inside your &lt;code&gt;Reconcile()&lt;/code&gt; function. Just update &lt;code&gt;SetupWithManager&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get https://github.com/arsenalzp/whyreconcile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Wrap your setup code&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"https://github.com/arsenalzp/whyreconcile"&lt;/span&gt;
    &lt;span class="n"&gt;appsv1&lt;/span&gt; &lt;span class="s"&gt;"k8s.io/api/apps/v1"&lt;/span&gt;
    &lt;span class="n"&gt;ctrl&lt;/span&gt; &lt;span class="s"&gt;"sigs.k8s.io/controller-runtime"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MyReconciler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;SetupWithManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mgr&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Create analyzer&lt;/span&gt;
    &lt;span class="n"&gt;analyzer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;whyreconcile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewAnalyzer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewControllerManagedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="c"&gt;// 2. Wrap Primary resource&lt;/span&gt;
        &lt;span class="n"&gt;For&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MyCR&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;analyzer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPrimaryOpts&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="c"&gt;// 3. Wrap Secondary resource (e.g., child Deployment)&lt;/span&gt;
        &lt;span class="n"&gt;Watches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;appsv1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
            &lt;span class="n"&gt;analyzer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewSecondaryHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"child-deployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnqueueRequestForOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetScheme&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRESTMapper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;v1alpha1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MyCR&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
        &lt;span class="c"&gt;// 4. Wrap your Reconciler&lt;/span&gt;
        &lt;span class="n"&gt;Complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analyzer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WrapReconcile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! Next time you run your operator locally or in a dev cluster, you will see exactly why each &lt;code&gt;Reconcile()&lt;/code&gt; was triggered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it out&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is open-source and free to use:&lt;/p&gt;

&lt;p&gt;📁 GitHub: &lt;a href="https://github.com/arsenalzp/whyreconcile" rel="noopener noreferrer"&gt;github.com/arsenalzp/whyreconcile&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you write Kubernetes operators, give it a try in your dev environment! I'd love to hear your feedback or ideas in the comments.&lt;/p&gt;

</description>
      <category>go</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
