DEV Community

Hive80-lab
Hive80-lab

Posted on

I automated my weekly client reports with 60 lines of Python — freelancers and solo IT people, steal this

I automated my weekly client reports with 60 lines of Python — freelancers and solo IT people, steal this

Every Friday, 4:30pm, same ritual: dig through logs, ssh into four boxes, remember what broke and when it got fixed, and turn it into a "Weekly Status Report" the client actually reads. 90 minutes, every week, forever. For years.

Then I did the math that finally broke me: 90 minutes × 48 weeks = 72 hours a year of copy-pasting my own work into an email. That's two full work weeks. I automated it in one evening. Here's the whole system — it's about 60 lines and it has run untouched for months.

The report that writes itself

The trick is to stop remembering and start harvesting. Everything the client cares about is already in your logs and your monitoring output. The script's job is only to turn machine events into human sentences.

#!/usr/bin/env python3
"""weekly_report.py — turn this week's machine events into a client-ready report."""
import subprocess, smtplib, ssl
from pathlib import Path
from datetime import datetime, timedelta
from email.message import EmailMessage

CLIENTS = {
    "acme": {"hosts": ["web01.acme", "db01.acme"], "to": "ops@acme.example"},
    "globex": {"hosts": ["app.globex"], "to": "it@globex.example"},
}
LOG_DIRS = ["/var/log/mytools", "/var/log/"]

def last_week_lines(patterns, logdir):
    """Pull lines from the past 7 days that match our event patterns."""
    since = (datetime.now() - timedelta(days=7)).strftime("%b %e")
    out = []
    for lf in Path(logdir).glob("*.log"):
        try:
            for line in lf.read_text(errors="ignore").splitlines():
                if since in line and any(p in line for p in patterns):
                    out.append(line.strip())
        except PermissionError:
            continue
    return out

def count_events(client):
    """Event patterns = what your own tooling writes: fixes, alerts, deploys."""
    fixes   = last_week_lines(["FIXED", "RESOLVED", "ROLLBACK"], logdir)
    alerts  = last_week_lines(["ALERT", "RESTORE_DRILL", "CRITICAL"], logdir)
    deploys = last_week_lines(["DEPLOY", "UPDATE-OK"], logdir)
    return fixes, alerts, deploys

def render(client, fixes, alerts, deploys):
    wk = (datetime.now() - timedelta(days=7)).strftime("%b %d")
    now = datetime.now().strftime("%b %d")
    L = [f"# Weekly Ops Report — {client}",
         f"**Period:** {wk} – {now}", "",
         "## Health at a glance",
         f"- Incidents handled & resolved: **{len(fixes)}**",
         f"- Automated alerts caught (and killed) before users noticed: **{len(alerts)}**",
         f"- Maintenance & updates applied: **{len(deploys)}**", ""]
    if fixes:
        L.append("## What we fixed")
        L += [f"- {f}" for f in fixes[:8]]
    if alerts:
        L.append("## Caught early by monitoring")
        L += [f"- {a}" for a in alerts[:8]]
    if not fixes and not alerts:
        L.append("## This week\n\nNo incidents. Monitoring and backup drills all green.")
    L += ["", "---", "*Generated automatically from live system logs — nothing hand-typed, nothing forgotten.*"]
    return "\n".join(L)

def send(to, subject, body):
    msg = EmailMessage()
    msg["From"], msg["To"], msg["Subject"] = "reports@yourstudio.com", to, subject
    msg.set_content(body)
    ctx = ssl.create_default_context()
    with smtplib.SMTP_SSL("smtp.yourprovider.com", 465, context=ctx) as s:
        s.login("reports@yourstudio.com", Path("/etc/report_smtp").read_text().strip())
        s.send_message(msg)

if __name__ == "__main__":
    import sys
    for client, cfg in CLIENTS.items():
        f, a, d = count_events(client)
        body = render(client, f, a, d)
        send(cfg["to"], f"[{client}] Weekly Ops Report", body)
        print(f"{client}: sent ({len(f)} fixes, {len(a)} alerts)")
Enter fullscreen mode Exit fullscreen mode

Why clients loved it more than the hand-written one

  • It never forgets. The 2am restart you fixed on your phone at 2:15am? It's in the log. It goes in the report. Hand-written reports always drop the invisible work — which is exactly the work clients are paying you for.
  • It proves the quiet weeks. "No incidents, all drills green" is a selling line when it's backed by data. Retention conversations get much easier when the client can see the machine-daily numbers.
  • It made me raise prices. A weekly artifact of work is the cheapest, strongest proof of value I've ever produced. Bills stop feeling arbitrary when the report arrives before the invoice.

The cron line

0 16 * * 5 /usr/local/bin/weekly_report.py >> /var/log/weekly_report.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Friday 4pm, every week. I stopped attending that ritual entirely. If the script errors, the log tells me — and I get a weekly alert instead of a daily chore.

The rest of the automation layer

The report script is one piece of a small army: overnight health checks, pager tests, restore drills (never trust a green backup tick without one), log harvesters. I packaged the whole layer:

Your Friday afternoons are worth more than copy-paste. Give the worst hour of your week to a script and take the 72 hours back.

Top comments (0)