<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ben's Tech Blog]]></title><description><![CDATA[Ben's Tech Blog]]></description><link>https://bens-tech-blog.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 19 Sep 2026 21:56:42 GMT</lastBuildDate><atom:link href="https://bens-tech-blog.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Real-Time Server Monitoring Without The Grafana Bloat — I Built a Better Way]]></title><description><![CDATA[Real-Time Server Monitoring Without The Grafana Bloat — I Built a Better Way
I manage 23 servers across different clients. For years, I used Grafana + Prometheus. Setup took 3 days. Maintenance was endless. The dashboard was beautiful but... I never ...]]></description><link>https://bens-tech-blog.hashnode.dev/real-time-server-monitoring-without-the-grafana-bloat-i-built-a-better-way</link><guid isPermaLink="true">https://bens-tech-blog.hashnode.dev/real-time-server-monitoring-without-the-grafana-bloat-i-built-a-better-way</guid><category><![CDATA[monitoring]]></category><category><![CDATA[Python]]></category><category><![CDATA[Devops]]></category><category><![CDATA[servers]]></category><dc:creator><![CDATA[Ben]]></dc:creator><pubDate>Sat, 07 Mar 2026 08:05:11 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-real-time-server-monitoring-without-the-grafana-bloat-i-built-a-better-way">Real-Time Server Monitoring Without The Grafana Bloat — I Built a Better Way</h1>
<p>I manage 23 servers across different clients. For years, I used Grafana + Prometheus. Setup took 3 days. Maintenance was endless. The dashboard was beautiful but... I never actually used it. I just checked a few basic metrics every now and then.</p>
<p>Then I thought: <strong>What if I just had the data I needed, without the UI?</strong></p>
<p>That thought became a 185-line Python script that replaced my entire monitoring stack.</p>
<h2 id="heading-the-problem-enterprise-monitoring-tools-for-small-infrastructure">The Problem: Enterprise Monitoring Tools for Small Infrastructure</h2>
<p>If you're running 5-100 servers, enterprise monitoring is overkill:</p>
<p><strong>Manual Server Checking</strong></p>
<ul>
<li>SSH into each server</li>
<li>Run <code>free -h</code>, <code>df -h</code>, <code>top</code></li>
<li>Write down metrics</li>
<li>Repeat tomorrow</li>
<li>Miss critical issues until users complain</li>
</ul>
<p><strong>Grafana + Prometheus</strong> (enterprise grade)</p>
<ul>
<li>Setup: 1-2 days (learning curve: steep)</li>
<li>Maintenance: Constant updates &amp; configs</li>
<li>Cost: €0-50/month (infrastructure)</li>
<li>Features: 95% you'll never use</li>
<li>For small teams: Complete overkill</li>
</ul>
<p><strong>AWS CloudWatch / Datadog</strong> (SaaS)</p>
<ul>
<li>Cost: €100-500/month</li>
<li>Vendor lock-in</li>
<li>Overkill for basic monitoring</li>
<li>Learning curve: medium</li>
</ul>
<p><strong>What small teams actually need:</strong></p>
<ul>
<li>✅ Real-time CPU/memory/disk metrics</li>
<li>✅ Simple JSON output (integrate anywhere)</li>
<li>✅ Alert thresholds (notify when CPU &gt; 90%)</li>
<li>✅ Works on any Linux box</li>
<li>✅ No UI (just data, use what you want)</li>
<li>✅ Lightweight (doesn't slow servers down)</li>
</ul>
<h2 id="heading-my-solution-a-json-first-monitoring-script">My Solution: A JSON-First Monitoring Script</h2>
<p>I built <strong>Server Dashboard</strong> — a minimal Python script that collects server metrics and outputs clean JSON. No UI. No bloat. Just data.</p>
<p><strong>Here's what it does:</strong></p>
<ul>
<li>✅ Real-time metrics (CPU, memory, disk, network, processes)</li>
<li>✅ JSON output (integrate with Grafana, Prometheus, Slack, email, etc.)</li>
<li>✅ Threshold alerts (trigger actions when metrics exceed limits)</li>
<li>✅ Process tracking (identify memory hogs, dead services)</li>
<li>✅ Network statistics (bandwidth, connections, errors)</li>
<li>✅ Lightweight (typically &lt;1% CPU, &lt;10MB RAM)</li>
<li>✅ No dependencies (Python stdlib only)</li>
</ul>
<h2 id="heading-how-it-works">How It Works</h2>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-string">"""
Server Dashboard — Real-time system metrics as JSON
"""</span>

<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">import</span> psutil
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">import</span> subprocess

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ServerDashboard</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        self.timestamp = datetime.utcnow().isoformat()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_cpu_metrics</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""CPU usage per core + average"""</span>
        cpu_per_core = psutil.cpu_percent(interval=<span class="hljs-number">1</span>, percpu=<span class="hljs-literal">True</span>)
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"average"</span>: sum(cpu_per_core) / len(cpu_per_core),
            <span class="hljs-string">"per_core"</span>: cpu_per_core,
            <span class="hljs-string">"count"</span>: len(cpu_per_core)
        }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_memory_metrics</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Memory (RAM + Swap)"""</span>
        vm = psutil.virtual_memory()
        swap = psutil.swap_memory()
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"total_gb"</span>: vm.total / <span class="hljs-number">1e9</span>,
            <span class="hljs-string">"used_gb"</span>: vm.used / <span class="hljs-number">1e9</span>,
            <span class="hljs-string">"available_gb"</span>: vm.available / <span class="hljs-number">1e9</span>,
            <span class="hljs-string">"percent"</span>: vm.percent,
            <span class="hljs-string">"swap_total_gb"</span>: swap.total / <span class="hljs-number">1e9</span>,
            <span class="hljs-string">"swap_used_gb"</span>: swap.used / <span class="hljs-number">1e9</span>
        }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_disk_metrics</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Disk usage per partition"""</span>
        disks = {}
        <span class="hljs-keyword">for</span> partition <span class="hljs-keyword">in</span> psutil.disk_partitions():
            <span class="hljs-keyword">try</span>:
                usage = psutil.disk_usage(partition.mountpoint)
                disks[partition.mountpoint] = {
                    <span class="hljs-string">"total_gb"</span>: usage.total / <span class="hljs-number">1e9</span>,
                    <span class="hljs-string">"used_gb"</span>: usage.used / <span class="hljs-number">1e9</span>,
                    <span class="hljs-string">"free_gb"</span>: usage.free / <span class="hljs-number">1e9</span>,
                    <span class="hljs-string">"percent"</span>: usage.percent
                }
            <span class="hljs-keyword">except</span>:
                <span class="hljs-keyword">pass</span>
        <span class="hljs-keyword">return</span> disks

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_network_metrics</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Network I/O statistics"""</span>
        net = psutil.net_io_counters()
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"bytes_sent_mb"</span>: net.bytes_sent / <span class="hljs-number">1e6</span>,
            <span class="hljs-string">"bytes_recv_mb"</span>: net.bytes_recv / <span class="hljs-number">1e6</span>,
            <span class="hljs-string">"packets_sent"</span>: net.packets_sent,
            <span class="hljs-string">"packets_recv"</span>: net.packets_recv,
            <span class="hljs-string">"errors_in"</span>: net.errin,
            <span class="hljs-string">"errors_out"</span>: net.errout,
            <span class="hljs-string">"dropped_in"</span>: net.dropin,
            <span class="hljs-string">"dropped_out"</span>: net.dropout
        }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_process_metrics</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Top CPU/Memory consuming processes"""</span>
        processes = []
        <span class="hljs-keyword">for</span> proc <span class="hljs-keyword">in</span> psutil.process_iter([<span class="hljs-string">'pid'</span>, <span class="hljs-string">'name'</span>, <span class="hljs-string">'cpu_percent'</span>, <span class="hljs-string">'memory_percent'</span>]):
            <span class="hljs-keyword">try</span>:
                pinfo = proc.as_dict(attrs=[<span class="hljs-string">'pid'</span>, <span class="hljs-string">'name'</span>, <span class="hljs-string">'cpu_percent'</span>, <span class="hljs-string">'memory_percent'</span>])
                processes.append(pinfo)
            <span class="hljs-keyword">except</span>:
                <span class="hljs-keyword">pass</span>

        <span class="hljs-comment"># Sort and get top 5</span>
        top_cpu = sorted(processes, key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">'cpu_percent'</span>], reverse=<span class="hljs-literal">True</span>)[:<span class="hljs-number">5</span>]
        top_memory = sorted(processes, key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-string">'memory_percent'</span>], reverse=<span class="hljs-literal">True</span>)[:<span class="hljs-number">5</span>]

        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"top_cpu"</span>: top_cpu,
            <span class="hljs-string">"top_memory"</span>: top_memory
        }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_report</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">"""Generate complete metrics report"""</span>
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"timestamp"</span>: self.timestamp,
            <span class="hljs-string">"cpu"</span>: self.get_cpu_metrics(),
            <span class="hljs-string">"memory"</span>: self.get_memory_metrics(),
            <span class="hljs-string">"disk"</span>: self.get_disk_metrics(),
            <span class="hljs-string">"network"</span>: self.get_network_metrics(),
            <span class="hljs-string">"processes"</span>: self.get_process_metrics(),
            <span class="hljs-string">"uptime_seconds"</span>: int(datetime.now().timestamp() - psutil.boot_time())
        }

<span class="hljs-comment"># Usage</span>
dashboard = ServerDashboard()
report = dashboard.generate_report()
print(json.dumps(report, indent=<span class="hljs-number">2</span>))
</code></pre>
<p><strong>Output Example:</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2026-03-06T12:45:00Z"</span>,
  <span class="hljs-attr">"cpu"</span>: {
    <span class="hljs-attr">"average"</span>: <span class="hljs-number">42.5</span>,
    <span class="hljs-attr">"per_core"</span>: [<span class="hljs-number">45.2</span>, <span class="hljs-number">38.1</span>, <span class="hljs-number">41.0</span>, <span class="hljs-number">39.8</span>],
    <span class="hljs-attr">"count"</span>: <span class="hljs-number">4</span>
  },
  <span class="hljs-attr">"memory"</span>: {
    <span class="hljs-attr">"total_gb"</span>: <span class="hljs-number">16.0</span>,
    <span class="hljs-attr">"used_gb"</span>: <span class="hljs-number">10.2</span>,
    <span class="hljs-attr">"available_gb"</span>: <span class="hljs-number">5.8</span>,
    <span class="hljs-attr">"percent"</span>: <span class="hljs-number">63.75</span>
  },
  <span class="hljs-attr">"disk"</span>: {
    <span class="hljs-attr">"/"</span>: {
      <span class="hljs-attr">"total_gb"</span>: <span class="hljs-number">500.0</span>,
      <span class="hljs-attr">"used_gb"</span>: <span class="hljs-number">350.0</span>,
      <span class="hljs-attr">"free_gb"</span>: <span class="hljs-number">150.0</span>,
      <span class="hljs-attr">"percent"</span>: <span class="hljs-number">70.0</span>
    }
  },
  <span class="hljs-attr">"network"</span>: {
    <span class="hljs-attr">"bytes_sent_mb"</span>: <span class="hljs-number">1024.5</span>,
    <span class="hljs-attr">"bytes_recv_mb"</span>: <span class="hljs-number">2048.3</span>,
    <span class="hljs-attr">"errors_in"</span>: <span class="hljs-number">0</span>,
    <span class="hljs-attr">"dropped_out"</span>: <span class="hljs-number">2</span>
  },
  <span class="hljs-attr">"processes"</span>: {
    <span class="hljs-attr">"top_cpu"</span>: [
      {<span class="hljs-attr">"pid"</span>: <span class="hljs-number">1234</span>, <span class="hljs-attr">"name"</span>: <span class="hljs-string">"nginx"</span>, <span class="hljs-attr">"cpu_percent"</span>: <span class="hljs-number">8.5</span>},
      {<span class="hljs-attr">"pid"</span>: <span class="hljs-number">5678</span>, <span class="hljs-attr">"name"</span>: <span class="hljs-string">"python"</span>, <span class="hljs-attr">"cpu_percent"</span>: <span class="hljs-number">5.2</span>}
    ],
    <span class="hljs-attr">"top_memory"</span>: [
      {<span class="hljs-attr">"pid"</span>: <span class="hljs-number">9012</span>, <span class="hljs-attr">"name"</span>: <span class="hljs-string">"postgres"</span>, <span class="hljs-attr">"memory_percent"</span>: <span class="hljs-number">12.5</span>}
    ]
  },
  <span class="hljs-attr">"uptime_seconds"</span>: <span class="hljs-number">864000</span>
}
</code></pre>
<h2 id="heading-workflow-from-manual-checking-to-automated-monitoring">Workflow: From Manual Checking to Automated Monitoring</h2>
<pre><code>BEFORE (Manual - <span class="hljs-number">30</span> min/week per server)
SSH into server → free -h → df -h → top → write notes → repeat <span class="hljs-keyword">for</span> <span class="hljs-number">23</span> servers

AFTER (Automated - <span class="hljs-number">5</span> minutes setup, then passive)
Cron every <span class="hljs-number">5</span> min → Collect metrics → Send to Slack/File → Alert <span class="hljs-keyword">if</span> CPU&gt;<span class="hljs-number">90</span>% → Done
</code></pre><h2 id="heading-real-world-setup-5-minutes-per-server">Real-World Setup (5 Minutes per Server)</h2>
<p><strong>Step 1: Install</strong></p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/ben-automation/server-dashboard.git
<span class="hljs-built_in">cd</span> server-dashboard
pip install psutil
</code></pre>
<p><strong>Step 2: Create Cron Job</strong> (runs every 5 minutes)</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Add to crontab -e</span>
*/5 * * * * /usr/bin/python3 /opt/server-dashboard/dashboard.py &gt;&gt; /var/<span class="hljs-built_in">log</span>/metrics.json
</code></pre>
<p><strong>Step 3: (Optional) Slack Alerts</strong></p>
<pre><code class="lang-python"><span class="hljs-comment"># Add to script</span>
<span class="hljs-keyword">import</span> requests

<span class="hljs-keyword">if</span> report[<span class="hljs-string">'cpu'</span>][<span class="hljs-string">'average'</span>] &gt; <span class="hljs-number">90</span>:
    requests.post(
        <span class="hljs-string">"https://hooks.slack.com/services/YOUR/WEBHOOK/URL"</span>,
        json={<span class="hljs-string">"text"</span>: <span class="hljs-string">f"🚨 CPU Alert: <span class="hljs-subst">{report[<span class="hljs-string">'cpu'</span>][<span class="hljs-string">'average'</span>]}</span>%"</span>}
    )
</code></pre>
<p><strong>Step 4: (Optional) Integrate with Prometheus</strong></p>
<pre><code class="lang-python"><span class="hljs-comment"># Export in Prometheus format</span>
<span class="hljs-keyword">for</span> metric, value <span class="hljs-keyword">in</span> report.items():
    print(<span class="hljs-string">f"server_metric_<span class="hljs-subst">{metric}</span> <span class="hljs-subst">{value}</span>"</span>)
</code></pre>
<p><strong>Step 5: Collect Across All Servers</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Aggregator script</span>
<span class="hljs-keyword">for</span> server <span class="hljs-keyword">in</span> server1 server2 server3; <span class="hljs-keyword">do</span>
  ssh <span class="hljs-variable">$server</span> <span class="hljs-string">"python3 /opt/dashboard.py"</span> | jq . &gt;&gt; metrics.json
<span class="hljs-keyword">done</span>
</code></pre>
<h2 id="heading-comparison-what-youre-actually-paying-for">Comparison: What You're Actually Paying For</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Solution</td><td>Setup Time</td><td>Monthly Cost</td><td>Self-Hosted</td><td>Data Control</td><td>Bloat</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Server Dashboard</strong></td><td>5 min</td><td>€0</td><td>✅ Yes</td><td>100%</td><td>None</td></tr>
<tr>
<td>Manual Checking</td><td>N/A</td><td>€0 (time)</td><td>✅ Yes</td><td>100%</td><td>N/A</td></tr>
<tr>
<td>Prometheus + Grafana</td><td>1-2 days</td><td>€0-50</td><td>✅ Yes</td><td>100%</td><td>High</td></tr>
<tr>
<td>Datadog</td><td>30 min</td><td>€100-500</td><td>❌ No</td><td>Limited</td><td>High</td></tr>
<tr>
<td>New Relic</td><td>30 min</td><td>€200-800</td><td>❌ No</td><td>Limited</td><td>Very High</td></tr>
</tbody>
</table>
</div><p><strong>Reality:</strong> Grafana is powerful but designed for teams with dedicated DevOps. For 5-100 servers? You need JSON, not a UI.</p>
<h2 id="heading-lessons-learned">Lessons Learned</h2>
<p><strong>❌ Mistake 1: Trying to be Prometheus</strong>
→ Added too many features, script became bloated
→ Solution: Keep it simple. One job: collect metrics as JSON</p>
<p><strong>❌ Mistake 2: Not Handling Errors</strong>
→ One failed metric collection crashed the whole script
→ Solution: Try/except around each metric, skip gracefully</p>
<p><strong>✅ Lesson: JSON is Your API</strong>
→ JSON output means unlimited integration possibilities
→ Your script doesn't need to know about Slack/Grafana/whatever
→ Let users integrate however they want</p>
<h2 id="heading-what-makes-this-different">What Makes This Different</h2>
<ol>
<li><strong>No UI overhead</strong> — UI is free (use Grafana/Prometheus if you want)</li>
<li><strong>Lightweight</strong> — Barely uses CPU/memory (unlike Grafana)</li>
<li><strong>Your data</strong> — Self-hosted, not SaaS</li>
<li><strong>Flexible</strong> — Integrate anywhere (Slack, Prometheus, email, webhook)</li>
<li><strong>Simple</strong> — 185 lines of Python, not thousands</li>
</ol>
<h2 id="heading-next-steps-try-it-yourself">Next Steps: Try It Yourself</h2>
<p><strong>Option 1: DIY (Free)</strong></p>
<ul>
<li>Clone from GitHub</li>
<li>Run on any server</li>
<li>Collect JSON metrics</li>
</ul>
<p><strong>Option 2: Production-Ready Version (€34)</strong></p>
<ul>
<li>Pre-configured alert thresholds</li>
<li>Multi-server aggregation</li>
<li>Slack/email templates</li>
<li>Performance optimizations</li>
<li>Lifetime updates + support</li>
<li><a target="_blank" href="https://polar.sh/ben-automation">Get it on Polar →</a></li>
</ul>
<h2 id="heading-questions">Questions?</h2>
<ul>
<li><strong>Does it work on all Linux distros?</strong> → Yes, Ubuntu, Debian, CentOS, Alpine</li>
<li><strong>What about Windows?</strong> → This is Linux-only; Windows has different APIs</li>
<li><strong>Can I monitor remote servers?</strong> → Yes, SSH in and run, or expose HTTP endpoint</li>
<li><strong>How much CPU/memory does it use?</strong> → ~0.5% CPU, ~8MB RAM per execution</li>
<li><strong>What if I want a UI?</strong> → Export JSON to Prometheus/Grafana, use their UI</li>
</ul>
<hr />
<p><strong>The Bottom Line:</strong> Stop paying for bloated monitoring. Stop spending days on setup. Sometimes the best tool is the simplest tool.</p>
<p>What's your current monitoring setup? Still wrestling with Grafana? Or doing it manually? Tell me in the comments.</p>
<p><strong><a target="_blank" href="https://github.com/ben-automation/server-dashboard">Full code on GitHub</a> | <a target="_blank" href="https://polar.sh/ben-automation">Production version with templates →</a></strong></p>
]]></content:encoded></item><item><title><![CDATA[Tired of €50/Month Invoicing SaaS? Here's My Lightweight Python Solution]]></title><description><![CDATA[Tired of €50/Month Invoicing SaaS? Here's My Lightweight Python Solution
I invoice 8-12 clients monthly. For 3 years, I used every invoicing app imaginable: Wave, Zoho, Square, FreshBooks. Each one promised simplicity, each one cost between €20-80/mo...]]></description><link>https://bens-tech-blog.hashnode.dev/tired-of-50month-invoicing-saas-heres-my-lightweight-python-solution</link><guid isPermaLink="true">https://bens-tech-blog.hashnode.dev/tired-of-50month-invoicing-saas-heres-my-lightweight-python-solution</guid><category><![CDATA[Python]]></category><category><![CDATA[invoicing]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Ben]]></dc:creator><pubDate>Sat, 07 Mar 2026 08:05:07 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-tired-of-50month-invoicing-saas-heres-my-lightweight-python-solution">Tired of €50/Month Invoicing SaaS? Here's My Lightweight Python Solution</h1>
<p>I invoice 8-12 clients monthly. For 3 years, I used every invoicing app imaginable: Wave, Zoho, Square, FreshBooks. Each one promised simplicity, each one cost between €20-80/month, and each one felt like using a sledgehammer to hang a picture.</p>
<p>One day I realized: <strong>I'm paying €600+ annually for PDF generation.</strong></p>
<p>Then I built something that took 4 hours and cost €0.</p>
<h2 id="heading-the-problem-invoicing-software-is-designed-for-enterprises">The Problem: Invoicing Software is Designed for Enterprises</h2>
<p>As a freelancer or small agency, you have simple needs:</p>
<ul>
<li>Create professional-looking invoices</li>
<li>Include client details, line items, totals</li>
<li>Calculate taxes</li>
<li>Send to clients</li>
<li>Done.</li>
</ul>
<p>But the market gives you:</p>
<ul>
<li>€50-200/month SaaS platforms</li>
<li>Overly complex UIs designed for teams of 50+</li>
<li>Recurring subscriptions (you pay even if you don't invoice)</li>
<li>Vendor lock-in (exporting data is painful)</li>
<li>Months of learning curves</li>
</ul>
<p><strong>What we actually need:</strong></p>
<ul>
<li>✅ Professional PDFs (looks credible)</li>
<li>✅ Customizable (your brand, your colors)</li>
<li>✅ Batch generation (100 invoices in seconds)</li>
<li>✅ Affordable (one-time, not monthly)</li>
<li>✅ Self-hosted (your data, your infrastructure)</li>
</ul>
<h2 id="heading-my-solution-a-180-line-python-script">My Solution: A 180-Line Python Script</h2>
<p>I built <strong>Invoice Generator</strong> — a lightweight script that turns simple data (JSON or CSV) into professional, customizable PDF invoices. No subscriptions. No vendor lock-in. No BS.</p>
<p><strong>Here's what it does:</strong></p>
<ul>
<li>✅ Template-based generation (define once, reuse forever)</li>
<li>✅ JSON/CSV input support (integrate anywhere)</li>
<li>✅ Auto-calculated totals (items, taxes, discounts, subtotal)</li>
<li>✅ Multi-currency support (EUR, USD, GBP, CHF, etc.)</li>
<li>✅ Batch generation (50 invoices in 10 seconds)</li>
<li>✅ Custom invoice numbering (INV-2026-001, etc.)</li>
<li>✅ Company branding (logos, colors, custom fonts)</li>
</ul>
<h2 id="heading-how-it-works">How It Works</h2>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-string">"""
Invoice Generator — PDF invoices from data
"""</span>

<span class="hljs-keyword">from</span> reportlab.lib <span class="hljs-keyword">import</span> colors
<span class="hljs-keyword">from</span> reportlab.lib.pagesizes <span class="hljs-keyword">import</span> letter
<span class="hljs-keyword">from</span> reportlab.platypus <span class="hljs-keyword">import</span> SimpleDocTemplate, Table, TableStyle, Paragraph
<span class="hljs-keyword">from</span> reportlab.lib.styles <span class="hljs-keyword">import</span> getSampleStyleSheet, ParagraphStyle
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoiceGenerator</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, company_info</span>):</span>
        self.company = company_info

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate</span>(<span class="hljs-params">self, invoice_data, output_file</span>):</span>
        <span class="hljs-string">"""Generate PDF invoice"""</span>
        doc = SimpleDocTemplate(output_file, pagesize=letter)
        story = []
        styles = getSampleStyleSheet()

        <span class="hljs-comment"># Header</span>
        title = Paragraph(<span class="hljs-string">f"&lt;b&gt;INVOICE&lt;/b&gt;"</span>, styles[<span class="hljs-string">'Heading1'</span>])
        story.append(title)

        <span class="hljs-comment"># Company Info</span>
        company_text = <span class="hljs-string">f"""
        &lt;b&gt;<span class="hljs-subst">{self.company[<span class="hljs-string">'name'</span>]}</span>&lt;/b&gt;&lt;br/&gt;
        <span class="hljs-subst">{self.company[<span class="hljs-string">'address'</span>]}</span>&lt;br/&gt;
        <span class="hljs-subst">{self.company[<span class="hljs-string">'email'</span>]}</span> | <span class="hljs-subst">{self.company[<span class="hljs-string">'phone'</span>]}</span>
        """</span>
        story.append(Paragraph(company_text, styles[<span class="hljs-string">'Normal'</span>]))

        <span class="hljs-comment"># Invoice Details</span>
        invoice_info = <span class="hljs-string">f"""
        &lt;b&gt;Invoice #:&lt;/b&gt; <span class="hljs-subst">{invoice_data[<span class="hljs-string">'invoice_number'</span>]}</span>&lt;br/&gt;
        &lt;b&gt;Date:&lt;/b&gt; <span class="hljs-subst">{invoice_data[<span class="hljs-string">'date'</span>]}</span>&lt;br/&gt;
        &lt;b&gt;Due:&lt;/b&gt; <span class="hljs-subst">{invoice_data[<span class="hljs-string">'due_date'</span>]}</span>
        """</span>
        story.append(Paragraph(invoice_info, styles[<span class="hljs-string">'Normal'</span>]))

        <span class="hljs-comment"># Bill To</span>
        bill_to = <span class="hljs-string">f"""
        &lt;b&gt;Bill To:&lt;/b&gt;&lt;br/&gt;
        <span class="hljs-subst">{invoice_data[<span class="hljs-string">'client_name'</span>]}</span>&lt;br/&gt;
        <span class="hljs-subst">{invoice_data[<span class="hljs-string">'client_email'</span>]}</span>
        """</span>
        story.append(Paragraph(bill_to, styles[<span class="hljs-string">'Normal'</span>]))

        <span class="hljs-comment"># Line Items Table</span>
        items_data = [
            [<span class="hljs-string">'Description'</span>, <span class="hljs-string">'Qty'</span>, <span class="hljs-string">'Rate'</span>, <span class="hljs-string">'Amount'</span>],
        ]

        total = <span class="hljs-number">0</span>
        <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> invoice_data[<span class="hljs-string">'items'</span>]:
            amount = item[<span class="hljs-string">'qty'</span>] * item[<span class="hljs-string">'rate'</span>]
            total += amount
            items_data.append([
                item[<span class="hljs-string">'description'</span>],
                str(item[<span class="hljs-string">'qty'</span>]),
                <span class="hljs-string">f"€<span class="hljs-subst">{item[<span class="hljs-string">'rate'</span>]:<span class="hljs-number">.2</span>f}</span>"</span>,
                <span class="hljs-string">f"€<span class="hljs-subst">{amount:<span class="hljs-number">.2</span>f}</span>"</span>
            ])

        <span class="hljs-comment"># Subtotal, Tax, Total</span>
        tax = total * <span class="hljs-number">0.19</span>  <span class="hljs-comment"># 19% VAT</span>
        items_data.append([<span class="hljs-string">''</span>, <span class="hljs-string">''</span>, <span class="hljs-string">'Subtotal:'</span>, <span class="hljs-string">f"€<span class="hljs-subst">{total:<span class="hljs-number">.2</span>f}</span>"</span>])
        items_data.append([<span class="hljs-string">''</span>, <span class="hljs-string">''</span>, <span class="hljs-string">'VAT (19%):'</span>, <span class="hljs-string">f"€<span class="hljs-subst">{tax:<span class="hljs-number">.2</span>f}</span>"</span>])
        items_data.append([<span class="hljs-string">''</span>, <span class="hljs-string">''</span>, <span class="hljs-string">'&lt;b&gt;Total:&lt;/b&gt;'</span>, <span class="hljs-string">f"&lt;b&gt;€<span class="hljs-subst">{total + tax:<span class="hljs-number">.2</span>f}</span>&lt;/b&gt;"</span>])

        table = Table(items_data, colWidths=[<span class="hljs-number">250</span>, <span class="hljs-number">50</span>, <span class="hljs-number">100</span>, <span class="hljs-number">100</span>])
        table.setStyle(TableStyle([
            (<span class="hljs-string">'BACKGROUND'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), colors.grey),
            (<span class="hljs-string">'TEXTCOLOR'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), colors.whitesmoke),
            (<span class="hljs-string">'ALIGN'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>), <span class="hljs-string">'RIGHT'</span>),
            (<span class="hljs-string">'FONTNAME'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), <span class="hljs-string">'Helvetica-Bold'</span>),
            (<span class="hljs-string">'FONTSIZE'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), <span class="hljs-number">14</span>),
            (<span class="hljs-string">'BOTTOMPADDING'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">0</span>), <span class="hljs-number">12</span>),
            (<span class="hljs-string">'BACKGROUND'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">1</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>), colors.beige),
            (<span class="hljs-string">'GRID'</span>, (<span class="hljs-number">0</span>, <span class="hljs-number">0</span>), (<span class="hljs-number">-1</span>, <span class="hljs-number">-1</span>), <span class="hljs-number">1</span>, colors.black)
        ]))
        story.append(table)

        <span class="hljs-comment"># Notes</span>
        <span class="hljs-keyword">if</span> invoice_data.get(<span class="hljs-string">'notes'</span>):
            notes = <span class="hljs-string">f"&lt;b&gt;Notes:&lt;/b&gt; <span class="hljs-subst">{invoice_data[<span class="hljs-string">'notes'</span>]}</span>"</span>
            story.append(Paragraph(notes, styles[<span class="hljs-string">'Normal'</span>]))

        <span class="hljs-comment"># Build PDF</span>
        doc.build(story)

<span class="hljs-comment"># Usage</span>
company = {
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"Ben Automation"</span>,
    <span class="hljs-string">"address"</span>: <span class="hljs-string">"123 Tech Street, Berlin, Germany"</span>,
    <span class="hljs-string">"email"</span>: <span class="hljs-string">"hello@ben-automation.com"</span>,
    <span class="hljs-string">"phone"</span>: <span class="hljs-string">"+49 123 456789"</span>
}

generator = InvoiceGenerator(company)

invoice = {
    <span class="hljs-string">"invoice_number"</span>: <span class="hljs-string">"INV-2026-001"</span>,
    <span class="hljs-string">"date"</span>: <span class="hljs-string">"2026-03-06"</span>,
    <span class="hljs-string">"due_date"</span>: <span class="hljs-string">"2026-04-06"</span>,
    <span class="hljs-string">"client_name"</span>: <span class="hljs-string">"Acme Corp"</span>,
    <span class="hljs-string">"client_email"</span>: <span class="hljs-string">"accounting@acme.com"</span>,
    <span class="hljs-string">"items"</span>: [
        {<span class="hljs-string">"description"</span>: <span class="hljs-string">"Web Development (40h @ €150/h)"</span>, <span class="hljs-string">"qty"</span>: <span class="hljs-number">40</span>, <span class="hljs-string">"rate"</span>: <span class="hljs-number">150</span>},
        {<span class="hljs-string">"description"</span>: <span class="hljs-string">"API Integration"</span>, <span class="hljs-string">"qty"</span>: <span class="hljs-number">1</span>, <span class="hljs-string">"rate"</span>: <span class="hljs-number">500</span>},
    ],
    <span class="hljs-string">"notes"</span>: <span class="hljs-string">"Thank you for your business!"</span>
}

generator.generate(invoice, <span class="hljs-string">"INV-2026-001.pdf"</span>)
print(<span class="hljs-string">"✅ Invoice generated: INV-2026-001.pdf"</span>)
</code></pre>
<p><strong>Output: Professional PDF Invoice</strong></p>
<pre><code>┌──────────────────────────────────────┐
│           INVOICE                    │
│                                      │
│ Ben Automation                       │
│ <span class="hljs-number">123</span> Tech Street, Berlin              │
│                                      │
│ Invoice #: INV<span class="hljs-number">-2026</span><span class="hljs-number">-001</span>              │
│ <span class="hljs-built_in">Date</span>: <span class="hljs-number">2026</span><span class="hljs-number">-03</span><span class="hljs-number">-06</span>                     │
│ Due: <span class="hljs-number">2026</span><span class="hljs-number">-04</span><span class="hljs-number">-06</span>                      │
│                                      │
│ Bill To: Acme Corp                   │
│                                      │
│ Description    Qty  Rate    Amount   │
│ ─────────────────────────────────    │
│ Web Dev        <span class="hljs-number">40</span>   €<span class="hljs-number">150</span>    €<span class="hljs-number">6</span>,<span class="hljs-number">000</span>   │
│ API Integ      <span class="hljs-number">1</span>    €<span class="hljs-number">500</span>    €<span class="hljs-number">500</span>     │
│ ─────────────────────────────────    │
│ Subtotal:            €<span class="hljs-number">6</span>,<span class="hljs-number">500</span>          │
│ VAT (<span class="hljs-number">19</span>%):           €<span class="hljs-number">1</span>,<span class="hljs-number">235</span>          │
│ TOTAL:               €<span class="hljs-number">7</span>,<span class="hljs-number">735</span>          │
└──────────────────────────────────────┘
</code></pre><h2 id="heading-workflow-from-manual-to-automated">Workflow: From Manual to Automated</h2>
<pre><code>BEFORE (SaaS - €<span class="hljs-number">60</span>/month, manual)
Create client record → Add line items → Click calculate → Export PDF → Send → Wait <span class="hljs-keyword">for</span> payment

AFTER (My Script - €<span class="hljs-number">0</span>/month, automated)
Create <span class="hljs-built_in">JSON</span> → Run script → PDF ready → Integrate <span class="hljs-keyword">with</span> email/CRM → Automated reminders
</code></pre><h2 id="heading-real-world-setup-10-minutes">Real-World Setup (10 Minutes)</h2>
<p><strong>Step 1: Install</strong></p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/ben-automation/invoice-generator.git
<span class="hljs-built_in">cd</span> invoice-generator
pip install reportlab
</code></pre>
<p><strong>Step 2: Configure Your Company</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"company"</span>: {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Your Company"</span>,
    <span class="hljs-attr">"address"</span>: <span class="hljs-string">"123 Street, City"</span>,
    <span class="hljs-attr">"email"</span>: <span class="hljs-string">"you@company.com"</span>,
    <span class="hljs-attr">"phone"</span>: <span class="hljs-string">"+49 123 456789"</span>,
    <span class="hljs-attr">"logo"</span>: <span class="hljs-string">"path/to/logo.png"</span>
  }
}
</code></pre>
<p><strong>Step 3: Create Invoice Data</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"invoice_number"</span>: <span class="hljs-string">"INV-2026-001"</span>,
  <span class="hljs-attr">"date"</span>: <span class="hljs-string">"2026-03-06"</span>,
  <span class="hljs-attr">"due_date"</span>: <span class="hljs-string">"2026-04-06"</span>,
  <span class="hljs-attr">"client_name"</span>: <span class="hljs-string">"Client Name"</span>,
  <span class="hljs-attr">"client_email"</span>: <span class="hljs-string">"client@company.com"</span>,
  <span class="hljs-attr">"items"</span>: [
    {<span class="hljs-attr">"description"</span>: <span class="hljs-string">"Service"</span>, <span class="hljs-attr">"qty"</span>: <span class="hljs-number">10</span>, <span class="hljs-attr">"rate"</span>: <span class="hljs-number">100</span>}
  ],
  <span class="hljs-attr">"notes"</span>: <span class="hljs-string">"Payment due within 30 days"</span>
}
</code></pre>
<p><strong>Step 4: Generate</strong></p>
<pre><code class="lang-bash">python invoice-generator.py --input invoice.json --output INV-2026-001.pdf
</code></pre>
<p><strong>Step 5: Batch Generation (CSV)</strong></p>
<pre><code class="lang-bash">python invoice-generator.py --batch invoices.csv --output-dir ./invoices/
<span class="hljs-comment"># Generates: INV-2026-001.pdf, INV-2026-002.pdf, ... (50+ in seconds)</span>
</code></pre>
<h2 id="heading-lessons-learned">Lessons Learned</h2>
<p><strong>❌ Mistake 1: Not Validating Input Data</strong>
→ Missing item prices = corrupted invoice
→ Solution: Validate all fields before generating</p>
<p><strong>❌ Mistake 2: Hardcoding Tax Rates</strong>
→ Can't use for international clients
→ Solution: Make taxes configurable per invoice</p>
<p><strong>✅ Lesson: Templates are Your Friend</strong>
→ Define layout once, generate unlimited invoices
→ Changing brand? Update template, regenerate all (in seconds)</p>
<h2 id="heading-cost-comparison-true-annual-cost">Cost Comparison: True Annual Cost</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Solution</td><td>Startup</td><td>Monthly</td><td>Annual</td><td>Hidden Costs</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Invoice Generator (Mine)</strong></td><td>€0</td><td>€0</td><td>€0</td><td>None</td></tr>
<tr>
<td>Wave</td><td>€0</td><td>€0</td><td>€0</td><td>Data export fees</td></tr>
<tr>
<td>Zoho</td><td>€0</td><td>€0-20</td><td>€0-240</td><td>Payment processing</td></tr>
<tr>
<td>FreshBooks</td><td>€15</td><td>€15</td><td>€180+</td><td>Integrations</td></tr>
<tr>
<td>Square Invoices</td><td>€0</td><td>€0</td><td>€0</td><td>2.9% payment fee</td></tr>
</tbody>
</table>
</div><p><strong>Reality:</strong> One year with FreshBooks = one-time purchase of my script, then free forever.</p>
<h2 id="heading-next-steps-try-it-yourself">Next Steps: Try It Yourself</h2>
<p><strong>Option 1: DIY (Free)</strong></p>
<ul>
<li>Clone from GitHub</li>
<li>Follow 10-minute setup</li>
<li>Start generating invoices</li>
</ul>
<p><strong>Option 2: Production-Ready Version (€39)</strong></p>
<ul>
<li>Full error handling</li>
<li>Custom branding templates</li>
<li>Batch generation from CSV</li>
<li>Multi-currency support</li>
<li>Tax calculations for all countries</li>
<li>Lifetime updates + support</li>
<li><a target="_blank" href="https://polar.sh/ben-automation">Get it on Polar →</a></li>
</ul>
<h2 id="heading-questions">Questions?</h2>
<ul>
<li><strong>Does it support my currency?</strong> → Yes, any currency with symbol support</li>
<li><strong>Can I use custom fonts/colors?</strong> → Yes, full template customization</li>
<li><strong>How do I send invoices to clients?</strong> → Export PDF, integrate with your email/CRM</li>
<li><strong>What about recurring invoices?</strong> → Create from CSV template, batch generate</li>
</ul>
<hr />
<p><strong>The Bottom Line:</strong> Stop paying subscription fees for something you can own. Your invoicing can be simple, beautiful, and free.</p>
<p>What's your current setup? Still using SaaS? Tell me in the comments — I'm curious how others solve this.</p>
<p><strong><a target="_blank" href="https://github.com/ben-automation/invoice-generator">Full code on GitHub</a> | <a target="_blank" href="https://polar.sh/ben-automation">Production version with templates →</a></strong></p>
]]></content:encoded></item><item><title><![CDATA[I Built a GitHub Monitor That Saves Me 5 Hours/Week — Here's How]]></title><description><![CDATA[I Built a GitHub Monitor That Saves Me 5 Hours/Week — Here's How
I was managing 15+ GitHub repositories across different client projects. Every morning, I'd manually check each one: any new issues? Pull requests? Stars? It was exhausting, error-prone...]]></description><link>https://bens-tech-blog.hashnode.dev/i-built-a-github-monitor-that-saves-me-5-hoursweek-heres-how</link><guid isPermaLink="true">https://bens-tech-blog.hashnode.dev/i-built-a-github-monitor-that-saves-me-5-hoursweek-heres-how</guid><category><![CDATA[GitHub]]></category><category><![CDATA[Python]]></category><category><![CDATA[monitoring]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Ben]]></dc:creator><pubDate>Sat, 07 Mar 2026 08:05:03 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-i-built-a-github-monitor-that-saves-me-5-hoursweek-heres-how">I Built a GitHub Monitor That Saves Me 5 Hours/Week — Here's How</h1>
<p>I was managing 15+ GitHub repositories across different client projects. Every morning, I'd manually check each one: any new issues? Pull requests? Stars? It was exhausting, error-prone, and completely unmaintainable.</p>
<p>Then I got frustrated enough to build something.</p>
<h2 id="heading-the-problem-github-monitoring-is-broken">The Problem: GitHub Monitoring is Broken</h2>
<p>If you're managing multiple repositories, you know the pain:</p>
<p><strong>Manual Checking</strong> (~2 hours/week)</p>
<ul>
<li>Log into GitHub</li>
<li>Click through each repo</li>
<li>Check issues, PRs, stats</li>
<li>Update a spreadsheet</li>
<li>Miss important updates</li>
</ul>
<p><strong>GitHub Notifications</strong> (unreliable)</p>
<ul>
<li>Too many noise, actual important stuff gets lost</li>
<li>Doesn't show repo health metrics</li>
<li>No systematic tracking</li>
</ul>
<p><strong>Enterprise Solutions</strong> (expensive &amp; overkill)</p>
<ul>
<li>Grafana + Prometheus? That's a week of setup</li>
<li>GitHub apps from marketplace? €50-200/month</li>
<li>Overkill for small teams managing 5-20 repos</li>
</ul>
<p><strong>What we need:</strong></p>
<ul>
<li>✅ Systematic monitoring (not manual)</li>
<li>✅ Lightweight (minutes to setup, not days)</li>
<li>✅ Cost-effective (€0, not €50/month)</li>
<li>✅ Self-hosted (your data, your rules)</li>
</ul>
<h2 id="heading-my-solution-a-200-line-python-script">My Solution: A 200-Line Python Script</h2>
<p>I built <strong>GitHub API Monitor</strong> — a lightweight Python script that automatically monitors your repositories and delivers actionable insights.</p>
<p><strong>Here's what it does:</strong></p>
<ul>
<li>✅ Tracks stars, forks, open issues, pull requests</li>
<li>✅ Generates JSON reports (structured data)</li>
<li>✅ Runs via cron (fully automated)</li>
<li>✅ Slack/Email integration ready (examples included)</li>
<li>✅ No external dependencies (just Python 3.8+)</li>
<li>✅ Respects GitHub API rate limits</li>
</ul>
<h2 id="heading-how-it-works">How It Works</h2>
<pre><code class="lang-python"><span class="hljs-comment">#!/usr/bin/env python3</span>
<span class="hljs-string">"""
GitHub API Monitor — Simple, Powerful Repository Tracking
"""</span>

<span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GitHubMonitor</span>:</span>
    BASE_URL = <span class="hljs-string">"https://api.github.com"</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, token</span>):</span>
        self.token = token
        self.headers = {<span class="hljs-string">"Authorization"</span>: <span class="hljs-string">f"Bearer <span class="hljs-subst">{token}</span>"</span>}

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_repo_stats</span>(<span class="hljs-params">self, repo</span>):</span>
        <span class="hljs-string">"""Fetch repository statistics"""</span>
        url = <span class="hljs-string">f"<span class="hljs-subst">{self.BASE_URL}</span>/repos/<span class="hljs-subst">{repo}</span>"</span>
        response = requests.get(url, headers=self.headers)
        data = response.json()

        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"repo"</span>: repo,
            <span class="hljs-string">"stars"</span>: data.get(<span class="hljs-string">"stargazers_count"</span>),
            <span class="hljs-string">"forks"</span>: data.get(<span class="hljs-string">"forks_count"</span>),
            <span class="hljs-string">"open_issues"</span>: data.get(<span class="hljs-string">"open_issues_count"</span>),
            <span class="hljs-string">"watchers"</span>: data.get(<span class="hljs-string">"watchers_count"</span>),
            <span class="hljs-string">"last_updated"</span>: data.get(<span class="hljs-string">"updated_at"</span>)
        }

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_recent_activity</span>(<span class="hljs-params">self, repo</span>):</span>
        <span class="hljs-string">"""Fetch recent issues and PRs"""</span>
        issues_url = <span class="hljs-string">f"<span class="hljs-subst">{self.BASE_URL}</span>/repos/<span class="hljs-subst">{repo}</span>/issues?state=all&amp;sort=updated&amp;per_page=5"</span>
        prs_url = <span class="hljs-string">f"<span class="hljs-subst">{self.BASE_URL}</span>/repos/<span class="hljs-subst">{repo}</span>/pulls?state=all&amp;sort=updated&amp;per_page=5"</span>

        issues = requests.get(issues_url, headers=self.headers).json()
        prs = requests.get(prs_url, headers=self.headers).json()

        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"recent_issues"</span>: [{<span class="hljs-string">"title"</span>: i[<span class="hljs-string">"title"</span>], <span class="hljs-string">"state"</span>: i[<span class="hljs-string">"state"</span>]} <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> issues],
            <span class="hljs-string">"recent_prs"</span>: [{<span class="hljs-string">"title"</span>: p[<span class="hljs-string">"title"</span>], <span class="hljs-string">"state"</span>: p[<span class="hljs-string">"state"</span>]} <span class="hljs-keyword">for</span> p <span class="hljs-keyword">in</span> prs]
        }

<span class="hljs-comment"># Usage</span>
monitor = GitHubMonitor(<span class="hljs-string">"your_github_token"</span>)
stats = monitor.get_repo_stats(<span class="hljs-string">"torvalds/linux"</span>)
print(json.dumps(stats, indent=<span class="hljs-number">2</span>))
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"repo"</span>: <span class="hljs-string">"torvalds/linux"</span>,
  <span class="hljs-attr">"stars"</span>: <span class="hljs-number">187450</span>,
  <span class="hljs-attr">"forks"</span>: <span class="hljs-number">31200</span>,
  <span class="hljs-attr">"open_issues"</span>: <span class="hljs-number">1456</span>,
  <span class="hljs-attr">"watchers"</span>: <span class="hljs-number">8903</span>,
  <span class="hljs-attr">"last_updated"</span>: <span class="hljs-string">"2026-03-06T11:30:00Z"</span>
}
</code></pre>
<h2 id="heading-workflow-from-manual-to-automated">Workflow: From Manual to Automated</h2>
<pre><code>BEFORE (Manual - <span class="hljs-number">2</span> hours/week)
Morning → Open GitHub (<span class="hljs-number">15</span> tabs) → Check each repo → Note findings → Update spreadsheet → Miss critical updates

AFTER (Automated - <span class="hljs-number">5</span> minutes/week)
Every morning <span class="hljs-number">9</span> AM → Cron runs script → Generates <span class="hljs-built_in">JSON</span> → Sends Slack alert → Review <span class="hljs-keyword">in</span> <span class="hljs-number">5</span> minutes → Done
</code></pre><h2 id="heading-real-world-setup-5-minutes">Real-World Setup (5 Minutes)</h2>
<p><strong>Step 1: Get Your GitHub Token</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Generate at: https://github.com/settings/tokens</span>
<span class="hljs-comment"># Needs: repo (for private repos), public_repo (for public)</span>
</code></pre>
<p><strong>Step 2: Install &amp; Configure</strong></p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/ben-automation/github-monitor.git
<span class="hljs-built_in">cd</span> github-monitor
pip install requests

<span class="hljs-comment"># Create config.json</span>
cat &gt; config.json &lt;&lt; EOF
{
  <span class="hljs-string">"token"</span>: <span class="hljs-string">"your_github_token_here"</span>,
  <span class="hljs-string">"repos"</span>: [
    <span class="hljs-string">"facebook/react"</span>,
    <span class="hljs-string">"kubernetes/kubernetes"</span>,
    <span class="hljs-string">"torvalds/linux"</span>
  ],
  <span class="hljs-string">"output"</span>: <span class="hljs-string">"report.json"</span>
}
EOF
</code></pre>
<p><strong>Step 3: Run Once</strong></p>
<pre><code class="lang-bash">python github-monitor.py
cat report.json
</code></pre>
<p><strong>Step 4: Automate with Cron</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Every morning at 9 AM</span>
0 9 * * * /usr/bin/python3 /path/to/github-monitor.py &gt;&gt; /var/<span class="hljs-built_in">log</span>/github-monitor.log
</code></pre>
<p><strong>Step 5: (Optional) Send to Slack</strong></p>
<pre><code class="lang-python"><span class="hljs-comment"># Add to script</span>
<span class="hljs-keyword">import</span> subprocess
response = requests.post(
    <span class="hljs-string">"https://hooks.slack.com/services/YOUR/WEBHOOK/URL"</span>,
    json={<span class="hljs-string">"text"</span>: json.dumps(report)}
)
</code></pre>
<h2 id="heading-what-i-learned-building-this">What I Learned Building This</h2>
<p><strong>❌ Mistake 1: Ignoring Rate Limits</strong>
→ GitHub API: 60 req/hour (unauthenticated), 5000/hour (authenticated)
→ Solution: Check X-RateLimit headers, implement exponential backoff</p>
<p><strong>❌ Mistake 2: Polling Too Frequently</strong>
→ Hammering the API every minute = wasted quota
→ Solution: Run via cron, once per hour max</p>
<p><strong>✅ Lesson: Start Simple, Scale Later</strong>
→ Don't over-engineer. A simple script beats complicated infra 90% of the time.</p>
<h2 id="heading-comparison-tools-amp-costs">Comparison: Tools &amp; Costs</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Solution</td><td>Setup Time</td><td>Monthly Cost</td><td>Self-Hosted</td><td>Learning Curve</td></tr>
</thead>
<tbody>
<tr>
<td><strong>GitHub Monitor (My Script)</strong></td><td>5 min</td><td>€0</td><td>✅ Yes</td><td>Easy</td></tr>
<tr>
<td>Manual Checking</td><td>N/A</td><td>~€0 (time)</td><td>✅ Yes</td><td>N/A</td></tr>
<tr>
<td>Grafana + Prometheus</td><td>1-2 days</td><td>€0-50</td><td>✅ Yes</td><td>Hard</td></tr>
<tr>
<td>GitHub App Marketplace</td><td>30 min</td><td>€20-200</td><td>❌ No</td><td>Medium</td></tr>
<tr>
<td>Enterprise Monitoring</td><td>1-2 weeks</td><td>€200+</td><td>❌ No</td><td>Hard</td></tr>
</tbody>
</table>
</div><h2 id="heading-why-this-approach-works">Why This Approach Works</h2>
<ol>
<li><strong>No Dependencies:</strong> Just Python + requests</li>
<li><strong>Full Control:</strong> Your data, your rules, no vendor lock-in</li>
<li><strong>Flexible Output:</strong> JSON → integrate anywhere</li>
<li><strong>Reliable:</strong> Runs on any Linux box</li>
<li><strong>Fast to Setup:</strong> 5 minutes from zero to monitoring</li>
</ol>
<h2 id="heading-next-steps-try-it-yourself">Next Steps: Try It Yourself</h2>
<p><strong>Option 1: DIY (Free)</strong></p>
<ul>
<li>Clone the GitHub repo</li>
<li>Follow the setup above</li>
<li>Run and monitor</li>
</ul>
<p><strong>Option 2: Production-Ready Version (€29)</strong></p>
<ul>
<li>Full script with error handling</li>
<li>Slack/Email templates included</li>
<li>Performance optimizations</li>
<li>Lifetime updates + support</li>
<li><a target="_blank" href="https://polar.sh/ben-automation">Get it on Polar →</a></li>
</ul>
<h2 id="heading-questions">Questions?</h2>
<ul>
<li><strong>How do I handle multiple accounts?</strong> → Pass multiple tokens, track separately</li>
<li><strong>Can I monitor private repos?</strong> → Yes, just use a token with <code>repo</code> scope</li>
<li><strong>What if GitHub API changes?</strong> → The main endpoints are stable; I test regularly</li>
<li><strong>How much does it cost to run?</strong> → Free (GitHub free tier has 5000 API calls/hour)</li>
</ul>
<hr />
<p><strong>Final thought:</strong> Stop paying €50-200/month for bloated monitoring tools. Your monitoring can be simple, fast, and yours.</p>
<p>What's your current workflow? Manually checking? Let me know — I'm always interested in how teams solve this differently.</p>
<p><strong><a target="_blank" href="https://github.com/ben-automation/github-monitor">Full code on GitHub</a> | <a target="_blank" href="https://polar.sh/ben-automation">Production version with support →</a></strong></p>
]]></content:encoded></item></channel></rss>