The One Git Command I Use Every Single Day (And It’s Not git push)

I see junior developers pushing code to main directly. I did the same thing for way too long.

Now I use one command more than any other:
git rebase --interactive HEAD~3

Before I push any branch, I rebase my last 3 commits and squash the “fix typo” and “wip” commits into one clean commit.

This changes how I work:

  • My commit history is readable (not a mess of “oops” messages)
  • My PRs are smaller and easier to review
  • I think more carefully before I commit

It’s not a flashy skill. But it’s the one that made me look more professional than any framework I learned.

What’s one Git command or workflow that made you feel like you actually knew what you were doing?

My full stack (and my clean commit history): My Portfolio

How I Built an AI Architecture Visualizer in 8 Hours (And Bypassed GitHub API Limits)

Tired of reading messy codebases? Meet AETHER. —

Have you ever joined a new project, looked at the repository, and realized you’re going to lose the next three days just trying to figure out how the files connect?

Yeah, me too.

As a Full-Stack developer, I got tired of manually mapping out data flows and component hierarchies. So, for the Baidu “Build with MeDo” Hackathon, I built AETHER: an AI-powered Developer Onboarding & Scoping Assistant.

AETHER takes any public GitHub repository and instantly maps its entire architecture using AI-generated interactive Mermaid.js diagrams.

Here is how I built it without writing custom backend code, and how I bypassed some major LLM and API bottlenecks.

⚡ What AETHER Actually Does

I didn’t just want a passive visualizer; I wanted an active workflow tool for the Work & Productivity track. AETHER has three main features:

1. Instant Architecture Mapping

You paste a GitHub URL, and AETHER instantly generates an interactive visual graph of the codebase. To avoid visual noise on massive repos, it includes a Context Pruning filter so you can isolate just the Frontend, Backend, or Database models.

2. “Impact Analysis” Mode (My Favorite Feature)

If you are scoping a sprint, you can type a feature request like “Migrate database to PostgreSQL”. AETHER runs an Impact Analysis, highlighting the exact nodes/files on the diagram in orange that need to be modified, alongside a step-by-step implementation checklist.

3. One-Click Auto-Documentation

Productivity tools need tangible deliverables. With one click, AETHER takes its context and generates a comprehensive, formatted ARCHITECTURE.md file that you can immediately download and commit to your repo.

🛠️ How I Built It (The MeDo Magic)

AETHER was built entirely using MeDo, Baidu’s AI application builder. Because I understand full-stack architecture, I treated MeDo like an orchestrator rather than a simple chatbot.

I wired together specific MeDo Skills to handle the heavy lifting:

  1. HTTP Request Skill: Safely communicates with the GitHub API to fetch recursive file trees.
  2. Data Processing Skill: Cleans the payload by stripping out heavy folders like node_modules and binaries. This is crucial—if you send a raw Node/Python project to an LLM, you will instantly blow up the context window.
  3. File Generator Skill: Compiles the AI’s analysis into the final, downloadable .md file.

Overcoming the Bottlenecks

Building AI wrappers is easy. Building a reliable AI workflow is hard. I ran into two major roadblocks that I had to engineer my way out of:

1. The GitHub API Rate Limit Wall
Unauthenticated GitHub API requests limit you to 60 calls per hour. AETHER burns through that instantly.

  • The Fix: I instructed MeDo to build a secure UI modal where users can input their own GitHub Personal Access Token (PAT). MeDo saves this locally and injects it into the HTTP Request header, bumping the limit to 5,000 requests an hour.

2. The Mermaid Syntax Hallucinations
LLMs are notoriously bad at finishing long blocks of code. AETHER kept crashing because the AI would hit its Output Token Limit mid-generation, leaving unclosed brackets that broke the Mermaid.js client renderer.

  • The Fix: I engineered highly strict system prompts forcing the AI to optimize tokens by grouping minor files, dropping spaces in subgraph IDs, and I maximized the token ceiling in MeDo’s settings to guarantee flawless execution.

🚀 Try It Out

Building this for the #BuiltWithMeDo hackathon was an incredible experience in “Vibe Coding” and spec-driven development.

Vote Me here

If you test it on one of your repositories, let me know how accurately it maps your architecture in the comments below! 👇

GDPR Audit Automation: 5 Compliance Checks You Are Probably Missing

GDPR has been enforceable since 2018, yet enforcement actions keep increasing year after year. The problem isn’t that developers don’t care — it’s that most compliance checks happen once, at launch, and then get forgotten. Here are five critical GDPR requirements that slip through the cracks on most SaaS products.

1. Data Processing Register (ROPA)

The GDPR requires all organisations processing personal data to maintain a Record of Processing Activities (Article 30). Most developers have never heard of it. Your ROPA must document:

  • What data you collect and why
  • The legal basis for processing (consent, legitimate interest, contract)
  • Data retention periods
  • Third-party processors (AWS, Stripe, Mixpanel — every one)
  • Cross-border data transfers

The fine for not having one: up to €10M or 2% of global turnover.

2. Data Subject Request Automation

Under GDPR, users have the right to access, rectify, erase, and port their data — within 30 days. Most SaaS products handle these manually (or ignore them entirely). At scale, this becomes unmanageable.

// Minimum viable DSR handler
app.post('/api/dsr/erasure', authenticate, async (req, res) => {
  const userId = req.user.id;

  // Must delete from ALL systems — not just your main DB
  await Promise.all([
    db.users.delete(userId),
    analyticsService.deleteUser(userId),
    emailService.unsubscribeAll(userId),
    backups.scheduleDataPurge(userId), // often forgotten
  ]);

  res.json({ status: 'processing', deadline: addDays(new Date(), 30) });
});

3. Legitimate Interest Assessment (LIA)

“Legitimate interest” is the most used (and most abused) legal basis for data processing. Using it correctly requires a three-part balancing test: purpose test, necessity test, and balancing test. Using it incorrectly — for marketing without consent, for example — is a violation.

4. Cookie Consent That Actually Works

A cookie banner that says “We use cookies” with a single OK button is not GDPR-compliant. Compliant consent requires:

  • Granular categories (functional, analytics, marketing)
  • Equal ease of accepting vs rejecting
  • No pre-ticked boxes
  • Stored consent records with timestamp and version
  • Re-consent when purposes change

5. Vendor Due Diligence

Every third-party service your app touches that handles personal data is a “data processor” under GDPR. You need:

  • A signed Data Processing Agreement (DPA) with each
  • Documented transfers under Article 46 (SCCs for US vendors)
  • A way to revoke access if they’re breached

Common oversight: using npm packages that phone home (analytics, error tracking, fonts) without documenting them.

Automating the Audit

Running these checks manually is error-prone and time-consuming. Tools like CompliPilot automate 200+ compliance checks across GDPR, HIPAA, CCPA, and NIS2 — giving you a scored audit report in under 60 seconds, with specific remediation steps for each finding.

The goal isn’t perfect compliance overnight. It’s knowing exactly where your gaps are so you can prioritise the highest-risk issues first.

I decided to build a Kubernetes alternative. Yes, I know I’m crazy

Using Kubernetes is a love-hate relationship.

Love, because before it, deploying to production was something dark and uncertain. Many tools tried to solve this problem — Mesos, Fleet, Nomad — but only Kubernetes meaningfully solved container orchestration at scale. It’s not without merit that today over 96% of organizations using containers run Kubernetes, according to the 2023 CNCF report.

But as nothing is perfect, a piece of software built by Google engineers fell short on design — not UI design, but how things actually work. Engineers think like engineers: it works, but Kubernetes complexity is something you’ll likely need a dedicated course to truly understand. On one side it created entire careers — the CKA certification became a market commodity — on the other, small companies that can’t afford a certified DevOps engineer find themselves at a crossroads: they feel the need for Kubernetes but can’t manage it.

The concrete problem: a minimal Kubernetes cluster requires understanding at least 15 different abstractions — Pods, Deployments, Services, Ingress, ConfigMaps, Secrets, RBAC, PVCs, StorageClasses, NetworkPolicies, and so on. And incredibly, with each release that number grows. Kubernetes 1.29 introduced over 60 API changes. In 2024, more than 10 APIs that projects depended on were deprecated.
Several projects tried to reduce this complexity. Rancher added a solid UI layer on top of Kubernetes. Dokploy, which doesn’t use Kubernetes but Docker Swarm instead, achieved something remarkable: the first time I used it, I had a project running in production in minutes, no course needed, no major concerns. Their UX is genuinely good.

But I have one problem with Dokploy: it uses Docker Swarm. And while Swarm is still technically maintained by Docker Inc., development has slowed dramatically in favor of Kubernetes-based solutions, the ecosystem stopped investing in it, and adoption has shrunk. It’s a functionally stagnant project — which for critical infrastructure is a real risk.

HashiCorp’s Nomad tried a different path — simpler than Kubernetes, single binary, without all the ceremony. But in my opinion it made the mistake of being too tightly coupled to the HashiCorp ecosystem: Consul for service discovery, Vault for secrets, Terraform for infrastructure. It works great if you adopt everything. If you don’t, it’s limiting. And now, after HashiCorp was acquired by IBM in 2024 and changed its license from MPL to BSL, that dependency became even riskier for anyone building on top of it.

Something about me you should know: I love studying anything related to IT. Beyond being my profession, it’s a hobby. And I like giving meaning to what I study.

With that said, I recently started a personal project. I have three VPS instances, started analyzing what I’d need, and realized I didn’t need the full complexity of the Kubernetes stack. I went with Dokploy — it works fine for an MVP. But the question remained: what happens when it grows?

So one day, in the shower, reflecting on all of this, I thought: I can use Dokploy while it’s an MVP, I can take a Kubernetes course and hope the AI configures everything correctly, or I can build a real alternative. I went with the obvious choice: build the alternative.

You might think I’m crazy. But Kubernetes itself was born from Google engineers who thought they could do better than Borg. Docker was born from someone who thought LXC could be simpler. Real projects emerge when people decide to think differently about known problems.

My goal isn’t fame or money. The project will be 100% open source, Apache 2 license, no paid tiers. What I want is to solve a real problem: unnecessary complexity in container orchestration. I’m not promising I’ll succeed — but it’s worth trying.

I’ve been working on this for a few weeks now. The repository is still private, but soon, once I have a more stable version, I’ll make it public. In the next posts I’ll detail the architecture decisions and the reasoning behind them — including why I built my own orchestrator instead of layering on top of Kubernetes.

If you also feel that Kubernetes complexity is out of proportion with the problem it solves, follow along. I’ll probably make a lot of mistakes — but I’ll document everything.

I think the real AI agent war is who owns your inbox, browser, and calendar

I went into a Reddit rabbit hole expecting the usual argument:

  • GPT-5 vs Claude Opus
  • Gemini vs DeepSeek
  • hosted vs local
  • benchmark chart of the week

That is not what serious agent users are arguing about.

After reading a big r/openclaw thread and digging through real setups, I think the actual battle is much simpler:

Who owns the workflow surface area?

Not who has the smartest model on a leaderboard.

Who controls the places where work actually happens:

  • inbox
  • calendar
  • docs
  • browser
  • chat
  • internal tools
  • task state
  • permissions

If you build agents for real work, that question matters more than another 2% on an eval.

The useful mental model: models are commodities, surfaces are moats

A model can be swapped.

A workflow surface is much harder to displace.

If an agent already has trusted access to Gmail, Google Docs, Calendar, Drive, Meet, Search, and Android, it starts with a huge advantage. It can read, draft, schedule, search, notify, and follow up without asking the user to glue together 12 APIs.

That is why Google is dangerous in agents.

Not because Gemini is magical.

Because Google already sits where work happens.

On the other side, tools like OpenClaw are winning a different way: not by owning the surface, but by giving you access to more of it.

That means:

  • Telegram as the control plane
  • browser automation for messy tasks
  • local model support
  • direct access to internal dashboards
  • persistent memory across projects
  • custom routing between multiple models

That is a very different product strategy.

And honestly, for a lot of developers, it is the more interesting one.

What I saw in real user setups

The best part of these Reddit threads was that people were not talking in abstractions.

They were posting actual stacks.

One user described a setup on a Mac Mini M4 with:

  • GPT-5 via OAuth
  • Telegram as the primary interface
  • memory and workflow routing
  • project-specific threads
  • a second framework running as a sandbox

That is not a “which model is smartest?” setup.

That is an orchestration setup.

The clever detail was using one Telegram group with just the user and bot, then creating a topic per project so each conversation becomes its own working session.

That is scrappy, weird, and very good.

Big vendors usually do not ship weird-first.

Open ecosystems do.

The thing that breaks first in production is not intelligence

It is cost and reliability.

That was the most practical takeaway from the threads.

People were not mainly complaining about model quality.

They were complaining about agents wasting money on dumb work:

  • heartbeat checks
  • cron-triggered polling
  • status checks
  • retries after browser failures
  • re-reading context that did not need premium reasoning

This is where a lot of “agent demos” fall apart when you run them all day.

The expensive part is often not the hard reasoning step.

It is the junk around the edges.

A simple routing rule beats a stronger model used everywhere

A lot of teams still treat model selection like this:

model: claude-opus

That is easy.

It is also how you end up with a giant bill for work that did not need Claude Opus.

A more realistic setup looks like this:

agent_routing:
  heartbeat_checks: glm-5.1
  cron_pings: glm-5.1
  browser_research: claude-sonnet-4.6
  hard_reasoning: gpt-5.4
  local_private_tasks: qwen-3.6-27b

That routing layer is boring compared to model launch drama.

It is also where your economics live.

If your agent runs 24/7, the difference between “best model everywhere” and “best model where it matters” is the difference between a toy and a system.

Closed ecosystems vs open ecosystems

I do not think one side wins completely.

I think the stack is splitting into two categories.

Category What wins
Closed ecosystem agents Native access, trust, convenience, polished UX, enterprise-friendly permissions
Open ecosystem agents Browser control, local models, custom workflows, internal tools, weird glue code, faster experimentation
Durable advantage Integrations, memory, routing, retries, permissions, and action surfaces

If you need:

  • Gmail
  • Calendar
  • Docs
  • Meet
  • Android notifications
  • admin-friendly controls

then a Google-style ecosystem is hard to beat.

If you need:

  • browser automation
  • Telegram control
  • local Qwen or Llama fallback
  • weird CRM integrations
  • direct database access
  • internal admin panels
  • custom long-running workflows

then the open side gets very attractive very quickly.

Why developers should care about “workflow surface area”

Because this changes how you should build agents.

If you are still thinking mostly in terms of “pick one best model,” you are optimizing the wrong layer.

The real architecture questions are more like:

  1. Where does the agent live?
  2. What systems can it touch?
  3. What state does it persist?
  4. Which steps need premium reasoning?
  5. Which steps can be cheap?
  6. What happens when browser automation fails?
  7. How do you keep it running without babysitting cost?

That is the practical agent stack.

Not just prompts. Not just evals. Not just model preference.

A more realistic agent architecture

This is closer to what production looks like than most AI demos:

Interface: Telegram / Slack / email
Memory: shared vector store + task state
Routing: cheap model for routine, premium model for edge cases
Actions: browser, docs, calendar, CRM, internal tools
Fallbacks: local Qwen or Llama when cloud access is blocked
Observability: logs, retries, alerts, usage tracking

That is not a chatbot.

That is an operating layer.

The cost problem nobody wants to admit

A lot of teams are quietly discovering that agent economics are still ugly.

A Zapier AI agent that runs a few times a day can survive sloppy orchestration.

A real always-on agent cannot.

If your system is constantly:

  • polling APIs
  • checking inboxes
  • re-summarizing threads
  • retrying browser steps
  • escalating everything to the most expensive model

then your architecture is broken, even if your demo looked great.

This is exactly why predictable compute matters more for agents than for casual chat.

Agents are persistent. They loop. They retry. They watch things. They perform background work.

That means per-token billing gets painful fast.

Not because one single request is expensive.

Because the system never really stops.

What I would build differently after reading all this

If I were designing an agent stack today, I would optimize in this order:

1. Own the interface

Pick the place users already live.

Examples:

  • Slack
  • Telegram
  • email
  • browser extension
  • internal ops dashboard

2. Minimize premium model usage

Use the strongest model only where failure is expensive.

Examples:

  • contract review
  • ambiguous browser recovery
  • long-horizon planning
  • code generation with side effects

3. Treat orchestration as the product

Your moat is not just the model.

It is the combination of:

  • memory
  • routing
  • retries
  • permissions
  • tool access
  • state management

4. Design for long-running economics

Assume the agent runs all day.

If the cost model only works in a demo, it does not work.

A concrete dev setup

If you are experimenting with agents right now, a practical stack might look like this:

# example services
agent-api
worker
browser-runner
memory-store
postgres
redis
telegram-bot
observability

And your routing layer might be as simple as:

function pickModel(taskType: string) {
  switch (taskType) {
    case "heartbeat":
    case "polling":
      return "glm-5.1";
    case "browser_research":
      return "claude-sonnet-4.6";
    case "hard_reasoning":
    case "critical_planning":
      return "gpt-5.4";
    case "private_local_task":
      return "qwen-3.6-27b";
    default:
      return "gpt-5.4";
  }
}

That one function can matter more than a week of benchmark discourse.

The boring infrastructure question that becomes the whole business

Once agents move from demos to production, you start caring about things that are not fun to tweet about:

  • request routing
  • concurrency
  • throttling
  • retries
  • batching
  • model fallback
  • cost ceilings
  • always-on usage patterns

This is also where a lot of teams hit a wall with standard API pricing.

Per-token billing is tolerable when humans are manually prompting.

It gets much worse when software is prompting constantly.

That is why “unlimited compute for agents” is not just a pricing gimmick. It changes what you are willing to automate.

If your n8n, Make, Zapier, OpenClaw, or custom agent workflow has to think about token spend on every loop, you end up designing around fear.

If the cost is predictable, you design around throughput and reliability instead.

That is a much better place to build from.

For teams running long-lived automations, this is the part worth paying attention to: a drop-in OpenAI-compatible API with flat monthly pricing means you can keep the orchestration logic you already have, but stop treating every background task like a billing risk.

That is the practical appeal of Standard Compute.

My take

The next AI agent war is not primarily Gemini vs GPT-5 vs Claude.

That fight matters, but it is downstream.

The upstream fight is:

Who gets to sit on top of your daily workflow surfaces?

If Google owns the trusted surfaces, it will be brutally strong.

If tools like OpenClaw keep owning the weird workflows, they will keep attracting the most inventive users.

And the teams that win will not just have good models.

They will have:

  • the best access to real work surfaces
  • the best orchestration layer
  • the best routing logic
  • the best memory and tool control
  • a cost structure that lets agents stay on all day

That last part is still underrated.

Model intelligence matters.

But for real agents, workflow gravity matters more.

How to Measure a Ship’s CO Emissions From Land

A practical walkthrough of measuring vessel CO₂ emissions via API — what EEXI actually means, why fuel type changes everything, and how to verify a number before you trust it.

How to Measure a Ship’s CO₂ Emissions From Land

Here’s a question that sounds simple until you try to answer it: how much CO₂ did that container ship just emit on its way from Shanghai to Rotterdam?

A practical walkthrough of measuring vessel CO₂ emissions via API — what EEXI actually means, why fuel type changes everything, and how to verify a number before you trust it.

You can see the ship. You know roughly where it went, and roughly how fast. You can look up its size. And yet the honest answer — the one a regulator or a carbon accountant would actually accept — requires you to know things about that vessel that aren’t printed on the hull. What fuel was in its tanks. What its engines were designed to do at three-quarters load. Whether its propeller has been polished this year. Whether the paint on its hull is the slick anti-fouling kind or the kind that’s currently hosting a small ecosystem of barnacles.

This tutorial is about closing that gap. We’re going to walk through how to ask our /emissions endpoint for the CO₂ output of a single vessel, and — more interestingly — how to understand what the API is actually doing under the hood. Because if you’re going to make a decision based on a number, you should know where the number came from.

The thing everyone gets wrong

Most people assume ship emissions are calculated the way car emissions are: you burn X litres of fuel, each litre contains Y grams of carbon, multiply, done.

That’s roughly right for a car. It falls apart for ships almost immediately.

The IMO does publish carbon factors for each marine fuel type, and they’re as clean as you could want. For heavy fuel oil (HFO), the number is 3.114 grams of CO₂ per gram of fuel burned. For VLSFO — the blended low-sulphur product most large vessels burn today — it’s 3.151. For marine gas oil (MGO), 3.206. For LNG, the direct combustion factor is 2.750. These aren’t interchangeable. Submitting an MRV report with the HFO factor against a tank of VLSFO will fail verification, which is exactly the kind of thing I got wrong the first time I tried to reconcile a vessel’s annual fuel mix against its reported emissions. The numbers look close. They aren’t.

If you knew exactly how much of each fuel a ship had burned, you’d be done. The problem is that almost nobody outside the ship does. Aggregate fuel consumption is reported annually under the IMO Data Collection System; per-voyage data is collected by the EU MRV regime for vessels calling at European ports, but it isn’t publicly accessible. If you want per-voyage emissions from the outside, you have to estimate them — working backwards from what the ship was designed to burn, and adjusting for what it probably actually did.

The starting point for that estimate is a three-letter acronym you’ll see everywhere in this domain.

What EEXI actually is

EEXI stands for Energy Efficiency Existing Ship Index. The name sounds like it was designed to deter questions, so let’s ignore it.

What EEXI actually is: a fuel economy rating. It’s the mpg sticker on the window, except the window is a 300-metre container ship and the sticker is buried in a classification society database in Hamburg.

For most existing vessels of 5,000 gross tonnes and above — broadly, the ships big enough to do international trade and already in service when the regulation took effect on 1 January 2023 — the IMO requires a calculation that answers a single question: if this ship sailed at the speed corresponding to 75% of its maximum continuous engine rating, in calm weather, in a defined reference state, how many grams of CO₂ would it emit per tonne of cargo per nautical mile?

That’s it. Standardised conditions, one number, comparable across vessels. A modern, efficient large bulk carrier might come in around 3 g CO₂/tonne-nm. An older, smaller one might be roughly double that. Container ships sit higher again because their capacity is measured differently. Lower is better, the way fewer litres per 100km is better on a car.

EEXI on its own doesn’t tell you what a ship emitted yesterday. But combined with a known route, a speed profile, and a load factor, it lets you build a credible estimate without ever needing the captain’s fuel logs.

The request

Let’s measure something. Here’s a minimal call for a vessel by IMO number:

curl "https://api.vesselapi.com/v1/emissions?imo=9395044" 
  -H "Authorization: Bearer YOUR_API_KEY"

And here’s the relevant chunk of the response, trimmed for clarity. The values are illustrative — they show the shape of the response, not certified figures for this vessel:

"emissions": {
  "eexi": {
    "attained": 6.82,
    "required": 7.84,
    "unit": "g_co2_per_tonne_nm",
    "compliant": true
  },
  "fuel": {
    "primary": "VLSFO",
    "eca": "MGO",
    "carbon_factor_primary": 3.151,
    "carbon_factor_eca": 3.206
  },
  "verifier": {
    "name": "DNV",
    "verified_at": "2023-04-12",
    "method": "approved_calculation"
  }
}

The most important field here is probably the one you’d skip past on first reading: verifier. We’ll come back to it. First, the things this response is telling you about the ship itself.

EEXI arrives as a pair — attained and required. EEXI is an efficiency index, so lower is better, and required is a ceiling: the maximum value the IMO will accept for this ship type and size. The vessel is compliant if attained sits at or below that ceiling. If it doesn’t, the primary remedy is an Engine Power Limitation: a technical and administrative measure that caps engine output and brings the attained index down. Switching fuels operationally doesn’t change EEXI itself — EEXI is a design-condition index, fixed at certification. Fuel choices affect the operational metric (CII) instead. A ship that can’t demonstrate EEXI compliance can’t get its certificate endorsed, which means in practice it can’t legally trade. It isn’t really a menu.

The fuel block reports two carbon factors because most large vessels switch fuels at ECA boundaries — the North Sea, the Baltic, the US and Canadian coasts, and (from 1 May 2025) the Mediterranean. VLSFO on the open ocean, MGO or another compliant blend inside the zone. For a voyage that crosses an ECA boundary, you need to know which fuel was burning where.

Why the verifier matters more than the number

An emissions number without a verifier is a rumour.

I mean this practically. EEXI calculations are prepared by the ship’s owner or technical manager and submitted to a classification society — DNV, ABS, Lloyd’s Register, ClassNK, Bureau Veritas. These bodies do the actual checking: engine specs, hull form, propulsion train, sea margin assumptions, the lot. They either approve the calculation or send it back. The number that survives this process is the only one that means anything in a regulatory context.

When the API returns a verifier block, it’s saying: this isn’t our estimate. This is the number a recognised classification society signed off on, with their reputation attached. If the verifier field is missing, or shows "method": "estimated", you’re looking at a calculated approximation. Most APIs don’t surface this distinction at all, which is how an estimated figure ends up in someone’s audited disclosure.

For the EU ETS, which began applying to shipping in 2024 (40% of verified emissions surrendered that year, 70% in 2025, 100% from 2026), or for CSRD disclosures, or for any scope 3 reporting that has to survive contact with an auditor — the verified number is the one that matters.

From EEXI to an actual voyage

EEXI gives you grams of CO₂ per tonne-nautical-mile under reference conditions. To get the emissions of an actual voyage, the back-of-envelope multiplication is:

voyage_emissions = EEXI × cargo_tonnes × distance_nm × correction_factor

This formula works, with friction. EEXI is calibrated to a defined reference state, which for bulk carriers and tankers is 70% of deadweight but for container ships is a TEU-based capacity figure — not a deadweight fraction at all. Check the IMO guidelines for the vessel class before applying a load correction. The cargo_tonnes you plug in should reflect actual cargo on board, because a container ship sailing 60% laden carries 60% of the cargo but burns considerably more than 60% of the fuel. And the formula degenerates on a ballast leg: zero cargo gives zero emissions, which is physically impossible. Ballast voyages need a displacement-based substitution or allocation back to the laden leg.

The correction factor is where the rest of the honesty lives. Real voyages aren’t reference conditions. Heavy weather adds fuel. Slow steaming subtracts it — dramatically. For full-form displacement vessels, fuel consumption scales with speed at an exponent typically between 2.7 and 3.5, with the cube as the standard admiralty anchor. That non-linearity is why dropping container ship service speeds from around 21 knots toward 14 was, by most accounts, the single biggest operational lever the industry pulled in the last decade. Not technology. Just patience, and the willingness to leave port a day earlier.

For a rough estimate, a correction factor between 1.1 and 1.3 covers most container and bulk voyages in fair-to-moderate conditions. For anything precise, you want AIS-derived speed profiles for the actual voyage — which is the bridge between the /emissions endpoint and the /positions endpoint. Combine the two and the error band tightens, though the residual error depends heavily on voyage type and the quality of the load assumption.

What this still doesn’t tell you

EEXI is a design-condition metric. It doesn’t capture hull fouling — industry studies suggest six months of moderate fouling can add roughly 10% to fuel burn, and severely fouled hulls have been measured at 30% or worse. It doesn’t capture weather routing decisions. It doesn’t capture auxiliary engines running in port, which on a large container ship is not a rounding error. And it doesn’t capture methane slip from LNG carriers, which deserves a paragraph of its own.

Methane slip is uncombusted natural gas escaping through the engine. It’s a separate problem from the LNG carbon factor, and it doesn’t show up in EEXI at all. Fossil methane carries a GWP₁₀₀ of roughly 30 per the most recent IPCC figures (the exact value depends on the assessment report and whether you include climate-carbon feedbacks), so a few percent of slip can erase the apparent climate advantage of switching from oil to gas. Two-stroke high-pressure LNG engines slip very little; some four-stroke low-pressure designs slip enough to matter. The API returns a CO₂-only figure and flags LNG vessels separately, because rolling slip into a single number requires picking a GWP horizon, and that choice is a political one as much as a scientific one.

So: where does that leave you? If you’re filing EU ETS reports, the verified EEXI number is where you start and your auditor decides whether you need to go further. If you’re building a product that surfaces emissions to end users, a well-bounded estimate is almost certainly fine and most users won’t know the difference. If you’re trying to prove in a contract dispute that a specific voyage exceeded a specific threshold — call a naval architect, not an API.

At sea, with no fuel pump to read and no tailpipe to sniff, the gap between what was burned and what we can prove was burned is wider — and stranger — than most people realise.

I Used AI for Code Review on a Production ERP for 6 Months. Here’s Where It Actually Failed Me.

Six months ago I started running every non-trivial piece of code through AI before it shipped. Not prototypes — real ERP and CRM modules with paying clients on the other end. Batch processing tables, MRP allocation logic, dynamic invoice builders, real-time dashboards.

Here’s what I found out.

What AI is actually good at

Duplicate functions. In a codebase that’s been touched by multiple devs over months, this is AI’s most reliable win. It flagged things like: “This mirrors formatCurrency in utils/formatters.ts” — the kind of thing that slips through human review because everyone assumes someone else already checked.

Calculation bugs in self-contained utilities. Off-by-one errors, wrong operator precedence, bad unit conversions — if the function is isolated and doesn’t depend on external state, AI catches these consistently. In ERP systems, a broken landed cost formula or tax calculation is instant client trust damage. AI has saved me here more than once.

Direct React state mutations. Pushing directly to an array, mutating nested objects without spreading — AI flags these reliably in simple components. Not groundbreaking, but useful as a first pass before compilation.

Where it completely fell apart

Race conditions. This was my most painful production bug in the last 6 months.

Our batch item table allows rapid concurrent mutations — user triggers cascading calculations, multiple async state updates overlap, one override silently wins. Classic race condition.

I ran the component through every AI tool I was using at the time. Zero flags. Not even a “this might be worth checking.” The bug only surfaced when a client hit a specific sequence of interactions in rapid succession.

AI does static analysis. It cannot simulate erratic user interaction timing. Race conditions are invisible to it.

Web Worker memory leaks. I implemented workers for heavy client-side calculations. Suspicious of potential leaks from rapid event-driven spawning, I tasked my entire AI stack with auditing the cleanup patterns.

Every tool gave a confident green pass.

My manual browser profiling found that workers weren’t reliably terminating under specific runtime exceptions. Ghost processes, staying alive. The AI verified the cleanup code existed. It could not verify the cleanup actually ran across every execution path.

CSS layout bugs on custom components. Built a proprietary data grid from scratch to hit strict UX requirements. Ran into padding misalignments and layout collapse under certain data payloads.

AI was almost useless. I’d describe the visual bug, it would claim to fix it, the rendered output would still be broken. Without a real rendering environment, it’s guessing at cascade behavior.

The thing nobody talks about: chunking

This was my biggest operational discovery.

I was building an MRP allocation table — quantities, supply constraints, fulfillment priorities cascading across hundreds of rows. I fed the full spec into the AI in a single pass.

Every tool failed. Not “slightly off” — confidently wrong logic that looked correct until you traced execution. Broken dependencies, misallocated quantities, state updates that would silently fail on specific edge cases.

Then I split it:

  1. One prompt — just the core allocation math
  2. One prompt — data validation constraints only
  3. One prompt — the immutable React state update pattern
  4. Final prompt — audit each module against the others

Every piece came back clean. Assembled system worked perfectly in production.

The mental model shift: AI degrades under compound logical dependencies, not under token length. An ERP module has overlapping validation paths, tax calculations, database state requirements. Feeding all of it simultaneously overloads the model’s situational logic.

If you can’t explain the task scope to a senior dev in 2 minutes out loud, chunk it before it touches an LLM.

The confidence problem

This is the part that actually concerns me.

When my Web Worker had a memory leak, no tool said: “This syntax looks valid, but I can’t verify runtime cleanup behavior — profile this manually.” They said it looked fine. Clean pass. Ship it.

The framing I’ve settled on:

An AI bug flag is high-signal — act on it immediately.
An AI clean pass is weak evidence — it means no obvious patterns matched, not that the code is safe.

The deadline trap is real. When you’re pushing to meet a sprint, “the AI said it’s fine” becomes a substitute for actual testing. That’s where production regressions come from.

My actual tool stack after 6 months

  • Codeium — inline autocomplete for single-file work. Hits a ceiling fast on cross-file reasoning.
  • ChatGPT — useful for isolated logic audits, but snippet output creates integration friction at scale.
  • Cursor (Claude Sonnet) — my go-to for refactoring. Full-file editing context is genuinely better than any chat interface.
  • Google Gemini Code Assist — primary daily driver. Large context window, cost-efficient for heavy ERP work.
  • Claude direct — architectural audits and high-level logic design. Most willing to flag issues with caveats instead of blind passes.
  • OpenAI Codex App — pre-PR repository-wide audits in sandboxed worktrees. Runs parallel agents without touching my local environment.

Each tool serves a distinct step. Switching between them isn’t tool-hopping — it’s using the right instrument for the job.

Quick reference

Trust AI for:

  • Utility functions under ~50 lines with clear I/O
  • Catching duplicate/redundant modules across large repos
  • Refactoring known-safe logic into cleaner patterns
  • Boilerplate React hooks, contexts, basic reducers

Verify manually:

  • Multi-tiered stateful components with rapid user interactions
  • Web Workers, Sockets, async lifecycle management
  • Custom visual components (non-library)
  • Core financial/billing calculations
  • Anything you can’t explain to a teammate in 2 minutes

Chunk before sending:

  • Features spanning more than 2 interacting subsystems
  • Full ERP modules with cascading state (MRP, inventory allocation)
  • Any prompt that needs multiple paragraphs just to define the constraints

The engineers getting the most out of AI aren’t the ones who trust it blindly. They’re the ones who know exactly where that trust stops.

Full breakdown with the complete MRP case study, tool-by-tool notes, and a practical checklist here → stacknovahq.com

The Cursor 3 Features Nobody Is Talking About Yet

Everyone is writing about the Agents Window. The parallel agents. The cloud handoff. The rebuilt interface from scratch under the codename Glass.

Those are the headline features and they deserve the coverage. But after spending serious time in Cursor 3 since it shipped on April 2, 2026, the features that have actually changed how I work are not the ones in the announcement post.

Here are the Cursor 3 features that are quietly doing the most useful work — and that most developers have either not found or not fully understood yet.

1. Design Mode — Point at UI Elements Instead of Describing Them

This one is buried in the changelog and barely mentioned in the main announcement.

In the Agents Window, you can activate Design Mode to annotate and target UI elements directly in the browser. Instead of writing a prompt like “the button in the top right corner of the navigation bar that appears on hover” — you point at it. The agent knows exactly what you are referring to.

This sounds small. It is not small.

Anyone who has spent time trying to describe UI context to an AI model knows the problem. You spend more words explaining what you are looking at than it would take to just fix it yourself. Design Mode collapses that entirely. You point. The agent acts.

To access it: open the Agents Window with Cmd+Shift+P -> Agents Window, then activate Design Mode from the agent toolbar. It only works in the Agents Window, not the traditional IDE view — which is probably why most developers using the old IDE interface have not found it yet.

2. The PR Review Experience That Replaces Your Whole Review Workflow

Cursor 3 shipped a PR review experience that most developers are treating as a minor feature. It is not minor.

You can now take a PR from creation to merge entirely within Cursor. The agent reviews the changes, flags issues, suggests improvements, and tracks the full lifecycle of the PR without you switching to GitHub, back to the terminal, back to GitHub, back to the editor.

The context switching tax in code review is enormous and almost completely invisible because it has always been there. Every time you leave the editor to check a PR comment and come back, you pay a cost. Cursor 3 eliminates that context switch.

The Bugbot integration makes this sharper. With the high effort setting — which you can now customize per team or per individual — Bugbot finds significantly more bugs per run than default. The tradeoff is cost and time. But for critical PRs where a missed bug is expensive, the option to dial up the effort is genuinely useful and not something most PR review tools give you control over.

3. Parallel Agents on Plans — The Feature That Changes How You Scope Work

When you create a plan in Cursor 3, the agents execute steps in parallel where dependencies allow. This sounds like a performance improvement. It is actually a workflow change.

Because steps run in parallel, you start thinking about work differently. Instead of a linear sequence of tasks, you think about the dependency graph. What needs to be done before what? What can happen simultaneously?
This is how senior engineers think about complex work. Cursor 3 essentially externalizes that mental model into the tool. You write the plan. The tool figures out what can run in parallel. You watch multiple things get built at once.

The quick action pills that shipped alongside this — common workflow actions surfaced as one click options rather than typed commands — reduce the friction of directing parallel work significantly. Less typing, faster iteration, more time watching things actually happen.

4. Composer 2 Is the Real Engine — And Most Developers Do Not Understand What It Is

Cursor 3 gets the attention. Composer 2, which shipped on March 19 and powers Cursor 3’s agents, is the part that actually explains why the agents feel different.

Composer 2 is not a wrapper around a frontier model. It is Cursor’s own model — built on Kimi K2.5 from Moonshot AI as a base, then continued pre-training on a code heavy data mix, followed by large scale reinforcement learning in realistic Cursor sessions. Around 75 percent of the total compute came from Cursor’s own training, not the base model.

What this means in practice is that Composer 2 has been trained on how developers actually use Cursor — not just on code in the abstract. It knows the tool it is running in. It understands the workflows. It has been reinforced on real Cursor sessions.

This is a fundamentally different approach from routing your requests to Claude or GPT and hoping the model understands your IDE context. Composer 2 was built for this environment specifically. The capability jump on complex multi-file tasks is noticeable — and it is not placebo.

5. Environment Version History — The Underrated Safety Net

This one shipped quietly in the most recent update and I have not seen anyone write about it yet.

Every development environment in Cursor now has its own version history that you can review and roll back. If an agent configures your environment incorrectly — installs the wrong dependency version, misconfigures a tool, breaks something in the setup — you can roll back to a previous environment state rather than debugging what changed.

For solo developers and small teams this is genuinely useful. Environment configuration bugs are some of the most time consuming to diagnose because the symptoms appear in your code but the cause is in your setup. Version history on the environment itself gives you a direct path back to a known good state.

Admins can also restrict rollback permissions to admins only — which matters for teams where environment consistency is critical and you do not want individual developers rolling back shared configurations without oversight.

The Feature I Am Still Watching

Cursor is rolling out automated environment configuration in private beta for Enterprise teams — where it inspects your repos, figures out the tools and dependencies required, and produces a configuration you can edit and version. As it configures your environment, it asks questions, flags missing credentials, and validates that your environment is set up properly.

This is not widely available yet. But if it works as described it solves one of the most painful parts of onboarding new developers or spinning up new projects — the environment setup that always takes longer than anyone planned and breaks in ways that are specific to the machine, not the code.

What Cursor 3 Actually Is

The announcement framed Cursor 3 as a unified workspace for building software with agents. That is accurate but understates the shift.
The traditional IDE view still exists and still works. You can switch back at any time. But the Agents Window represents a genuinely different model of how software gets built — you as the architect directing agents, not you as the typist writing every line.

The Agents Window, parallel execution, cloud agent handoff, Design Mode, and built-in Git are all meaningful additions that move Cursor from a smart IDE with AI toward something closer to an agent orchestration platform that happens to have an excellent IDE inside it. exactsolution
The features nobody is talking about are the ones that make that shift feel real in daily use rather than just impressive in a demo. Design Mode, PR review lifecycle, environment version history, parallel plan execution — these are the features that change Tuesday afternoon, not just the ones that change the keynote slide.

Exact Solution is a certified refurbished electronics marketplace shipping across Europe. We stock the best refurbished laptops from Apple, Dell, HP, and Lenovo — all fully tested and ready to ship.

Production-Grade Observability: Building a Complete LGTM Stack with SLOs, DORA Metrics, and Intelligent Alerting

Introduction
In modern DevOps, simply knowing whether your application is “up” or “down” isn’t enough. Users care about latency, reliability, and the consistency of your service. To meet these expectations at scale, we built a production-grade observability platform using the LGTM stack (Loki, Grafana, Tempo, Prometheus), integrated DORA metrics for CI/CD visibility, and implemented SLI/SLO/error budget frameworks to align engineering with business outcomes.
This blog post walks through our complete implementation—from architecture and infrastructure-as-code to burn-rate alerting, incident management, and live chaos testing. We’ll show you how to move beyond CPU/RAM monitoring into meaningful reliability engineering.

Why LGTM Over Managed Alternatives?
We evaluated several observability solutions: Datadog, New Relic, Splunk, and managed ELK. Here’s why we chose LGTM:

Cost transparency: No surprise bills based on volume spikes. Fixed infrastructure cost with predictable scaling.
Full control: We own the alerting rules, dashboard definitions, and data retention policies—all version-controlled in Git.
Composable: Each tool (Prometheus, Loki, Tempo, Grafana) is best-in-class for its job. You’re not locked into one vendor’s compromise.
Community-driven: The ecosystem is mature, well-documented, and battle-tested by thousands of companies.
Learning value: Understanding how these tools work teaches deeper systems thinking than a button-click interface ever could.

Architecture: A Complete Data Flow
Our stack comprises nine services orchestrated via Docker Compose:
Data Flow

Metrics: The sample app exports Prometheus metrics (counters, histograms, gauges). Prometheus scrapes the app, node-exporter, blackbox-exporter, and the DORA exporter at configurable intervals (default 15s).
Logs: The app sends structured JSON logs to the OpenTelemetry Collector via OTLP. The collector forwards them to Loki, which indexes by service_name and other labels.
Traces: The app instruments request spans and ships them to the collector, which exports to Tempo for distributed tracing.
Alerts: Prometheus evaluates rules (all version-controlled). Alert rules fire to Alertmanager, which groups, inhibits, and routes to Slack with structured payloads.

Key detail: Loki’s datasource is configured with a derived field for trace_id. When you spot an error in logs, one click drills into Tempo to see the full trace—the ultimate observability feedback loop.

The Four Golden Signals as SLIs
The Four Golden Signals are a framework for measuring service health: Latency, Traffic, Errors, and Saturation. We define each as a Service Level Indicator (SLI)—a measurable proxy for what users care about.

  1. Latency: How long does it take to serve a request?
    This PromQL expression calculates the 95th percentile of request latency over a 5-minute window. We distinguish successful requests (status < 500) from error responses—a slow error is cheaper to tolerate than a slow success.
    Our SLI target: 95% of successful requests complete in under 500ms.

  2. Traffic: How much demand is the system handling?
    This measures requests per second. Traffic acts as a leading indicator—if traffic spikes before errors, you can scale proactively. If traffic drops suddenly, that’s also a sign of trouble upstream.
    Our SLI target: No hard limit; we monitor for anomalies via trends.

  3. Errors: What fraction of requests fail?
    This calculates the 5-minute error rate as a ratio. “Errors” include explicit 5xx responses, timeouts, and policy violations (e.g., slow timeouts count as errors for SLO purposes).
    Our SLI target: Error rate < 1% (equivalently, 99% success rate).

  4. Saturation: How full is the service?
    Saturation covers CPU, memory, disk, and connection pools. When saturation approaches 100%, performance degrades sharply. Early warning of saturation lets you scale before users hit the wall.
    Our SLI targets:

CPU: warn at 80%, critical at 90%.
Memory: warn at 80%, critical at 90%.
Disk: warn at 75%, critical at 90%

SLOs, Error Budgets, and the Policy That Binds Them
An SLO is a commitment: “We promise 99% availability over a 30-day window.” The unspent portion—1% bad events over 30 days—is your error budget.

Calculating Error Budget Burn
Suppose your error SLO is 99% success over 30 days. The error budget is 1% of 30 days = 7.2 hours of allowed “bad” requests.
Fast burn rate (14.4x burn): Consuming 1% of the budget in 1 hour. Example: error rate jumps to 2.88% (all budget burned in ~2.4 hours). Action: Immediate incident response, rollback, or emergency fix.
Slow burn rate (5x burn): Consuming 1% of the budget in 6 hours. Example: error rate rises to 1.48% (all budget consumed in ~30 hours). Action: Schedule a reliability sprint; don’t rush, but prioritize.
Our Error Budget Policy
At 50% consumed: Service owner reviews recent changes and opens mitigation tasks.
At 75% consumed: Non-critical features require service-owner approval before merging.
At 100% consumed: Feature freeze—focus entirely on reliability until SLO recovers or leadership signs off on the risk.
Review cadence: Every two weeks or after significant incidents. If burn-rate alerts are noisy, we adjust thresholds rather than ignoring them.
Ownership: Service owner owns SLO health and burn rate decisions. Platform engineering owns observability tooling correctness.
Prometheus Alert Rules: From Theory to Action
All alert rules live in version-controlled YAML files under prometheus/alerts/. Here are the key categories:
Infrastructure Alerts

yaml
-alert: HighCPUWarning
expr: 100 – (avg by (instance) (rate(node_cpu_seconds_total{mode=”idle”}[5m])) * 100) > 80
for: 5m
annotations:
summary: “CPU usage is above 80%”
dashboard_url: “http://localhost:3000/d/node-exporter/node-exporter“
runbook_url: “runbooks/high-cpu.md”

This fires when CPU exceeds 80% for 5+ consecutive minutes. The for clause prevents alert flapping—transient spikes don’t trigger paging.
Key design choice: We alert on warning (80%) before critical (90%), giving operators time to respond gracefully. Both alerts include runbook links, so responders know their first investigation steps.

SLO Burn-Rate Alerts

yaml

  • alert: FastBurnRate
    expr: |
    (
    sum(rate(http_requests_total{status_code=~”5..”}[5m]))
    /
    clamp_min(sum(rate(http_requests_total[5m])), 1)
    ) > 0.144 # 99% SLO allows ~1.44% error rate for fast burn
    for: 5m
    labels:
    severity: critical
    annotations:
    summary: “Fast error-budget burn detected”
    dashboard_url: “http://localhost:3000/d/slo-error-budget/slo-error-budget”
    runbook_url: “runbooks/fast-burn.md”
    The expression 0.144 represents a 14.4x burn rate—consuming the error budget 14 times faster than normal. This threshold is calibrated to the SLO and measurement window.
    Why burn-rate alerts work: They’re based on rate of degradation, not absolute thresholds. A 2% error rate might be acceptable for 1 hour (slow burn), but not for 5 minutes (fast burn). This reduces alert fatigue and focuses on what users actually experience.
    Alertmanager Routing and Slack Templates
    Alerts funnel through Alertmanager, which groups, inhibits, and routes them:
    yamlroute:
    receiver: “slack”
    group_by: [“service”, “severity”]
    group_wait: 30s
    group_interval: 5m
    repeat_interval: 4h
    routes:

    • match:
      service: sample-app
      receiver: “slack-app”
      continue: true

inhibit_rules:

  • source_match:
    severity: critical
    service: node
    target_match:
    severity: warning
    equal: [“instance”]
    Inhibition: If a host is fully unreachable (critical), suppress CPU/memory/latency noise on the same host. This prevents alert storms during outages.
    Grouping: Alerts for the same service/severity are bundled into a single Slack message every 5 minutes. If you have 10 CPU warnings on different hosts, you get one grouped notification, not 10 individual pings.
    The Slack template includes:

Alert name and severity
Current metric value (e.g., “CPU 94.2%”)
Affected instance/service
Grafana dashboard link for drill-down
Runbook link for action items

DORA Metrics: Connecting Engineering Velocity to Reliability
DORA (DevOps Research & Assessment) metrics measure engineering team performance and correlate it with system stability:

Deployment Frequency (DF): How often do you ship?

Elite: > 1 per day
High: 1–7 per week
Medium: 1–4 per month
Low: < 1 per month

Lead Time for Changes (LTC): How long from commit to production?

Elite: < 1 hour
High: 1–24 hours
Medium: 1–7 days
Low: > 1 month

Change Failure Rate (CFR): What % of deployments cause incidents?

Elite: 0–15%
High: 15–45%
Medium: 45–60%
Low: > 60%

Mean Time to Restore (MTTR): How long to fix failed deployments?

Elite: < 1 hour
High: 1–24 hours
Medium: 1–7 days
Low: > 1 week

Our Implementation
We built a custom DORA exporter (dora-exporter/app.py) that queries GitHub Actions API:

Scrapes workflow runs and their success/failure status
Extracts deployment timestamps, commit hashes, and PR merge times
Exposes metrics as Prometheus gauges and counters
Calculates deployment frequency and classifications

The Grafana dashboard shows:

Deployment frequency with DORA classification banner
Lead time breakdown: commit → pipeline trigger → pipeline complete → deployment confirmed
Change failure rate trended over 7 and 30-day windows with SLO threshold line
MTTR distribution (how long alerts take to resolve).

Why DORA Matters
Engineering metrics are business metrics. High deployment frequency with low CFR and MTTR signals a mature CI/CD process and strong incident response. Teams improving these metrics often see:

Faster incident recovery (MTTR)
Fewer rollbacks and hotfixes (CFR)
Higher customer satisfaction
Less on-call burden.

Grafana Dashboards: The View Into Your System
All five dashboards are provisioned as JSON, never hand-configured in the UI:

  1. Unified Observability Dashboard
    This is the most important. A user sees a latency spike. They:

Click the spike in the latency panel
See logs from that time window in Loki
Click a trace_id derived field link
Land in Tempo with the full distributed trace
Identify the slow service and failing endpoint

The underlying magic: Loki’s derived fields interpret trace_id in logs and link directly to Tempo.

  1. SLO & Error Budget Dashboard
    Shows:

SLI vs. SLO gauges (e.g., “Current error rate: 0.3% vs. SLO 1%”)
Error budget remaining in percentage and absolute time
Burn rate time series with fast/slow thresholds as red/yellow bands
7-day and 30-day SLO compliance history

When the burn rate line crosses into the red zone, operators know they’re in fast-burn territory and should engage incident response.

  1. DORA Metrics Dashboard
    Shows:

Deployment frequency trend with DORA classification badge
Lead time breakdown by phase
CFR and MTTR distributions
Recent deployments with outcomes

  1. Node Exporter Dashboard
    System-level metrics: CPU (total and per-core), memory (used/cached/available), disk I/O, network I/O, load averages.
  2. Blackbox Exporter Dashboard
    Synthetic monitoring: uptime/downtime timeline, HTTP response time percentiles (p50, p90, p99), SSL expiration countdown, probe success rate.
    All dashboards link to each other and to runbooks, creating a cohesive incident response workflow.

Runbooks: From Alert to Resolution
A runbook is a structured troubleshooting guide. For each alert, we provide:

What is this alert? Plain-English definition and SLO/metric context.
Why did it fire? Likely causes (bad deploy, traffic spike, resource leak, etc.).
First 3 investigation steps: Specific queries and checks to narrow the problem.
How do I resolve it? Step-by-step fix procedures.
When do I roll back? Criteria for reverting the last deploy.
When do I escalate? Conditions for involving leadership (e.g., no progress in 30 minutes).

Game Day: Chaos and Failure Scenarios
To validate the entire stack, we ran three production-like failure scenarios:
Scenario 1: Deployment Failure
Trigger: Force a failed GitHub Actions workflow.
Expected: DORA exporter detects failure, CFR alert fires in Slack, dashboard CFR panel updates, team follows runbook to determine if rollback is needed.
Validation: Screenshot showing alert in Slack, CFR spike in Grafana, and timeline of detection-to-notification.
Scenario 2: Latency Injection
Trigger: Call /slow endpoint repeatedly via load testing.
Expected:

Latency histogram in Grafana rises (95th percentile > 500ms)
SLI degrades (error rate approaches threshold)
Burn-rate alert fires (fast or slow)
Logs show slow requests from the app
Click trace_id link → Tempo shows the slow span

Validation: Screenshots of each drill-down step.
Scenario 3: Resource Pressure
Trigger: CPU spike (stress tool) or memory exhaustion.
Expected:

Warning alert (80% CPU) fires first → runbook
Critical alert (90% CPU) fires if pressure continues
As pressure clears, recovery notification sent
Dashboard shows metrics normalizing

Validation: Timeline showing warning → critical → recovery sequence with recovery notification in Slack.
All three scenarios confirm that:

Alerts fire reliably with correct timing
Runbooks are actionable and lead to resolution
Dashboard drill-downs (logs → traces) work end-to-end
Team confidence in incident response increases.

Toil Identification and Automation
We identified three sources of toil (repetitive manual work):

  1. Dashboard Screenshot Collection
    Before: Manually opening each dashboard in Grafana, taking screenshots, and uploading to Slack for weekly reports.
    After: Automated script using Grafana API (GET /api/dashboards/db/:slug, export as image) to generate a weekly PDF report. Reduces toil by ~4 hours/week.
  2. Incident Timeline Assembly
    Before: Manually sifting through logs, traces, and alert history to construct a timeline during post-incident reviews.
    After: Python script querying Loki for logs and Tempo for traces by alert timestamp, auto-generating a structured timeline with severity progression. Reduces PIR prep by ~2 hours/incident.
  3. Runbook Maintenance
    Before: Runbooks drift from reality as systems change; alerts point to outdated procedures.
    After: Runbook links embedded in alerts trigger a CI/CD check: does the linked file exist and contain required sections? Missing/broken links fail the build. Reduces runbook staleness and improves on-call experience.

Infrastructure as Code: One-Command Deployment
The entire stack deploys with:
bashcp .env.example .env
make up
Under the hood:

docker-compose.yml orchestrates 9 services
Prometheus, Alertmanager, Loki, Tempo configs are volume-mounted from version-controlled files
Grafana datasources and dashboards are auto-provisioned via /etc/grafana/provisioning/
Alert rules are read from prometheus/alerts/*.yml

For Terraform users, we provide a wrapper:
bashcd terraform
terraform init
terraform apply
This abstracts Docker infrastructure (if deploying to AWS, Kubernetes, etc.) while keeping the stack definition identical.
Philosophy: Infrastructure should be repeatable and auditable. A new team member should be able to run make up and have a fully functional observability platform in 3 minutes, no “click here” steps.

Lessons Learned and Future Improvements
What Worked Well

SLO-driven alerting: Burn-rate alerts are far less noisy than absolute thresholds. Team adoption was immediate.
Derived fields in Loki: The one-click logs → traces drill-down is transformative for troubleshooting.
Version-controlled dashboards: Dashboard-as-code eliminates UI-drift and enables code review on observability changes.
Inhibition rules: Suppressing secondary alerts during outages prevents alert storms.

What We’d Improve

Cardinality management: High-cardinality labels (e.g., user IDs, request paths) can balloon Prometheus storage. We’d implement stricter label policies and use recording rules more aggressively.
Temporal data retention: 7 days of logs / 5 days of traces is tight for deep investigations. Moving to S3-backed storage would extend retention without local-disk bloat.
Multi-team onboarding: Today, dashboard creation requires JSON knowledge. A dashboard-builder UI (while still exporting as JSON) would democratize observability.

Conclusion
Building a production-grade observability platform is a journey, not a destination. The LGTM stack gives you:

Metrics (Prometheus): Understand system behavior at scale.
Logs (Loki): Contextualize metrics with event sequences.
Traces (Tempo): See causal chains across services.
Visualization (Grafana): Unify all signals in dashboards.

Layered with SLOs, error budgets, and DORA metrics, you move from “Is it up?” to “Are we meeting user expectations?” That shift is where reliability engineering begins.
The observability platform is code, the alert rules are testable, and the runbooks are executable—just like the services you’re monitoring. Treat observability as a first-class infrastructure concern, and your team will spend less time fighting fires and more time building features.

I Just Wanted to Reuse Async Functions in Rust. I Ended Up Building a Tiny Task Manager

I Just Wanted to Reuse Async Functions in Rust. I Ended Up Building a Tiny Task Manager

I am currently studying Rust, and one of the things that kept bothering me was surprisingly simple:

How do I reuse an async function more than once in a clean way?

At first, this did not look like a big problem. I already had async functions. I already had Tokio. I could already spawn tasks.

So what was the issue?

The issue was that I did not just want to run a task once.

I wanted to:

  • register different async functions
  • run them inside the same async system
  • treat them as generic tasks
  • restart some of them depending on their role
  • keep the system logic separate from the task logic

That is the point where “I just need an async function” slowly becomes “I think I am building a task manager”.

This article is the story of that shift.

If you want to follow along with the code, the project is here:

https://github.com/icsboyx/async_task_manager

And this is the central idea that finally made the whole thing click for me:

A Future is not a task definition.

A Future is one execution.

If I want restartable async work, I should store the recipe: Fn() -> Future.

The Real Problem Was Not Async

The real problem was not writing async functions.

Rust makes it easy to write something like this:

pub async fn task1() -> anyhow::Result<()> {
    loop {
        println!("task 1 is running");
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    }
}

This function intentionally never returns, so there is no trailing Ok(()).
If I later add shutdown logic, I can exit the loop cleanly with something like break Ok(()).

The problem starts when you want to say:

“I want to store multiple async functions, possibly with different behaviors, and start them when I want.”

That sounds natural, but in Rust it immediately forces you to think about:

  • concrete types
  • futures
  • trait objects
  • closures
  • pinning
  • ownership
  • reusability

And that is where I hit my first wall.

My First Wrong Mental Model

My first instinct was something like:

let tasks = vec![task1, task2, task3];

Conceptually, that is exactly what I wanted:

  • here are my tasks
  • keep them in a collection
  • run them when needed
  • restart them if needed

But that nice mental model hides the real issue.

Each async fn returns a future, and each future has its own concrete type.
So the moment I want one task manager to store several different async starters behind one uniform interface, I need to be much more explicit about the abstraction I want.

That was the first real lesson:

If I want to restart async work, I should not store a future that is already created.

I should store something that can create a fresh future every time.

That single realization changed the whole design.

task1 vs task1(): The Difference That Actually Matters

This was the point where Rust forced me to think more clearly.

The first mistake would be trying to store this:

task1()

Why is that wrong for my use case?

Because task1() is not the task definition.
It is a future instance created right now.

That means:

  • it represents one execution
  • once that execution finishes, that value is done
  • if I want to restart the task, I do not want the old future back
  • I need a brand new future for the next run

So the real thing I needed was not:

task1()

but:

task1

or, more generally:

Fn() -> Future

That is the key mental shift.

Function Item, Function Pointer, Closure, dyn Fn

This also helped me understand something I had been hand-waving before.

task1 is not automatically “a dynamic function”.

At first, it is a function item: a concrete value tied to one exact function.
Rust knows exactly which function it is and gives it a concrete type.

From there, I can move through more general callable shapes:

  • task1: a concrete function item
  • fn() -> _: a function pointer
  • move || ...: a closure
  • dyn Fn() -> _: a dynamically dispatched callable trait object

That distinction mattered a lot for me, because the real transition was not just:

  • from task1() to task1

but:

  • from one concrete callable
  • toward one uniform callable abstraction I could store in the task manager

Once I saw it that way, the problem stopped being “why is Rust being difficult?” and became “what callable shape do I actually need?”

The Important Type Transition: From Concrete to Dynamic

This was probably the most important Rust lesson in the whole project.

When I write:

pub async fn task1() -> anyhow::Result<()>

Rust gives me a concrete future type behind the scenes.
The same is true for task2, task3, and every other async fn.

So even if all my functions look conceptually similar, Rust still sees different concrete future types.

That means this line is doing two jobs in the article:

let tasks = vec![task1, task2, task3];

Repetita iuvant.
Yes, I am showing the same line again on purpose.
The first time, it was my naive mental model.
This time, it marks the exact point where the type system pushes back.

If I want a HashMap, a queue, or a task manager to store all tasks uniformly, I need one shared stored type.
I cannot keep each future in its own concrete shape and still expect one collection to treat them all the same.

So this is where I had to make the move that, for me, changed the whole design:

  • from one concrete future type
  • to one dynamic future abstraction I could actually store

Because this is the point where Rust stops feeling like:

I wrote some async functions

and starts feeling like:

I need to model a reusable async execution boundary

In other words, I stopped thinking:

Store this exact future type.

and started thinking:

Store anything that behaves like a future returning Result<()>, even if the concrete future behind it changes from task to task.

In cleaned-up form, the idea that finally made sense to me was this:

use std::{
    future::Future,
    pin::Pin,
    sync::Arc,
};

type TaskFuture =
    Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + Send + 'static>>;

type TaskFn =
    Arc<dyn Fn() -> TaskFuture + Send + Sync + 'static>;

And this was exactly the moment where my brain went:

wow wow wow wow wow… what the duck is this?

Because the first time you see something like this while learning Rust, it does not look like “a reusable task”.
It looks like somebody dropped an entire type system on your keyboard.

And yet, once I slowed down and read it piece by piece, it stopped looking absurd.

This pair of types says exactly what I need:

  • a callable thing I can keep
  • that I can invoke more than once
  • and that gives me an owned future I can spawn each time

That is the real abstraction boundary in the design.

Why dyn Future Needs Box

Trait objects like dyn Future do not have a known size at compile time.

Rust therefore cannot store them directly by value.
I need indirection.

That is why the owned dynamic future shape is typically:

Box<dyn Future<Output = Result<(), anyhow::Error>> + Send>

The Box gives me:

  • one stable heap allocation for the concrete future
  • one uniform, sized handle I can store
  • a way to erase the concrete future type behind a common interface

The same idea applies to the callable itself.

I do not want one Task<F, Fut> per task starter.
I want one storable task function type shared by the whole manager.

Why Pin<Box<dyn Future<...>>>

This was another thing that looked scary until it clicked.

Many futures produced by async fn are not Unpin.
After polling starts, Rust may require that they are not moved around in memory.

That is why the common owned dynamic future shape is not just:

Box<dyn Future<Output = Result<(), anyhow::Error>> + Send>

but:

Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + Send>>

So:

  • dyn Future gives me dynamic dispatch
  • Box gives me indirection and a sized owned container
  • Pin gives me the movement guarantees a future may require

Once that clicked, the ugly type stopped looking random.
It started looking like a precise description of the thing I actually needed to own.

The Closure That Makes the Whole Thing Reusable

Even after understanding the dynamic future type, I still needed a bridge between:

  • the original concrete callable
  • and the uniform task function type I wanted to store

That bridge is the closure.

In cleaned-up form, the constructor looks like this:

impl Task {
    pub fn new<F, Fut>(
        name: impl Into<String>,
        function: F,
        task_type: TaskType,
    ) -> Self
    where
        F: Fn() -> Fut + Send + Sync + 'static,
        Fut: Future<Output = Result<(), anyhow::Error>> + Send + 'static,
    {
        let start_fn: TaskFn = Arc::new(move || {
            Box::pin(function()) as TaskFuture
        });

        Self {
            uuid: uuid::Uuid::new_v4().to_string(),
            name: name.into(),
            start_fn,
            task_type,
        }
    }
}

At first glance, this looks like glue code.

It is not.

This closure is doing the real adaptation work.

It does three jobs at once:

  1. it owns the original concrete callable
  2. it calls it each time a fresh execution is needed
  3. it erases the concrete future type into TaskFuture

That is what makes the design restartable.

Without this adapter, I would not really have a reusable task definition.
I would just have a bunch of specific functions with no common storage shape.

The move also matters here.
It makes the closure take ownership of function, so the task can keep that callable around and use it later, long after Task::new has returned.

If I had to summarize this entire section in one sentence, it would be:

The reusable thing is not the future.

The reusable thing is the factory that creates a fresh future on demand.

A Diagram of What Actually Happens

Here is the transformation that finally helped me think about it clearly:

flowchart LR
    A["Concrete async function<br/>task1"] --> B["Concrete callable<br/>F: Fn() -> Fut"]
    B --> C["Adapter closure<br/>move || Box::pin(function())"]
    C --> D["Dynamic task factory<br/>TaskFn"]
    D --> E["Stored inside Task<br/>start_fn"]
    E --> F["Called by TaskManager"]
    F --> G["Fresh owned future<br/>TaskFuture"]
    G --> H["Spawned into Tokio JoinSet"]

This is also the point where the whole thing started to feel like function inception:

a function that stores a function that creates a future that runs a task

Ridiculous? A little.
Correct? Also yes.

Building a Task Type

Once I understood that I needed a restartable task factory, the next step was creating a task representation with both execution behavior and system meaning.

Conceptually, the task shape became:

pub struct Task {
    uuid: String,
    name: String,
    start_fn: TaskFn,
    task_type: TaskType,
}

I liked this design because each task now had:

  • an identity
  • a name for logs
  • a reusable startup function
  • a system-level policy

And that last point turned out to be very important.

Running a Task Is Not Enough. You Need to Know What It Means

While building the manager, I realized that “a task ended” is not enough information.

A task manager should not only know how to run a task.
It should know what that task means to the system.

Some tasks are optional.
Some tasks should stop the whole system when they end.
Some tasks are essential enough that if they disappear, the rest of the system no longer makes sense.

That is why I introduced this enum:

pub enum TaskType {
    ShutDownOnExit,
    Mandatory(i32),
    Detachable(i32),
}

This was a big architectural improvement, because now the manager was not just executing tasks.
It was interpreting them.

ShutDownOnExit

If this task exits, the manager stops.

This is perfect for something like a ctrl_c_handler.

Mandatory(restarts)

This task is essential for the system.

If it exits, restart it while budget remains.
If the restart budget is exhausted, stop the whole manager.

For me, this means:

if this task is gone, keeping the rest of the system alive no longer makes sense

Detachable(restarts)

This task is useful, but not essential.

If it exits successfully, I can discard it.
If it fails, I can restart it while budget remains.
If the restart budget is exhausted, I discard it and let the rest of the system continue.

That made the whole project feel much more like a small supervised async system, and much less like a pile of random spawned tasks.

The Restart Counter Was More Subtle Than I Expected

Another thing that looked easy at first was restart logic.

I thought:

“Fine, I just decrement a counter and restart the task.”

But once I started implementing it, I had to answer a more precise question:

What does 3 actually mean?

Does it mean:

  • the task can run 3 times total?
  • the task can fail 3 times?
  • the task can be restarted 3 times after the first start?

I chose the last meaning.

So in my design:

Mandatory(3) or Detachable(3) means:

  • 1 initial start
  • up to 3 restarts

That sounds simple, but it forced me to be careful with off-by-one logic.

The behavior I wanted was:

  • if remaining restarts are greater than zero, restart and decrement
  • if remaining restarts are zero, do not restart anymore

That was one of those small implementation details that taught me a lot about translating a verbal idea into precise behavior.

Why JoinSet Helped a Lot

Once I had reusable task factories, I still needed a way to:

  • spawn multiple tasks
  • wait for whichever one completes next
  • know which task completed
  • separate task completion from task policy decisions

tokio::task::JoinSet was a very good fit for this.

The manager can spawn tasks into the set and then wait for completions:

result = self.task_set.join_next_with_id() => {
    // handle task completion here
}

That with_id part matters a lot.

If a task finishes, I do not just want to know that something finished.
I want to know which task finished, so I can:

  • remove it from the active set
  • look up its metadata
  • apply the correct restart or shutdown policy

Another subtle but important point is that if the spawned task returns Result<(), anyhow::Error>, then the join result is nested.

In practice, that means these cases are different:

Some(Ok((id, Ok(()))))   // Tokio join succeeded and the task returned Ok
Some(Ok((id, Err(e))))   // Tokio join succeeded but the task returned an application error
Some(Err(e))             // Tokio JoinError: panic, cancellation, abort, etc.
None                     // No more tasks in the JoinSet

That distinction matters because “the join worked” is not the same thing as “the task succeeded”.

JoinSet gave me a clean place to separate:

  • execution mechanics
  • task outcome
  • system policy

And that made the task manager much easier to reason about.

The Core Idea of the Manager

At a high level, the flow became:

  1. Create tasks and push them into a queue.
  2. Spawn them into a JoinSet.
  3. Wait for whichever task finishes next.
  4. Identify the task.
  5. Inspect how it ended.
  6. Apply the policy defined by TaskType.
  7. Restart, discard, or stop the system.

That is the moment where the project stopped being “a few async functions” and became a tiny async supervisor.

The Part That Taught Me the Most

If I had to summarize what this project really taught me, it would be this:

1. Reusability in async Rust is about factories

If I want to execute the same async logic multiple times, I should usually store something that creates a future, not the future itself.

2. System behavior should not live inside each task

The task should focus on doing work.
The manager should decide what to do when that work ends.

That separation made the code much easier to reason about.

3. Semantics matter more than syntax

A lot of the difficulty was not “how do I write this in Rust?”

It was:

  • what exactly does restart mean?
  • when should the whole system stop?
  • which tasks are optional and which are essential?

Those are architecture questions, not syntax questions.

Rust just forces you to answer them more explicitly.

Current Shape of the Project

Right now, the project is small on purpose.

It starts a few demo tasks, including a Ctrl+C handler, and routes their behavior through a small task manager with different policies.

It is not a production-grade supervisor, and I do not think it needs to be.

For me, it is a learning project with a very specific goal:

to understand how to reuse async functions cleanly, and how to build a small but structured execution model around them.

And honestly, I think that is why this project was useful.

It did not try to solve everything.
It solved one problem that kept bothering me, and in doing so it forced me to understand several Rust concepts more deeply.

Final Thought

I started with a very small question:

How can I reuse async functions in Rust without turning everything into a mess?

I ended up learning about:

  • boxed futures
  • trait objects
  • closures as adapters
  • pinning
  • task supervision
  • restart policy design
  • async orchestration with Tokio

That is one of the things I like most about learning Rust:

you often start by trying to solve a local problem, and end up improving the way you think about the whole system.

If you are learning Rust too, and you have hit the same wall around reusable async functions, I hope this helps.

Sometimes the real progress is not just making the code compile.
Sometimes it is finally understanding what kind of abstraction you actually needed.

If you want to explore the code itself, you can find the project here:

https://github.com/icsboyx/async_task_manager