<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Akeem Amusat</title>
        <link>https://a4m.dev</link>
        <description>Your blog description</description>
        <lastBuildDate>Sat, 29 Aug 2026 14:34:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Akeem Amusat</title>
            <url>https://a4m.dev/favicon.ico</url>
            <link>https://a4m.dev</link>
        </image>
        <copyright>All rights reserved 2026</copyright>
        <item>
            <title><![CDATA[Detecting APT Lateral Movement in Kubernetes with Lightweight ML]]></title>
            <link>https://a4m.dev/articles/detecting-apt-lateral-movement-in-kubernetes-with-lightweight-ml</link>
            <guid>https://a4m.dev/articles/detecting-apt-lateral-movement-in-kubernetes-with-lightweight-ml</guid>
            <pubDate>Sat, 29 Aug 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[My MSc research: building a fintech microservices testbed, emulating APT lateral movement, and training lightweight ML models on network-flow features to detect an attacker moving between pods — evaluated against a signature-based IDS baseline.]]></description>
            <content:encoded><![CDATA[<p>This is a write-up of my MSc research project, <em>Detecting APT Lateral Movement in Kubernetes-Hosted Microservices Using Lightweight Machine Learning on Network Traffic</em>. It is longer and more technical than my usual posts, because the point is not a single trick — it is an end-to-end methodology: a realistic testbed, an emulated adversary, a labelled dataset built from first principles, two trained models, and an honest comparison against the tool this is supposed to improve on.</p>
<p>I have organised it around four things: the <strong>problem</strong>, the <strong>system architecture</strong> I built to study it, the <strong>machine-learning pipeline</strong>, and the <strong>findings</strong> — including the parts that should make you cautious.</p>
<h2>The problem</h2>
<p>Advanced Persistent Threats (APTs) are defined less by how they get in than by what they do once inside: they establish a foothold, then move <em>laterally</em> — from the initially compromised workload toward higher-value targets — often over weeks, deliberately blending into normal activity.</p>
<p>Kubernetes makes that lateral phase unusually comfortable for an attacker. The default network posture is allow-all: any pod can reach any other pod&#x27;s service. Workloads authenticate <em>users</em> at the edge but frequently trust each other implicitly inside the cluster. So once an adversary controls one pod — via a vulnerable dependency, a leaked token, an over-permissive service account — their next moves look like ordinary east-west traffic. Reusing a valid JWT. Enumerating service ports over cluster DNS. Reading a ConfigMap with the pod&#x27;s own credentials. There is no malicious payload for a scanner to match; the bytes are legitimate.</p>
<p>Signature-based intrusion detection is structurally poorly suited to this. A signature can only match a known-bad pattern, and there is no CVE-shaped payload here — only <em>behaviour</em> that is anomalous in context. That framing produced three research questions:</p>
<ul>
<li><strong>RQ1</strong> — Can lightweight machine learning, using only statistical features of network flows (no payload inspection), detect APT lateral movement between microservices in a Kubernetes cluster?</li>
<li><strong>RQ2</strong> — How do two candidate classifiers — Random Forest and XGBoost — compare on detection quality <em>and</em> deployment cost (latency, memory, model size)?</li>
<li><strong>RQ3</strong> — How does the ML approach compare against a production signature-based IDS (Suricata) run over the same traffic?</li>
</ul>
<p>&quot;Lightweight&quot; is a first-class constraint, not an afterthought. The detector has to be cheap enough to run <em>beside</em> the workload it protects, on the same constrained infrastructure — so model size, inference latency, and memory footprint are evaluation criteria, not footnotes.</p>
<h2>The system architecture</h2>
<p>There is no public dataset of APT lateral movement inside a fintech Kubernetes cluster, so the first contribution is the testbed that produces one. It has three layers: the application, the service mesh, and the traffic generators (benign and adversarial).</p>
<h3>The application: eight microservices</h3>
<p>The workload is a deliberately simple but realistic fintech backend — eight services in a <code>fintech</code> namespace on a single-node minikube cluster. Five are FastAPI (Python), three are Go using the standard-library <code>net/http</code>. State is in-memory except the audit sink, which uses SQLite.</p>
<table><thead><tr><th>#</th><th>Service</th><th>Lang</th><th>Port</th><th>Role</th></tr></thead><tbody><tr><td>1</td><td>auth-service</td><td>FastAPI</td><td>8001</td><td>Login, JWT issuance, token introspection</td></tr><tr><td>2</td><td>user-service</td><td>FastAPI</td><td>8002</td><td>User profiles + credential verification</td></tr><tr><td>3</td><td>payment-service</td><td>Go</td><td>8003</td><td>Payment orchestration</td></tr><tr><td>4</td><td>account-service</td><td>Go</td><td>8004</td><td>Balances + ledger</td></tr><tr><td>5</td><td>notification-service</td><td>FastAPI</td><td>8005</td><td>Simulated dispatch</td></tr><tr><td>6</td><td>audit-service</td><td>FastAPI</td><td>8006</td><td>Append-only audit sink (SQLite)</td></tr><tr><td>7</td><td>config-service</td><td>Go</td><td>8007</td><td>Non-secret config from a ConfigMap</td></tr><tr><td>8</td><td>report-service</td><td>FastAPI</td><td>8008</td><td>Admin compliance/summary reports</td></tr></tbody></table>
<p>The design principle throughout: <strong>realism lives in the inter-service call patterns, not in business completeness.</strong> The app exists to generate believable east-west traffic. Those calls form a graph — the exact traffic that gets captured later:</p>
<pre class="language-text"><code class="language-text">client ──login──▶ auth-service ──▶ user-service (verify-credentials)
                       └─────────▶ audit-service (login events)

client ──▶ payment-service ──▶ account-service (debit / credit ledger)
                └──────────────▶ audit-service (payment_processed)
                └──────────────▶ notification-service (payment_confirmation)
account-service ──▶ audit-service (ledger_applied)
account-service ──▶ payment-service (/payments/{ref}/callback)
report-service  ──▶ account-service + audit-service (admin reports)
auth, payment, report ──▶ config-service (GET /config, on startup + every ~30s)
</code></pre>
<p>One decision is threaded through every layer and is worth stating loudly: <strong>internal pod-to-pod calls have no authentication.</strong> Users authenticate at the edge with a JWT, but services trust each other implicitly inside the cluster. That is not a bug I neglected to fix — it is a faithful model of the default Kubernetes trust boundary, and it is precisely the gap the research studies. &quot;Hardening&quot; it would delete the phenomenon under investigation.</p>
<h3>The mesh: Istio with permissive mTLS</h3>
<p>The services run under Istio 1.19.9 with PeerAuthentication in <strong>PERMISSIVE</strong> mTLS mode, with Prometheus, Grafana, and Kiali for observability. Permissive mode matters for two reasons. First, it is what real clusters run during migration, so it is realistic. Second, and more importantly for the method: the traffic is encrypted, but the detector never looks inside a packet. It works entirely on flow-level metadata — sizes, counts, timing, TCP window values — which survives encryption. The approach does not depend on decrypting anything, and that is a deliberate property, not a limitation.</p>
<h3>The traffic: benign baseline and emulated adversary</h3>
<p>Two generators run against the mesh:</p>
<p><strong>Benign traffic</strong> comes from Locust, deployed in a separate <code>load-gen</code> namespace (no sidecar, so its own traffic is distinguishable). It runs two personas — a majority of <code>CustomerUser</code>s and a few <code>AdminUser</code>s — under a <em>diurnal load shape</em> that varies concurrency by the real hour of day, so the baseline breathes like a real system rather than droning at constant RPS.</p>
<p><strong>Adversarial traffic</strong> comes from a compromised pod inside the <code>fintech</code> namespace, running five MITRE ATT&amp;CK lateral-movement techniques:</p>
<ul>
<li><strong>T1046</strong> — Network Service Discovery (scanning the mesh for reachable services)</li>
<li><strong>T1550.001</strong> — Use Alternate Authentication Material: Application Access Token (replaying a harvested JWT)</li>
<li><strong>T1210</strong> — Exploitation of Remote Services (IDOR-style object enumeration + a malformed request)</li>
<li><strong>T1083</strong> — File and Directory Discovery, adapted to ConfigMap/Secret enumeration via the pod&#x27;s service account</li>
<li><strong>T1557</strong> — Adversary-in-the-Middle</li>
</ul>
<p>A note on method here: the original plan used MITRE Caldera as the C2 orchestrator. After several build cycles fighting an end-of-life base image, a Python version that had removed a module Caldera depended on, and an API returning silent 500s, I judged the C2 server to be <em>incidental complexity</em> — the techniques themselves are just commands run from the compromised pod. I replaced Caldera with a ~200-line runner that produces identical on-the-wire traffic and, crucially, writes a <strong>structured ground-truth log</strong> for every operation: technique ID, start/finish timestamps, source pod, and destination. That log is what makes the dataset labellable at all. The pivot is documented as a methodological limitation rather than hidden.</p>
<h2>The machine-learning pipeline</h2>
<p>The data path turns raw packets into a trained, evaluated classifier in five stages. Each stage hides a correctness problem that took real effort to get right.</p>
<pre class="language-text"><code class="language-text">tcpdump (CNI bridge)  →  CICFlowMeter  →  flow-tuple labelling  →  feature engineering  →  RF / XGBoost
raw packets              ~78 features/flow   ±2s vs ground truth     variance→corr→RFECV      train &amp; evaluate
</code></pre>
<p><strong>1. Capture.</strong> A <code>hostNetwork</code> tcpdump pod captures packets off the CNI bridge across the pod CIDR. This is metadata-only by design — no payload is retained or inspected.</p>
<p><strong>2. Flow extraction.</strong> CICFlowMeter converts packets into per-flow records — roughly 78 statistical features per bidirectional flow (packet-length distributions, inter-arrival timing, byte and packet counts, TCP window sizes, flags). A flow, not a packet, is the unit of classification.</p>
<p><strong>3. Labelling.</strong> This is where correctness actually lives. A flow is labelled malicious only if the attacker pod is an endpoint, its start time falls within ±2 seconds of a logged attack operation, <em>and</em> its destination matches that operation&#x27;s target. Everything else — including legitimate traffic from the attacker pod outside attack windows — is benign. Every bug I hit at this stage was an <em>alignment</em> bug: flow timestamps in local time while ground truth was UTC; then, once fixed, sub-second attack windows (one technique&#x27;s window was 0.384s) collapsing into each other under one-second timestamp resolution. The fix was microsecond timestamps plus a best-match rule that prefers the tightest-fitting window, and resolving destinations by both pod IP and ClusterIP.</p>
<p><strong>4. Feature engineering.</strong> Feature selection was data-driven, not hand-picked: drop near-zero-variance features, remove one of each highly-correlated pair (keeping the higher Random-Forest-importance member), then run recursive feature elimination with cross-validation (RFECV). That reduced ~78 candidates to <strong>12 features</strong>:</p>
<pre class="language-text"><code class="language-text">fwd_pkt_len_max     fwd_pkt_len_mean    pkt_len_min       pkt_len_mean
fwd_header_len      fwd_act_data_pkts   flow_iat_std      init_fwd_win_byts
init_bwd_win_byts   subflow_fwd_byts    protocol          dst_port
</code></pre>
<p>Two of these (<code>protocol</code>, <code>dst_port</code>) are categorical, which matters for the balancing step below.</p>
<p><strong>5. Dataset construction and training.</strong> A 3-hour collection yielded <strong>100,131 benign flows and 420 malicious (0.42%)</strong> — a real, severe class imbalance. I used a stratified 70/15/15 split and then made two disciplined choices:</p>
<ul>
<li><strong>StandardScaler was fit on the training split only</strong>, then applied to validation and test — no test statistics leak into preprocessing.</li>
<li><strong>SMOTENC oversampling was applied only to the training set</strong>, balancing it to 70,091 / 70,091. Validation and test were left at their true, ugly, 0.42% distribution. Balancing the test set would have been measuring a fiction.</li>
</ul>
<p>Both models — Random Forest and XGBoost — were tuned with Optuna (60 trials, TPE sampler, validation-F1 objective), then evaluated on the held-out imbalanced test set, with 5-fold cross-validation and SHAP for interpretability.</p>
<h2>The findings</h2>
<h3>RQ2 — Random Forest vs XGBoost</h3>
<p>Both models detect the attacks; Random Forest edged it on the imbalanced test set.</p>
<table><thead><tr><th>Metric</th><th>Random Forest</th><th>XGBoost</th></tr></thead><tbody><tr><td>Precision</td><td>0.984</td><td>0.969</td></tr><tr><td>Recall</td><td>1.000</td><td>1.000</td></tr><tr><td><strong>F1</strong></td><td><strong>0.992</strong></td><td>0.984</td></tr><tr><td>False-positive rate</td><td>0.00007</td><td>0.00007</td></tr><tr><td>ROC-AUC</td><td>1.000</td><td>1.000</td></tr><tr><td>Inference</td><td>0.0015 ms/sample</td><td>0.0005 ms/sample</td></tr><tr><td>Model size on disk</td><td>0.20 MB</td><td>0.40 MB</td></tr><tr><td>Peak inference memory</td><td>4.5 MB</td><td>—</td></tr></tbody></table>
<p>The Random Forest confusion matrix on the test set was <strong>TP 63, FN 0, FP 1, TN 15,019</strong> — every malicious flow caught, a single benign flow misclassified. Per-technique recall was 1.0 for T1046 (37 flows), T1550.001 (13), T1210 (10), and T1083 (3). T1557 produced too few labelled flows in the test split to report — a real limitation of a short collection against a rare technique.</p>
<p>Both models answer RQ1 affirmatively <em>and</em> satisfy the lightweight constraint: sub-millisecond inference, sub-megabyte on disk, a few megabytes of inference working set. Random Forest was selected as the deployed model for its slightly higher F1 and its more interpretable structure.</p>
<h3>RQ3 — the signature baseline</h3>
<p>I ran Suricata 8.0.6 with the Emerging Threats Open ruleset over the same captured packets. It detected <strong>zero of the five techniques</strong> — recall 0.00. Its only output was ~1.6 million &quot;invalid checksum&quot; decoder warnings, which are an artifact of capturing on a virtual bridge (checksum offload), not detections; I had to filter them out and re-run with checksum validation disabled to confirm there was genuinely nothing.</p>
<p>This is the crux of the comparison, and I want to be precise about what it shows. It is <em>not</em> &quot;my model beats Suricata at Suricata&#x27;s job.&quot; It is that signature detection is <strong>structurally blind</strong> to novel, Kubernetes-native lateral movement, because these techniques have no payload signature to match — which is exactly the gap the ML approach is meant to fill.</p>
<h3>Was the result too good?</h3>
<p>An F1 of 0.992 and an AUC of 1.0 are a smell, not a trophy. There are two ways to earn a score like that dishonestly — leaking a feature, or a trivially separable task — and I tested for both.</p>
<p><strong>Leakage.</strong> My concern was <code>dst_port</code>: if the model just learned &quot;traffic to this port is bad,&quot; that is an address lookup, not detection. So I ran an ablation, retraining with suspect features removed:</p>
<pre class="language-text"><code class="language-text">full feature set                          F1 = 0.9921
minus dst_port                            F1 = 0.9921   ← unchanged
minus dst_port + both TCP-window features F1 = 0.9844   ← barely moved
</code></pre>
<p>Removing <code>dst_port</code> changed the score by exactly zero. The signal is distributed across the packet-length and timing features — it is behavioural, not an addressing artifact. That is the reassuring result.</p>
<p><strong>Separability — the honest caveat.</strong> The task is genuinely easy <em>in this testbed</em>, and that is the single most important sentence in the whole project. The emulated attacks are burst-style: rapid, uniform, machine-generated — twenty calls in four seconds, a port scan that fans out at once. Against varied, human-paced benign traffic, that stands out sharply. A real APT moving <em>low and slow</em> — deliberately pacing actions to blend into the baseline — is a materially harder problem, and this dataset does not demonstrate the model would catch it. The near-perfect numbers are a property of the adversary emulation as much as the classifier, and they belong in the limitations chapter, stated plainly, not buried.</p>
<h3>Deployment cost</h3>
<p>I also deployed the Random Forest as an in-cluster FastAPI inference service in a <code>security-monitoring</code> namespace to measure real overhead. Server-side inference held around 0.004 ms/sample. One finding is worth surfacing: the pod would not start in the 256 MB the methodology originally budgeted — not because the model is heavy (it is under a megabyte) but because the Python + scikit-learn <em>serving runtime</em> has a ~175 MB resident floor. The model is lightweight; a Python process serving it is less so. That distinction — model cost vs. serving-runtime cost — is a genuine deployment insight, and I raised the limit to 512 MB with a startup probe to accommodate it.</p>
<h2>Limitations</h2>
<p>Being explicit about these is part of the contribution:</p>
<ul>
<li><strong>Single node.</strong> Everything ran on one minikube node, which saturated under combined load + capture and truncated some collection windows. A multi-node cluster would exercise cross-node traffic paths this setup cannot.</li>
<li><strong>Loud adversary.</strong> As above, burst-style emulation makes the task easier than a patient real APT would.</li>
<li><strong>Short collection.</strong> Three hours gave a usable but small malicious class; T1557 in particular was under-represented.</li>
<li><strong>Emulated, not real.</strong> Techniques are faithful ATT&amp;CK emulations, not captured real-world APT traffic — the perennial constraint of this research area.</li>
</ul>
<h2>Conclusion</h2>
<p>The research answers its questions. Lightweight ML on flow-level metadata <strong>can</strong> detect emulated APT lateral movement between Kubernetes microservices (RQ1); Random Forest slightly outperforms XGBoost while both meet the lightweight constraint (RQ2); and a mature signature IDS detects none of the same techniques (RQ3) — evidence that behavioural ML addresses a gap signatures cannot.</p>
<p>The two results have to be held together honestly. The satisfying one: a sub-megabyte model, reading only encryption-safe flow features, caught every technique a production IDS missed, cheaply enough to run inside the cluster it protects. The sobering one: the emulated attacks were loud, and the real test is a quiet adversary I have not yet built.</p>
<p>For me the most valuable output was not the 0.992. It was the reproducible pipeline — capture, label, select, train, and <em>interrogate your own results</em> — and a methodology honest enough to name why its best number might not survive contact with a patient attacker. That is the next piece of work: a low-and-slow adversary model, on a multi-node cluster, over a much longer collection.</p>]]></content:encoded>
            <author>hello@a4m.dev (Akeem Amusat)</author>
        </item>
        <item>
            <title><![CDATA[How I Stopped Copy-Pasting Jira Tickets Into Codex]]></title>
            <link>https://a4m.dev/articles/how-i-stopped-copy-pasting-jira-tickets-into-codex</link>
            <guid>https://a4m.dev/articles/how-i-stopped-copy-pasting-jira-tickets-into-codex</guid>
            <pubDate>Sun, 05 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[I built a Jira MCP server so Codex can fetch ticket details directly from Jira. Now I can hand it a ticket key instead of pasting long issue descriptions and comments into every prompt.]]></description>
            <content:encoded><![CDATA[<p>I got tired of being a human API between Jira and Codex.</p>
<p>That was the whole problem.</p>
<p>Every time I wanted Codex to work on a real ticket, I had to open Jira, copy the title, copy the description, copy the acceptance criteria, sometimes copy the recent comments, then paste all of that into the prompt before I could even ask it to start.</p>
<p>It was stupid work.</p>
<p>Not hard work. Not valuable work. Just stupid work.</p>
<p>And worse, it is exactly the kind of work that makes AI tooling feel more magical in demos than it does in real life. In a demo, people say, “Just ask the agent to do the task.” In practice, somebody still has to shovel the context into the prompt first.</p>
<p>I did not want that somebody to keep being me.</p>
<p>What I wanted was this:</p>
<pre class="language-text"><code class="language-text">Work on FM-1234.
</code></pre>
<p>That is what this project was about.</p>
<p>I built a Jira MCP server in Go so Codex could fetch ticket details directly from Jira. Now I can hand it a ticket key instead of pasting a small essay into every prompt.</p>
<h2>The Problem With Copy-Paste Prompts</h2>
<p>Copy-pasting Jira into prompts is annoying, but the real problem is that it is a bad interface.</p>
<p>It is slow.</p>
<p>It is easy to get incomplete.</p>
<p>It goes stale immediately.</p>
<p>And it trains you to think that “prompt engineering” means manually flattening your tooling into text.</p>
<p>Real Jira tickets are rarely just a title and a neat little description. They usually include:</p>
<ul>
<li>acceptance criteria</li>
<li>linked metadata</li>
<li>clarifying comments</li>
<li>updates from product or engineering</li>
</ul>
<p>So every time I pasted one into Codex, I was making two things worse at the same time:</p>
<ol>
<li>my workflow got slower</li>
<li>the prompt got dirtier</li>
</ol>
<p>Instead of writing a clean instruction like this:</p>
<pre class="language-text"><code class="language-text">Use jira_get_issue for FM-1234, summarize the requirements, implement the change, and run tests.
</code></pre>
<p>I was writing prompts bloated with context that should never have been in the prompt in the first place.</p>
<p>That was the motivation for the project. I was not trying to build “AI infrastructure” for the sake of it. I was trying to remove a useless step from a workflow I was already using every day.</p>
<h2>Why MCP Was The Right Fit</h2>
<p>I think a lot of people over-explain MCP. The practical value is simple: it gives the agent a structured way to use external systems instead of forcing you to paste everything as text.</p>
<p>In my case, the shape was obvious:</p>
<pre class="language-text"><code class="language-text">Codex -&gt; MCP server -&gt; Jira API
</code></pre>
<p>That is better than copy-paste prompts for a few reasons:</p>
<ul>
<li>Codex can fetch the latest ticket details on demand</li>
<li>the workflow is reusable instead of being rebuilt every time</li>
<li>the data can be exposed as tools, not just dumped as prose</li>
<li>I can keep the same integration locally over stdio and remotely over HTTP</li>
</ul>
<p>Once I saw that clearly, the project stopped feeling like “AI stuff” and started feeling like what it actually was: a small integration service with very normal engineering constraints.</p>
<h2>What I Built</h2>
<p>I built a read-oriented Jira MCP server in Go.</p>
<p>The scope was intentionally narrow. I did not start with “let the agent manage Jira.” I started with “stop making me act like a clipboard.”</p>
<p>The server exposes a small set of tools:</p>
<ul>
<li><code>jira_get_issue</code></li>
<li><code>jira_search_issues</code></li>
<li><code>jira_my_open_issues</code></li>
</ul>
<p>It also exposes issue content as a resource so clients can consume a normalized version of a ticket instead of raw Jira noise.</p>
<p>I kept it read-only on purpose.</p>
<p>I am opinionated about this: read access gives you most of the value with a fraction of the risk.</p>
<p>The biggest win here is not letting an agent transition issues or spray comments into Jira. The biggest win is letting it read the task from the source of truth without me manually relaying it.</p>
<h2>The Real Target Workflow</h2>
<p>The workflow I wanted was very simple:</p>
<ol>
<li>I mention a Jira key in a prompt.</li>
<li>Codex uses the Jira MCP server to fetch the issue.</li>
<li>It summarizes the work, inspects the codebase, and implements the task.</li>
<li>I review the result instead of manually relaying ticket context.</li>
</ol>
<p>That changes the shape of the whole interaction.</p>
<p>Before:</p>
<pre class="language-text"><code class="language-text">Here is the Jira ticket:

Title: ...
Description: ...
Acceptance criteria: ...
Comments: ...

Now inspect this repo and implement the change.
</code></pre>
<p>After:</p>
<pre class="language-text"><code class="language-text">Use the Jira MCP server to fetch FM-1234 first.
Summarize the ticket, inspect the repo, implement the change, and run tests.
</code></pre>
<p>That is a much better interface.</p>
<p>The prompt goes back to being an instruction instead of a data dump. That sounds like a small difference, but it changes the feel of the workflow completely.</p>
<h2>Why I Chose Go</h2>
<p>Go was the obvious choice for this kind of tool.</p>
<ul>
<li>it is a good fit for small services</li>
<li>shipping a single binary is convenient</li>
<li>the standard library is enough for most of the plumbing</li>
<li>it encourages boring code, which is exactly what I wanted</li>
</ul>
<p>This was not a project where cleverness would help. I wanted something small, legible, and deployable without ceremony.</p>
<h2>The First Version: Stdio</h2>
<p>I started with stdio because that is the fastest path to proving the idea works.</p>
<p>I only cared about three things at that stage:</p>
<ul>
<li>can Codex call the tools</li>
<li>can the server authenticate to Jira</li>
<li>can the issue data be normalized into something useful for an agent</li>
</ul>
<p>That first version mattered because it kept the project honest. Before I spent time on hosting, reverse proxies, or custom domains, I needed to know whether the workflow improvement was real.</p>
<p>It was.</p>
<p>The moment the local version worked, the value was obvious. I stopped copying Jira tickets into prompts and started giving Codex a ticket key instead.</p>
<p>That was enough to justify the rest of the work.</p>
<h2>Normalizing Jira For An Agent</h2>
<p>One of the most important parts of the build was not transport. It was shape.</p>
<p>Jira issue payloads are designed for Jira, not for coding agents.</p>
<p>If you hand raw Jira JSON to an agent, you get what you deserve:</p>
<ul>
<li>nested field structures</li>
<li>formatting metadata</li>
<li>fields that are technically present but practically irrelevant</li>
</ul>
<p>So I normalized the issue data into something more useful:</p>
<ul>
<li>concise issue metadata</li>
<li>a readable text representation</li>
<li>structured fields the client can still use downstream</li>
</ul>
<p>I think this part gets underrated. Access alone is not enough. Good tooling reduces entropy before the data ever reaches the model.</p>
<h2>From Local Tool To Deployable Service</h2>
<p>Once the stdio flow worked, I wanted a deployable HTTP version too.</p>
<p>The runtime now supports:</p>
<ul>
<li><code>stdio</code> for local use</li>
<li>streamable HTTP for remote access</li>
</ul>
<p>I deployed it as an HTTP service, protected it with bearer token auth, added a custom domain, and configured Codex to talk to the remote endpoint.</p>
<p>At that point the architecture looked more like this:</p>
<pre class="language-text"><code class="language-text">Codex client
  -&gt; remote MCP endpoint
  -&gt; Jira API
</code></pre>
<p>That is a better setup if I want to reuse the server across machines, shells, or tools without treating my laptop as the integration layer.</p>
<h2>One Gotcha: HTTP MCP Session Initialization</h2>
<p>The most annoying protocol detail showed up after I deployed the HTTP version.</p>
<p>A plain <code>curl</code> to the MCP endpoint does not prove much. With stateful HTTP MCP, you need the right sequence:</p>
<ol>
<li><code>initialize</code></li>
<li>capture <code>Mcp-Session-Id</code></li>
<li>send <code>notifications/initialized</code></li>
<li>then call methods like <code>tools/list</code></li>
</ol>
<p>If you skip that flow, you can get errors that look confusing at first, such as methods being “invalid during session initialization”.</p>
<p>That was a good reminder that once you move from local stdio to hosted HTTP, you are not just writing a wrapper anymore. You are dealing with protocol state, auth, sessions, hosting, and all the boring details that demos conveniently skip.</p>
<h2>Making Deployment Boring</h2>
<p>I wanted deployment to be boring, because boring is what makes internal tools survive.</p>
<p>The server now supports:</p>
<ul>
<li>environment-based configuration</li>
<li><code>.env</code> loading with sane precedence</li>
<li>Docker builds</li>
<li>Heroku-friendly HTTP binding via <code>PORT</code></li>
<li>bearer token protection for the remote MCP endpoint</li>
</ul>
<p>That stuff matters more than people like to admit. A lot of internal tools die in the gap between “it works locally” and “I trust it enough to depend on it.”</p>
<p>My standard for success was simple:</p>
<ul>
<li>deploy it</li>
<li>hit <code>/healthz</code></li>
<li>point Codex at the <code>/mcp</code> endpoint</li>
<li>stop thinking about the server unless something actually breaks</li>
</ul>
<p>If the tool itself becomes noisy, then all I have done is replace one annoying workflow tax with another.</p>
<h2>Observability Was Worth Adding Early</h2>
<p>Once the service was remotely deployed, observability stopped being optional.</p>
<p>I added Sentry so I could capture:</p>
<ul>
<li>runtime errors</li>
<li>request traces</li>
<li>mirrored application logs</li>
<li>server-side failures and <code>5xx</code> paths</li>
</ul>
<p>I am glad I added that early. Protocol bugs and deployment bugs are much less interesting when you are debugging them blind.</p>
<p>Without observability, the loop looks like this:</p>
<ul>
<li>the client says something vague failed</li>
<li>you check the server</li>
<li>you reproduce manually</li>
<li>you guess</li>
</ul>
<p>With observability, you get a far tighter loop.</p>
<p>For a service that sits between a coding agent and Jira, that is important. If the tool layer is unreliable, the user stops trusting the entire workflow.</p>
<h2>CI Was Also Non-Negotiable</h2>
<p>This repo is small, but small repos still deserve CI.</p>
<p>I added GitHub Actions for:</p>
<ul>
<li><code>gofmt</code> checks</li>
<li><code>go mod tidy</code> verification</li>
<li>tests</li>
<li>build verification</li>
<li>Docker build validation</li>
</ul>
<p>That is not over-engineering. It is the minimum needed to keep a small integration service honest.</p>
<p>I do not want to rediscover a broken auth path or a bad config regression in production because I was too lazy to add a basic workflow.</p>
<h2>What Changed In My Day-To-Day Workflow</h2>
<p>This is the only part that really matters.</p>
<p>Before, I had to manually translate Jira into prompt input.</p>
<p>Now, I can do something much closer to this:</p>
<pre class="language-text"><code class="language-text">Use Jira to fetch FM-1234, summarize the task, inspect the repo, implement the change, and run tests.
</code></pre>
<p>Or, with a standing instruction in my environment, simply:</p>
<pre class="language-text"><code class="language-text">Work on FM-1234.
</code></pre>
<p>That is the win.</p>
<p>The interface between me and Codex is smaller and cleaner:</p>
<ul>
<li>I provide intent</li>
<li>the server provides ticket context</li>
<li>Codex works from the source of truth</li>
</ul>
<p>That is how these tools should feel. I do not want to babysit the context-loading step if the information already exists in a system the agent can read.</p>
<h2>What I Would Add Next</h2>
<p>If I keep pushing the project forward, the next improvements are pretty obvious.</p>
<p>The first is deeper trace instrumentation around outbound Jira calls so latency inside the Jira client is easier to understand.</p>
<p>The second is better ticket context shaping, especially around comments and acceptance criteria, so the most relevant parts rise to the top more aggressively.</p>
<p>The third is carefully scoped write operations, such as adding comments or transitioning issues, but only if I am confident the safety model is clear.</p>
<p>I would still keep the default posture conservative. Read access is where most of the value is, and it is much easier to trust than write automation.</p>
<h2>Lessons From Building It</h2>
<p>Building this reinforced a few opinions I already had.</p>
<h3>1. Practical AI tooling starts with workflow pain</h3>
<p>The project existed because copy-pasting Jira tickets into prompts was annoying enough to be worth fixing.</p>
<p>That is a better starting point than trying to invent a use case for a protocol or an agent.</p>
<h3>2. Transport is less interesting than interface quality</h3>
<p>MCP is useful, but the protocol is not the headline.</p>
<p>The headline is that Codex can ask for the right ticket data in a clean, structured way without me manually stuffing it into the prompt.</p>
<h3>3. Deployment and observability matter quickly</h3>
<p>The moment a tool becomes part of your daily workflow, you need enough operational maturity that it can be trusted.</p>
<p>That means:</p>
<ul>
<li>sane config</li>
<li>clear auth</li>
<li>a health check</li>
<li>CI</li>
<li>traces and error visibility</li>
</ul>
<h3>4. Read-only tools can be incredibly useful</h3>
<p>You do not need full autonomy to get real value. Sometimes the biggest win is simply giving the agent direct access to the right context at the right time.</p>
<h2>Final Thoughts</h2>
<p>I built this because I was tired of copy-pasting Jira into Codex.</p>
<p>That is not a glamorous origin story, but I think it is the right kind.</p>
<p>The best internal tools usually start with a very ordinary frustration. Then, if you solve the right problem cleanly, they become part of your default workflow.</p>
<p>That is what happened here.</p>
<p>I no longer think in terms of “paste the ticket into the prompt.” I think in terms of “give Codex the ticket key and let the tooling do its job.”</p>
<p>To me, that is the real value of projects like this. Not that they sound futuristic. Not that they use a new protocol. The value is that they reduce friction in a workflow you already care about.</p>
<p>In this case, the difference between:</p>
<pre class="language-text"><code class="language-text">Here is a pasted Jira ticket. Please work on it.
</code></pre>
<p>and:</p>
<pre class="language-text"><code class="language-text">Work on FM-1234.
</code></pre>
<p>is bigger than it looks.</p>]]></content:encoded>
            <author>hello@a4m.dev (Akeem Amusat)</author>
        </item>
        <item>
            <title><![CDATA[Monitoring Temporal Schedulers with Sentry Cron in Go]]></title>
            <link>https://a4m.dev/articles/monitoring-temporal-schedulers-with-sentry-cron-in-go</link>
            <guid>https://a4m.dev/articles/monitoring-temporal-schedulers-with-sentry-cron-in-go</guid>
            <pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[How I added Sentry Cron Monitoring to critical Temporal schedulers in Go so missed runs, failures, and stuck jobs became visible immediately instead of being discovered late.]]></description>
            <content:encoded><![CDATA[<p>Temporal gives you a lot out of the box: retries, workflow history, durability, and a clean way to model long-running work.</p>
<p>What it does not automatically give you is confidence that your critical schedulers are actually running the way the business expects.</p>
<p>That gap matters more than people like to admit.</p>
<p>If a scheduler fails loudly, you usually notice. If it stops running, runs late, or gets stuck halfway through a critical recurring workflow, you can lose hours before anyone realizes something is wrong. That is exactly the kind of failure that hurts in production because nothing looks obviously broken until downstream numbers start drifting.</p>
<p>I ran into that problem while working on a backend system with Temporal schedulers that were important enough to deserve first-class visibility. We already had Sentry in place, so the obvious next step was to use <strong>Sentry Cron Monitoring</strong> for the workflows that mattered most.</p>
<p>That turned out to be one of those small observability changes that pays for itself quickly.</p>
<h2>Why errors were not enough</h2>
<p>A lot of teams think scheduler monitoring means “capture the exception if the job crashes.”</p>
<p>That is not enough.</p>
<p>Schedulers can fail in more boring ways:</p>
<ul>
<li>the workflow never starts</li>
<li>the workflow starts late</li>
<li>the workflow gets stuck and never reaches a terminal state</li>
<li>a child workflow fails but the top-level schedule keeps looking alive</li>
<li>the workflow completes inconsistently and no one notices until a report is wrong</li>
</ul>
<p>If your only signal is error reporting, you will miss some of the most operationally annoying failure modes.</p>
<p>That is why cron-style monitoring is useful. It gives you a heartbeat for scheduled work, not just exception capture after things have already gone sideways.</p>
<h2>What Sentry Cron Monitoring gives you</h2>
<p>What I like about Sentry Cron Monitoring is that the model is simple.</p>
<p>You send a check-in when a scheduled run starts, and then you mark that same run as successful or failed when it finishes. Once you do that consistently, Sentry can tell you:</p>
<ul>
<li>whether the job started on time</li>
<li>whether it completed</li>
<li>whether it failed</li>
<li>whether it missed an expected run</li>
</ul>
<p>And once alerts are wired properly, the signal becomes immediately useful instead of just being another dashboard nobody checks.</p>
<p>For critical schedulers, that is exactly the kind of visibility you want.</p>
<h2>The pattern I used</h2>
<p>The implementation pattern was straightforward:</p>
<ol>
<li>Send an <code>in_progress</code> check-in when the workflow starts.</li>
<li>Keep the returned check-in ID.</li>
<li>Mark that same check-in as <code>ok</code> on success.</li>
<li>Mark that same check-in as <code>error</code> on failure.</li>
</ol>
<p>That is the whole loop.</p>
<p>The important part is not the amount of code. The important part is being disciplined enough to send both the start and terminal states consistently.</p>
<h2>Step 1: wrap the Sentry check-in in a small activity</h2>
<p>I prefer putting the Sentry call behind a small activity instead of scattering <code>CaptureCheckIn</code> calls around workflow code directly.</p>
<p>That gives you a reusable abstraction and keeps the workflow code easier to read.</p>
<p>Here is a generalized version of the pattern:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">package</span> activities

<span class="token keyword">import</span> <span class="token punctuation">(</span>
  <span class="token string">&quot;context&quot;</span>

  <span class="token string">&quot;github.com/getsentry/sentry-go&quot;</span>
<span class="token punctuation">)</span>

<span class="token keyword">type</span> SentryCronMonitorActivity <span class="token keyword">struct</span><span class="token punctuation">{</span><span class="token punctuation">}</span>

<span class="token keyword">type</span> CheckInParams <span class="token keyword">struct</span> <span class="token punctuation">{</span>
  MonitorSlug <span class="token builtin">string</span>
  Status      sentry<span class="token punctuation">.</span>CheckInStatus
  CheckInID   sentry<span class="token punctuation">.</span>EventID
<span class="token punctuation">}</span>

<span class="token keyword">func</span> <span class="token punctuation">(</span>a <span class="token operator">*</span>SentryCronMonitorActivity<span class="token punctuation">)</span> <span class="token function">CheckInCronMonitor</span><span class="token punctuation">(</span>
  ctx context<span class="token punctuation">.</span>Context<span class="token punctuation">,</span>
  params <span class="token operator">*</span>CheckInParams<span class="token punctuation">,</span>
<span class="token punctuation">)</span> <span class="token punctuation">(</span>sentry<span class="token punctuation">.</span>EventID<span class="token punctuation">,</span> <span class="token builtin">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  hub <span class="token operator">:=</span> sentry<span class="token punctuation">.</span><span class="token function">GetHubFromContext</span><span class="token punctuation">(</span>ctx<span class="token punctuation">)</span>
  <span class="token keyword">if</span> hub <span class="token operator">==</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
    hub <span class="token operator">=</span> sentry<span class="token punctuation">.</span><span class="token function">CurrentHub</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Clone</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token punctuation">}</span>

  checkIn <span class="token operator">:=</span> <span class="token operator">&amp;</span>sentry<span class="token punctuation">.</span>CheckIn<span class="token punctuation">{</span>
    MonitorSlug<span class="token punctuation">:</span> params<span class="token punctuation">.</span>MonitorSlug<span class="token punctuation">,</span>
    Status<span class="token punctuation">:</span>      params<span class="token punctuation">.</span>Status<span class="token punctuation">,</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">if</span> params<span class="token punctuation">.</span>CheckInID <span class="token operator">!=</span> <span class="token string">&quot;&quot;</span> <span class="token punctuation">{</span>
    checkIn<span class="token punctuation">.</span>ID <span class="token operator">=</span> params<span class="token punctuation">.</span>CheckInID
  <span class="token punctuation">}</span>

  checkInID <span class="token operator">:=</span> hub<span class="token punctuation">.</span><span class="token function">CaptureCheckIn</span><span class="token punctuation">(</span>checkIn<span class="token punctuation">,</span> <span class="token boolean">nil</span><span class="token punctuation">)</span>
  <span class="token keyword">if</span> checkInID <span class="token operator">==</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
    <span class="token keyword">return</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">,</span> <span class="token boolean">nil</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">return</span> <span class="token operator">*</span>checkInID<span class="token punctuation">,</span> <span class="token boolean">nil</span>
<span class="token punctuation">}</span>
</code></pre>
<p>There are two details here that matter:</p>
<ul>
<li><code>MonitorSlug</code> must stay stable for the job you are monitoring</li>
<li>the terminal update must reuse the <code>CheckInID</code> returned by the initial <code>in_progress</code> event</li>
</ul>
<p>If you skip the second part, you are not really updating the same run anymore.</p>
<h2>Step 2: send an <code>in_progress</code> check-in as soon as the workflow starts</h2>
<p>I like doing this at the top of the workflow, before the real work begins.</p>
<p>In Temporal, I usually send it with a <strong>local activity</strong> because the call is lightweight and I want it close to workflow startup.</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">ReconcileBalances</span><span class="token punctuation">(</span>ctx workflow<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> monitorSlug <span class="token operator">=</span> <span class="token string">&quot;reconcile-balances&quot;</span>

  logger <span class="token operator">:=</span> workflow<span class="token punctuation">.</span><span class="token function">GetLogger</span><span class="token punctuation">(</span>ctx<span class="token punctuation">)</span>
  logger<span class="token punctuation">.</span><span class="token function">Info</span><span class="token punctuation">(</span><span class="token string">&quot;starting reconcile balances workflow&quot;</span><span class="token punctuation">)</span>

  <span class="token keyword">var</span> monitorActivity <span class="token operator">*</span>activities<span class="token punctuation">.</span>SentryCronMonitorActivity
  localCtx <span class="token operator">:=</span> workflow<span class="token punctuation">.</span><span class="token function">WithLocalActivityOptions</span><span class="token punctuation">(</span>ctx<span class="token punctuation">,</span> workflow<span class="token punctuation">.</span>LocalActivityOptions<span class="token punctuation">{</span>
    StartToCloseTimeout<span class="token punctuation">:</span> <span class="token number">30</span> <span class="token operator">*</span> time<span class="token punctuation">.</span>Second<span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span>

  <span class="token keyword">var</span> checkInID sentry<span class="token punctuation">.</span>EventID
  <span class="token boolean">_</span> <span class="token operator">=</span> workflow<span class="token punctuation">.</span><span class="token function">ExecuteLocalActivity</span><span class="token punctuation">(</span>
    localCtx<span class="token punctuation">,</span>
    monitorActivity<span class="token punctuation">.</span>CheckInCronMonitor<span class="token punctuation">,</span>
    <span class="token operator">&amp;</span>activities<span class="token punctuation">.</span>CheckInParams<span class="token punctuation">{</span>
      MonitorSlug<span class="token punctuation">:</span> monitorSlug<span class="token punctuation">,</span>
      Status<span class="token punctuation">:</span>      sentry<span class="token punctuation">.</span>CheckInStatusInProgress<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>localCtx<span class="token punctuation">,</span> <span class="token operator">&amp;</span>checkInID<span class="token punctuation">)</span>

  <span class="token comment">// main workflow logic continues here...</span>
  <span class="token keyword">return</span> <span class="token boolean">nil</span>
<span class="token punctuation">}</span>
</code></pre>
<p>That first check-in buys you something important immediately: if the scheduler stops triggering or starts drifting badly, you now have a proper signal around that behavior.</p>
<h2>Step 3: mark the run as failed when the workflow fails</h2>
<p>This is where a lot of implementations get sloppy.</p>
<p>If you only send the start state and never close the run properly, you end up with noisy monitoring and confusing signals.</p>
<p>A failure path should explicitly update the same check-in:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
  <span class="token boolean">_</span> <span class="token operator">=</span> workflow<span class="token punctuation">.</span><span class="token function">ExecuteLocalActivity</span><span class="token punctuation">(</span>
    localCtx<span class="token punctuation">,</span>
    monitorActivity<span class="token punctuation">.</span>CheckInCronMonitor<span class="token punctuation">,</span>
    <span class="token operator">&amp;</span>activities<span class="token punctuation">.</span>CheckInParams<span class="token punctuation">{</span>
      MonitorSlug<span class="token punctuation">:</span> monitorSlug<span class="token punctuation">,</span>
      Status<span class="token punctuation">:</span>      sentry<span class="token punctuation">.</span>CheckInStatusError<span class="token punctuation">,</span>
      CheckInID<span class="token punctuation">:</span>   checkInID<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>localCtx<span class="token punctuation">,</span> <span class="token boolean">nil</span><span class="token punctuation">)</span>

  <span class="token keyword">return</span> err
<span class="token punctuation">}</span>
</code></pre>
<p>That makes the run state clear in Sentry and gives your alert rules something concrete to work with.</p>
<h2>Step 4: mark the run as successful when the workflow completes</h2>
<p>Success needs the same discipline as failure.</p>
<p>Do not assume “no exception” is enough. Send the terminal success state explicitly.</p>
<pre class="language-go"><code class="language-go"><span class="token boolean">_</span> <span class="token operator">=</span> workflow<span class="token punctuation">.</span><span class="token function">ExecuteLocalActivity</span><span class="token punctuation">(</span>
  localCtx<span class="token punctuation">,</span>
  monitorActivity<span class="token punctuation">.</span>CheckInCronMonitor<span class="token punctuation">,</span>
  <span class="token operator">&amp;</span>activities<span class="token punctuation">.</span>CheckInParams<span class="token punctuation">{</span>
    MonitorSlug<span class="token punctuation">:</span> monitorSlug<span class="token punctuation">,</span>
    Status<span class="token punctuation">:</span>      sentry<span class="token punctuation">.</span>CheckInStatusOK<span class="token punctuation">,</span>
    CheckInID<span class="token punctuation">:</span>   checkInID<span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>localCtx<span class="token punctuation">,</span> <span class="token boolean">nil</span><span class="token punctuation">)</span>

<span class="token keyword">return</span> <span class="token boolean">nil</span>
</code></pre>
<p>That closes the loop properly.</p>
<h2>A fuller workflow example</h2>
<p>Here is a more complete sketch that shows the start, failure, and success flow together:</p>
<pre class="language-go"><code class="language-go"><span class="token keyword">func</span> <span class="token function">ReconcileBalances</span><span class="token punctuation">(</span>ctx workflow<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token builtin">error</span> <span class="token punctuation">{</span>
  <span class="token keyword">const</span> monitorSlug <span class="token operator">=</span> <span class="token string">&quot;reconcile-balances&quot;</span>

  <span class="token keyword">var</span> monitorActivity <span class="token operator">*</span>activities<span class="token punctuation">.</span>SentryCronMonitorActivity
  localCtx <span class="token operator">:=</span> workflow<span class="token punctuation">.</span><span class="token function">WithLocalActivityOptions</span><span class="token punctuation">(</span>ctx<span class="token punctuation">,</span> workflow<span class="token punctuation">.</span>LocalActivityOptions<span class="token punctuation">{</span>
    StartToCloseTimeout<span class="token punctuation">:</span> <span class="token number">30</span> <span class="token operator">*</span> time<span class="token punctuation">.</span>Second<span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">)</span>

  <span class="token keyword">var</span> checkInID sentry<span class="token punctuation">.</span>EventID
  <span class="token boolean">_</span> <span class="token operator">=</span> workflow<span class="token punctuation">.</span><span class="token function">ExecuteLocalActivity</span><span class="token punctuation">(</span>
    localCtx<span class="token punctuation">,</span>
    monitorActivity<span class="token punctuation">.</span>CheckInCronMonitor<span class="token punctuation">,</span>
    <span class="token operator">&amp;</span>activities<span class="token punctuation">.</span>CheckInParams<span class="token punctuation">{</span>
      MonitorSlug<span class="token punctuation">:</span> monitorSlug<span class="token punctuation">,</span>
      Status<span class="token punctuation">:</span>      sentry<span class="token punctuation">.</span>CheckInStatusInProgress<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>localCtx<span class="token punctuation">,</span> <span class="token operator">&amp;</span>checkInID<span class="token punctuation">)</span>

  <span class="token keyword">if</span> err <span class="token operator">:=</span> <span class="token function">runCriticalSteps</span><span class="token punctuation">(</span>ctx<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">{</span>
    <span class="token boolean">_</span> <span class="token operator">=</span> workflow<span class="token punctuation">.</span><span class="token function">ExecuteLocalActivity</span><span class="token punctuation">(</span>
      localCtx<span class="token punctuation">,</span>
      monitorActivity<span class="token punctuation">.</span>CheckInCronMonitor<span class="token punctuation">,</span>
      <span class="token operator">&amp;</span>activities<span class="token punctuation">.</span>CheckInParams<span class="token punctuation">{</span>
        MonitorSlug<span class="token punctuation">:</span> monitorSlug<span class="token punctuation">,</span>
        Status<span class="token punctuation">:</span>      sentry<span class="token punctuation">.</span>CheckInStatusError<span class="token punctuation">,</span>
        CheckInID<span class="token punctuation">:</span>   checkInID<span class="token punctuation">,</span>
      <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>localCtx<span class="token punctuation">,</span> <span class="token boolean">nil</span><span class="token punctuation">)</span>

    <span class="token keyword">return</span> err
  <span class="token punctuation">}</span>

  <span class="token boolean">_</span> <span class="token operator">=</span> workflow<span class="token punctuation">.</span><span class="token function">ExecuteLocalActivity</span><span class="token punctuation">(</span>
    localCtx<span class="token punctuation">,</span>
    monitorActivity<span class="token punctuation">.</span>CheckInCronMonitor<span class="token punctuation">,</span>
    <span class="token operator">&amp;</span>activities<span class="token punctuation">.</span>CheckInParams<span class="token punctuation">{</span>
      MonitorSlug<span class="token punctuation">:</span> monitorSlug<span class="token punctuation">,</span>
      Status<span class="token punctuation">:</span>      sentry<span class="token punctuation">.</span>CheckInStatusOK<span class="token punctuation">,</span>
      CheckInID<span class="token punctuation">:</span>   checkInID<span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">Get</span><span class="token punctuation">(</span>localCtx<span class="token punctuation">,</span> <span class="token boolean">nil</span><span class="token punctuation">)</span>

  <span class="token keyword">return</span> <span class="token boolean">nil</span>
<span class="token punctuation">}</span>
</code></pre>
<p>This is intentionally boring code, and that is a good thing. Monitoring code should be boring.</p>
<h2>A few implementation choices I would make again</h2>
<p>After using this pattern, there are a few choices I would repeat without hesitation.</p>
<h3>1. Start with critical schedulers only</h3>
<p>Do not try to instrument every recurring workflow on day one.</p>
<p>Start with the jobs that actually matter:</p>
<ul>
<li>anything tied to customer balances</li>
<li>anything that drives money movement</li>
<li>anything that feeds reporting or downstream systems</li>
<li>anything that will create painful cleanup if it quietly stops</li>
</ul>
<p>You will get more value from five well-monitored critical schedulers than from fifty noisy low-value ones.</p>
<h3>2. Use human-readable monitor slugs</h3>
<p>Do not make the slug clever.</p>
<p>Use something obvious, stable, and searchable:</p>
<pre class="language-text"><code class="language-text">collect-daily-balances
sync-ledger-entries
reconcile-wallet-transactions
</code></pre>
<p>When an alert fires, the person reading it should understand what broke without decoding naming trivia.</p>
<h3>3. Reuse the same check-in ID</h3>
<p>This is easy to get wrong and worth repeating.</p>
<p>The first <code>in_progress</code> check-in gives you an ID. Keep it. Use it again when sending <code>ok</code> or <code>error</code>.</p>
<p>That is how Sentry knows all of those status changes belong to the same scheduled run.</p>
<h3>4. Keep cron monitoring separate from business error handling</h3>
<p>Cron monitoring tells you whether the scheduled run happened and how it ended.</p>
<p>It does <strong>not</strong> replace your normal error capture, structured logs, or domain-level alerts.</p>
<p>I still want:</p>
<ul>
<li>captured exceptions</li>
<li>application logs</li>
<li>traces</li>
<li>domain-specific alerting where needed</li>
</ul>
<p>Cron monitoring is one layer. A useful one, but still one layer.</p>
<h3>5. Wire alerts somewhere people actually watch</h3>
<p>Monitoring without response is decoration.</p>
<p>If the signal matters, route it to a place where the team will actually see it. Slack is the usual answer for many teams, and that was a big part of the value for us internally too.</p>
<h2>Why I like this pattern</h2>
<p>I like this pattern because it is small, explicit, and useful.</p>
<p>It does not pretend Temporal is insufficient. Temporal is great. It just accepts a reality that shows up in every production system eventually: durable execution and scheduler visibility are related, but they are not the same thing.</p>
<p>You still need a clear operational signal that says:</p>
<ul>
<li>this recurring job started</li>
<li>it finished</li>
<li>it failed</li>
<li>or it never showed up when it was supposed to</li>
</ul>
<p>That is the gap Sentry Cron Monitoring closes nicely.</p>
<h2>Where this helps the most</h2>
<p>I think this pattern is most useful in services where recurring workflows have business consequences beyond simple housekeeping.</p>
<p>For example:</p>
<ul>
<li>balance aggregation</li>
<li>statement generation</li>
<li>repayment schedules</li>
<li>reconciliation jobs</li>
<li>settlement pipelines</li>
<li>compliance or reporting exports</li>
</ul>
<p>In those cases, “we can inspect workflow history later” is not an operational strategy. You want to know quickly when the scheduler itself is unhealthy.</p>
<h2>Final thoughts</h2>
<p>I do not think every observability improvement needs to be dramatic to be valuable.</p>
<p>This one certainly was not dramatic. It was a small addition to an existing stack:</p>
<ul>
<li>Temporal for recurring workflows</li>
<li>Sentry for visibility</li>
<li>alert routing for fast response</li>
</ul>
<p>But it solved a real production problem: critical schedulers should not fail quietly.</p>
<p>That is the standard I care about.</p>
<p>If you already run important Temporal schedulers in Go and you are only relying on errors or dashboard checks, I think Sentry Cron Monitoring is worth adding. The implementation is small, the signal is useful, and the operational payoff is immediate.</p>]]></content:encoded>
            <author>hello@a4m.dev (Akeem Amusat)</author>
        </item>
        <item>
            <title><![CDATA[Unleashing the Power of Generators in Python - Memory-Efficient Code That Flows Effortlessly]]></title>
            <link>https://a4m.dev/articles/unleashing-the-power-of-generators-in-python</link>
            <guid>https://a4m.dev/articles/unleashing-the-power-of-generators-in-python</guid>
            <pubDate>Tue, 17 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Learn how Python generators can help you write memory-efficient code that handles large datasets with ease. Discover yield keyword, generator expressions, and real-world applications.]]></description>
            <content:encoded><![CDATA[<p>Tired of memory-hungry code that slows down your Python projects?
Meet generators, your key to crafting efficient, elegant, and performant programs. Sometimes we want to write applications that load very large amounts of data or have a complex function that needs to maintain an internal state every time it is called.</p>
<p>In this whirlwind tour, we&#x27;ll explore:</p>
<ul>
<li>What they are and how they work their magic</li>
<li>Creating generators with the yield keyword</li>
<li>Mastering generator expressions for concise iterables</li>
<li>Real-world scenarios where generators shine</li>
</ul>
<p>Ready to dive in?</p>
<h2>Unlocking the Secrets of Generators</h2>
<p>Think of generators as magical iterators that produce values on demand, rather than storing an entire sequence in memory.</p>
<p>This means:</p>
<ul>
<li>Memory efficiency: Ideal for handling large datasets or infinite sequences.</li>
<li>Performance optimization: Streamline data processing for smoother operations.</li>
<li>Elegant code: Create readable and concise iterables with ease.</li>
</ul>
<h2>Creating a Generator: The yield Keyword</h2>
<p>To craft a generator function, employ the <code>yield</code> keyword:</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">simple_generator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token keyword">yield</span> <span class="token number">1</span>
    <span class="token keyword">yield</span> <span class="token number">2</span>
    <span class="token keyword">yield</span> <span class="token number">3</span>

gen <span class="token operator">=</span> simple_generator<span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">next</span><span class="token punctuation">(</span>gen<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token comment"># Output: 1</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">next</span><span class="token punctuation">(</span>gen<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token comment"># Output: 2</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">next</span><span class="token punctuation">(</span>gen<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token comment"># Output: 3</span>
</code></pre>
<p>Generators are excellent for representing infinite sequences, thanks to their lazy evaluation. For example, a generator that produces an infinite sequence of Fibonacci numbers:</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">fibonacci_generator</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    a<span class="token punctuation">,</span> b <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">1</span>
    <span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span>
        <span class="token keyword">yield</span> a
        a<span class="token punctuation">,</span> b <span class="token operator">=</span> b<span class="token punctuation">,</span> a <span class="token operator">+</span> b

fib <span class="token operator">=</span> fibonacci_generator<span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">next</span><span class="token punctuation">(</span>fib<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token comment"># Output: 0</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">next</span><span class="token punctuation">(</span>fib<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token comment"># Output: 1</span>
<span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">next</span><span class="token punctuation">(</span>fib<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token comment"># Output: 1</span>
</code></pre>
<h2>Generators in Action: Real-World Scenarios</h2>
<p>Generators excel in various contexts, including:</p>
<ul>
<li>Processing large files: Read and process data chunk by chunk, saving memory.</li>
<li>Implementing custom iterators: Create unique iterables tailored to your needs.</li>
<li>Building infinite sequences: Generate values endlessly (e.g., Fibonacci numbers).</li>
<li>Pipelining data: Connect generators for efficient data flow.</li>
<li>Coroutines in asynchronous programming: Manage concurrent tasks effectively.</li>
</ul>
<h3>Simple CSV processing</h3>
<p>Suppose we have a CSV file of size 2 Gigabytes, how do you think we can open/read the file in our application? Well, if you think we can just us the <code>open()</code> function and read the file contents, then you maybe wrong. This approach requires a lot of system resources to open and read the CSV file into memory. Below is a code sample to read a CSV file using <code>with</code> context keyword and <code>yield</code> keyword.</p>
<pre class="language-python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">read_csv</span><span class="token punctuation">(</span>filename<span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>filename<span class="token punctuation">,</span> <span class="token string">&#x27;r&#x27;</span><span class="token punctuation">)</span> <span class="token keyword">as</span> <span class="token builtin">file</span><span class="token punctuation">:</span>
        csv_reader <span class="token operator">=</span> csv<span class="token punctuation">.</span>reader<span class="token punctuation">(</span><span class="token builtin">file</span><span class="token punctuation">)</span>
        <span class="token builtin">next</span><span class="token punctuation">(</span>csv_reader<span class="token punctuation">)</span>  <span class="token comment"># Skip header</span>
        <span class="token keyword">for</span> row <span class="token keyword">in</span> csv_reader<span class="token punctuation">:</span>
            <span class="token keyword">yield</span> row
</code></pre>
<p>This way, we&#x27;re not loading the whole file into memory. Instead, our generator is returning the next row in the file.</p>
<h2>Advantages of Generators:</h2>
<ul>
<li>Memory efficiency</li>
<li>Performance optimization</li>
<li>Concise iterables</li>
<li>Lazy evaluation</li>
<li>Flexibility for custom iterators</li>
<li>Handling infinite sequences</li>
<li>Pipelining data</li>
<li>Asynchronous programming (coroutines)</li>
<li>Embrace generators, and watch your Python code soar to new levels of efficiency and elegance!</li>
</ul>
<h2>Conclusion</h2>
<p>Generators in Python provide a powerful tool for handling large datasets, performing lazy evaluation, and optimizing memory usage. By incorporating generators into your code, you can enhance its efficiency, readability, and scalability. Whether you&#x27;re processing files, dealing with infinite sequences, or performing complex calculations, generators offer an elegant and resource-efficient solution. Consider integrating generators into your Python projects to unlock their potential and streamline your code.</p>]]></content:encoded>
            <author>hello@a4m.dev (Akeem Amusat)</author>
        </item>
    </channel>
</rss>