Every morning, for way too long, my first real “work” task wasn’t building or designing. It was sifting through error logs. Our staging environment, our production servers – both spitting out their daily complaints, just waiting for me to manually poke around. It was a chore, pure and simple, and one I utterly despised.
The Daily Grind That Drove Me Nuts
See, running a small setup, you wear all the hats. Dev, ops, support, even occasional janitorial duty. And for a while, the grep 'ERROR' dance on our staging server, followed by a separate mental scan of our production Sentry dashboard, felt like an unavoidable evil. It was never just a quick look. It was a mental context switch, often leading to rabbit holes, and inevitably, a missed critical issue now and then because I was either rushing or simply overwhelmed by the sheer volume of noise.
My brain would start the day already tired, sifting through lines like [2026-10-03 14:21:05] production.ERROR: User session expired. User ID: 12345 mixed with harmless warnings, trying to discern if anything was truly on fire. It probably ate up 30-45 minutes every single morning by the time I'd consolidated it into something resembling awareness.
My Failed Attempts at a "Better" Way
Of course, I tried to code my way out of it. My first thought was a Python script. I imagined it running on a cron job, connecting to AWS CloudWatch Logs (where our production API logs live) and also scraping our staging server's /var/log/app.log file directly. I even got a basic prototype working. It would pull logs from CloudWatch for the last 24 hours, filter for ERROR or FATAL severity, and then SSH into staging to cat and grep its logs.
But the reality quickly set in. Maintaining that script became its own headache. Environment variables for AWS credentials, SSH keys for staging, dependency management with pipenv – it felt like I was building a mini-monitoring system, not just a simple log aggregator. What if the log format changed? What if an API key rotated? It wasn't robust enough for the small time investment I could justify, and it certainly wasn't something I wanted to onboard anyone else to maintain. I needed something simpler, more visual, something that just worked without demanding constant attention.
How a Low-Code Flow Became My Morning Hero
That's when I finally gave n8n a serious shot. I'd tinkered with it before, but never for a problem this persistent. The beauty of n8n, for me, is its self-hostable nature combined with a fantastic visual flow builder. It felt like I was building a proper service, but without writing lines of boilerplate.
Here’s the flow I eventually landed on, and it’s been running reliably for months now:
Schedule Trigger: Kicks off daily at 8:30 AM EST. Early enough to catch overnight issues, late enough that I'm actually awake and ready to react.
CloudWatch Logs Node: This is where the production magic happens. I configured it to connect to our AWS account, pulling logs from specific log groups for the past 24 hours. The key here was using the filterPattern option set to { $.level = "ERROR" || $.level = "FATAL" }. This filters directly at the source, drastically reducing the data I pull down. I also added a jsonPath expression to extract just the message field, like so: $json.logEvents[*].message.
SSH Command Node (for Staging): This node connects to our staging VM. It runs a simple command: grep -E 'ERROR|FATAL' /var/log/app.log | tail -n 50. It pulls the last 50 error or fatal lines, which is usually more than enough for our staging environment.
Item Lists & Merge: I then used a few Item Lists nodes to transform the CloudWatch JSON into a readable list of strings and did the same for the SSH output. A Merge node then combines these two lists into a single collection of error messages.
Dedup and Format: A Code node (my one tiny bit of custom JS) deduplicates the messages and formats them into a nice Markdown string, adding context like "Production Error:" or "Staging Error:". This is where I ensure that User session expired (a common, benign prod error) is filtered out if it's the only thing in the list, to reduce noise further.
Slack Node: Finally, this node takes the formatted string and sends it to a dedicated #dev-alerts Slack channel. If there are no new errors, it sends a cheerful "All clear!" message instead.
javascript
// Inside n8n Code Node for deduplication and formatting
const items = $input.all();
const uniqueErrors = new Set();
items.forEach(item => {
const type = item.json.source;
const messages = item.json.messages || [];
messages.forEach(msg => uniqueErrors.add(${type}: ${msg.trim()}));
});
let output = '';
if (uniqueErrors.size === 0 || (uniqueErrors.size === 1 && Array.from(uniqueErrors)[0].includes('User session expired')) ) {
output = '🚀 All clear! No new significant errors across environments today.';
} else {
output = 🚨 Daily Error Digest (${new Date().toLocaleDateString('en-US')}) 🚨\n\n;
output += Array.from(uniqueErrors).join('\n');
}
return [{ json: { text: output } }];
This entire setup, which honestly took me about two hours to piece together and refine, now saves me that 30-45 minutes every morning. More importantly, it gives me peace of mind. Critical issues are flagged automatically, and the digest is waiting for me in Slack – no manual digging required. It's a game-changer for my daily routine, letting me focus on actually building instead of just reacting. This is the real power of low-code for indie devs: solving specific, annoying problems with minimal overhead. My n8n instance, running version 1.25.0, has been rock solid. No more mental load before my coffee even kicks in!
Top comments (0)