DEV Community

Finley Zhou
Finley Zhou

Posted on

Free AI Log Triage: Cut Weekly Review to 15 Minutes

A free model classified 12,431 log lines from a production API and flagged 17 as suspicious; two were real. That ratio is the contract you should expect from free-tier AI—the other 15 false positives still saved me an hour of grepping and cut weekly review from two hours to fifteen minutes.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

This is a case study of a Node.js inventory API, plain-text logs, and a triage pipeline on MonkeyCode's free model access and free server option. The goal was not perfect detection. It was a short list of line numbers with reasons, so a human could make the final call.

Why I stopped grepping and started triaging

The API runs on a low-end VPS and writes every request, response, and error to /var/log/api/access.log. The file is plain text, rotated daily. Every week I would SSH in, grep for error, timeout, and ECONNREFUSED, and read the matches.

The ritual worked, but it was slow. Most hits were noise. Real anomalies hid inside bursts of expected errors. I wanted a first-pass filter that separated "look at this" from "ignore this"—closer to treating logs as an event stream, as in the Twelve-Factor App, except mine is a daily batch.

I did not want the model to decide what was broken. I wanted JSON with line_number, severity, reason, and suggested_action. The pipeline had to run unattended on a free server, cheap enough for a daily job.

Grep vs. this filter, in practice:

  • Grep returns every matching token; the model returns only lines that might indicate a problem.
  • Grep has no severity; the model labels high, medium, low, or unknown.
  • Grep cannot explain why a line matters; the reason field is what I actually read.

Five deliberately boring steps

Each step is small on purpose. The prompt is the only part I expected to tune.

Bound yesterday's logs

A cron job on the free server compresses yesterday's log and truncates the active file:

0 0 * * * tar -czf /var/log/api/$(date +\%F).tar.gz /var/log/api/access.log && : > /var/log/api/access.log
Enter fullscreen mode Exit fullscreen mode

The : > truncates without deleting the file. Bounding the input matters more than the model: a free tier has quotas, and a 200-line chunk is useless if you send a week of unrotated logs.

Prompt for JSON, then chunk the call

The prompt is the contract. Omit normal lines. Do not guess:

You are a log triage assistant. Analyze the log lines below.
For each line, output a JSON array with objects containing:
line_number, severity (one of "high", "medium", "low", "unknown"),
reason, suggested_action.
Only include lines that might indicate a problem.
If a line is normal, omit it.
Do not guess; if you are unsure, use severity "unknown".
Enter fullscreen mode Exit fullscreen mode

The script reads the log in chunks of 200 lines, posts each chunk to the MonkeyCode endpoint, and collects JSON. I used the endpoint from the MonkeyCode README; the exact URL and payload shape may differ, so adjust the code to your environment.

#!/usr/bin/env python3
import json
import os
import requests

LOG_FILE = "/var/log/api/access.log"
CHUNK_SIZE = 200

def chunk_lines(lines, size):
    for i in range(0, len(lines), size):
        yield i, lines[i:i+size]

def classify(lines, start):
    prompt = f"""You are a log triage assistant. Analyze the log lines below.
For each line, output a JSON array with objects containing:
line_number, severity, reason, suggested_action.
Only include lines that might indicate a problem.
Do not guess; if unsure, use severity "unknown".

Lines (starting at {start}):
{''.join(lines)}"""
    resp = requests.post(
        os.environ["MONKEYCODE_ENDPOINT"],
        headers={"Content-Type": "application/json"},
        json={"messages": [{"role": "user", "content": prompt}]},
        timeout=60,
    )
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]

def main():
    with open(LOG_FILE) as f:
        lines = f.readlines()
    all_flags = []
    for start, chunk in chunk_lines(lines, CHUNK_SIZE):
        raw = classify(chunk, start)
        try:
            flags = json.loads(raw)
        except json.JSONDecodeError:
            print(f"WARNING: chunk {start} returned invalid JSON, skipping")
            continue
        for flag in flags:
            flag["line_number"] += start
            all_flags.append(flag)
    with open("/var/log/api/flags.json", "w") as f:
        json.dump(all_flags, f, indent=2)
    print(f"Wrote {len(all_flags)} flags")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Too large a chunk, and the model truncates or drifts. Too small, and you burn extra requests. Two hundred lines worked for this format. Invalid JSON is skipped—not retried. Parsing with Python's json module makes the contract explicit: if the model does not return an array, that chunk is discarded.

Whitelist noise and schedule the job

The model over-flagged. Most false positives were health checks, retries, and slow-but-successful requests. Drop flags whose reason matches known noise:

WHITELIST = {"health check", "retry", "slow response"}

def is_noise(flag):
    return any(w in flag["reason"].lower() for w in WHITELIST)

with open("/var/log/api/flags.json") as f:
    flags = json.load(f)
filtered = [f for f in flags if not is_noise(f)]
with open("/var/log/api/flags.filtered.json", "w") as f:
    json.dump(filtered, f, indent=2)
Enter fullscreen mode Exit fullscreen mode

The whitelist is where domain knowledge lives. The model does not know your health checks; you do. It grows as you confirm which reasons are noise.

A final cron job runs triage, then sends a summary to a Telegram chat via a simple webhook, daily at 1 AM:

0 1 * * * cd /opt/log-triage && ./triage.py && ./notify.py
Enter fullscreen mode Exit fullscreen mode

notify.py reads the filtered JSON and sends the first ten items. If there are no items, it sends nothing.

What 12,431 lines actually produced

The first run produced 17 flags. After the whitelist, 12 remained. I read those 12 lines and found two real problems.

  1. Database connection pool exhaustion. A burst of ECONNREFUSED over a three-minute window. The model grouped them and assigned severity: high.
  2. Unhandled promise rejection. It only happened when a specific query parameter was missing. The error message was generic; the reason field pointed at the missing parameter.

The other ten flags were timeouts and retries that looked alarming but were expected under load. The whitelist missed them because their wording differed from patterns I had already added.

Two hours of grepping became fifteen minutes of reading a filtered list. The model did not find everything. It found the two lines I would have reached for last.

What I would repeat:

  • Treat a free model as a triage assistant, not a detective.
  • Tune the prompt first; it moved the false-positive rate more than chunk size or the whitelist.
  • Put domain knowledge in the whitelist, not in hope that the model "knows" health checks.
  • A free server makes the experiment cost nothing. The real cost is validating output.

Limitations—and who should skip this

This pattern fits small services where a weekly manual review is the baseline. Skip or adapt it when:

  • Log format changes. Accuracy drops until you update the prompt.
  • Logs contain PII. Do not send customer PII to an external API. Assume logs are scrubbed or non-sensitive; the OWASP Logging Cheat Sheet is a practical checklist before anything leaves the box.
  • Free-tier quotas. Check the MonkeyCode README for current numbers before you depend on it in production.
  • You need real-time alerts. This pipeline runs daily. It is after-the-fact review, not incident response.
  • Volume is huge. Millions of lines per day need a streaming solution, not a batch script.

If you are still grepping logs by hand, try this pattern with any free model endpoint. MonkeyCode's free tier is a convenient place to start, but the workflow is the point: the model scans; you decide.

Do this next: copy the prompt and the 200-line chunker, set MONKEYCODE_ENDPOINT from the MonkeyCode README, and run it once against a single day's log. Read every flag before you add cron or Telegram. Tune the prompt until the leftover list is a fifteen-minute job—then schedule it.

Top comments (0)