What happens when the model reading your staging logs cannot see half the window it is supposed to summarize? I ran that exact experiment for 48 hours on a free setup, and the first day humbled me more than the code ever did. Staging logs are loud enough to look important and quiet enough to skip, which made them a perfect candidate for an automated triage pass. I wanted short summaries that told me what broke and nothing more.
MonkeyCode's free model access kept the token-spending anxiety away, and its free server option gave the pipeline a cheap place to live between batches. Disclosure: This article was prepared as part of MonkeyCode's product outreach. My plan was simple: cut the logs into five-minute windows, ask a free model what happened in each one, then validate every noun it produced against ground truth. The model behaved; my assumptions about context did not.
What I tried
Three pieces made up the pipeline, and only two of them behaved.
- A bucket function that groups log lines into fixed five-minute windows.
- A strictly worded prompt that I called the evidence fence.
- A validator that rejects any hostname or error code it cannot verify.
import re
from collections import defaultdict
INVENTORY = {"web-1", "web-2", "api-1", "db-1"}
KNOWN_ERRORS = {"timeout", "refused", "retry", "ratelimit"}
WINDOW = 300 # seconds
def bucket_lines(lines):
buckets = defaultdict(list)
for line in lines:
ts = int(re.search(r"\[(\d+)\]", line).group(1))
buckets[ts // WINDOW].append(line)
return dict(buckets)
def fence_prompt(chunk, window_id):
return f"""You triage one log window. Follow every rule exactly.
Attached lines (window {window_id} only):
log
{chunk}
Return JSON: {{"summary": str, "hosts": [str], "errors": [str]}}
Rules:
1. Use ONLY the attached lines. You have no memory of earlier windows.
2. If a fact is missing, write UNKNOWN. Never infer it.
3. Never invent a hostname, timestamp, or error code.
4. If the window is empty, return hosts: [] and errors: []."""
python
def validate(parsed, window_id):
issues = []
for host in parsed.get("hosts", []):
if host not in INVENTORY:
issues.append(f"window {window_id}: invented host {host}")
for err in parsed.get("errors", []):
if err not in KNOWN_ERRORS:
issues.append(f"window {window_id}: unknown error {err}")
return issues
The bucket function was boring, the prompt was strict, and the validator was the part that eventually saved me. I fed every window through the fence, collected the JSON, and compared the rejects against what actually shipped that day.
What broke
The model filled gaps my own logs left open
Truncated windows turned absence into evidence. At hour thirty the model wrote api-3 is down and needs a restart, and api-3 had never been deployed; the line about api-2 timeout had been cut off, so the model completed the pattern. It did not say UNKNOWN, because the shape of the log suggested a host should be there. That one fabricated sentence taught me more than all the clean summaries combined.
Fixed token budgets beat clever summarization
My first version used variable windows sized to the message history so nothing would truncate. Chatty services blew past the limit, tail lines landed in context, and answers started mixing errors from two different deployments. The fix was boring: hard five-minute windows, one prompt per window, and zero sliding memory between calls.
Shape-only validation let hallucinations through
On day one I checked JSON before I checked truth. Perfect JSON with an invented host slipped straight into the summary because the schema was valid. Validators that parse are not validators that verify.
What I'd repeat
- The evidence fence: every rule exists to force an UNKNOWN instead of a guess.
- Fixed windows: variable sizing looked smart and leaked context everywhere.
- Entity-level validation: every host and error code matched against real inventories.
- The reject path: paging on REJECT and reviewing FLAG only.
| Signal | Without the fence | With the fence |
|---|---|---|
| Unknown hostname | accepted into the summary | REJECT, page someone |
| Timestamp outside window | mixed into the narrative | window dropped |
| Unknown error code | merged with a similar name | FLAG for a human |
Who should not copy this
Raw logs can contain secrets, so if your lines carry tokens or PII, do not send them to any remote model, free or paid. If your alerts need to fire in milliseconds, a regex threshold beats an LLM every time; use the model for prose summaries, not for paging decisions. If you need a provable audit trail of why an alert fired, deterministic filters give you more evidence than a generated explanation ever will. And free-model behavior and quotas change without warning, so treat this as a triage layer with a human on the loop, not a guarantee.
What I'd do next
Would I run the same 48 hours again? Yes, but with the fence from minute one instead of hour twenty. Keep the validator closer than the model, and never let a fluent summary make you trust a noun you cannot verify. If you try this on your own logs, tell me what your fence caught at hour thirty.
Top comments (0)