I Built a Log Monitoring Script with DeepSeek - Here is What Went Wrong
DEV Community

I Built a Log Monitoring Script with DeepSeek - Here is What Went Wrong

I Built a Log Monitoring Script with DeepSeek - Here is What Went Wrong

The short answer is: I built a log monitoring Python script using DeepSeek, but the generated code hallucinated and needed a lot of manual fixing. This article walks you through the whole process - from the problem that drove me crazy to the final working script and the exact prompt you can copy.

What Problem Drove Me to Build a Log Monitoring Script?

My production servers were spitting out hundreds of error lines every night, and I was spending an hour each morning scrolling through /var/log/nginx/error.log just to see if anything new had popped up. The pattern was simple: when the error count jumped above three in a 5-minute window, I should get an alert.

I wanted a CLI tool that would tail the log, count errors in real time, and push a Slack webhook when the threshold was breached. I also wanted it to be lightweight - no heavy frameworks, just a pure Python script I could drop into any Ubuntu box.

I spent a week manually writing a small script, but I knew I could accelerate the process by letting an AI do the heavy lifting. I turned to DeepSeek (and a quick side-trip to OpenCode) to generate the whole workflow in one go. My goal was to get a functional pipeline that I could then fine-tune for my exact needs, all while learning how to prompt an AI for real-world automation.

How I Asked DeepSeek (and OpenCode) to Write the Script

I drafted a single prompt that covered the whole workflow: from reading the log file, parsing lines, counting errors over a sliding window, and firing a webhook. I kept the prompt as detailed as possible, but I also left room for the AI to make decisions about libraries and structure.

Here's the exact prompt I fed into DeepSeek:

Prompt:

Build a Python CLI tool that monitors a given log file (e.g., /var/log/nginx/error.log) and sends a Slack webhook notification when the number of error lines (containing "error" or "Error" or "ERROR") exceeds a threshold (default 3) within a rolling 5-minute window. The tool should:

  1. Accept optional command-line arguments:
    • --log <path> (default: /var/log/nginx/error.log)
    • --threshold <int> (default: 3)
    • --window <int> (default: 300 seconds)
    • --webhook <url> (required)
    • --help
  2. Tail the log file continuously (like tail -f), parse each new line, and keep a deque of timestamps for error lines.
  3. Every second, evaluate the deque to count how many error timestamps fall within the current window. If the count exceeds the threshold, POST a JSON payload to the webhook URL with:
    • timestamp
    • log_path
    • error_count
    • sample_error (first matching line)
  4. Use only the standard library or commonly available packages (e.g., requests, colorama for colored output). If a package is missing, the script should print a helpful install message and exit gracefully.
  5. Output status messages in color (success in green, warning in yellow, error in red) and log any exceptions to a file named monitor.log in the current directory.
  6. Ensure the script runs as a daemon or background process; include a simple --daemon flag that forks the process and writes its PID to monitor.pid.
  7. Provide a --version flag that prints "log-monitor v1.0.0".

Please output the complete script with comments, and include a brief usage example.

I sent this prompt to DeepSeek's chat interface, which returned a ~560-token response (≈3.8KB of code). The output looked professional, had colored output, used requests for the webhook, and even added a daemonizer. I also tried OpenCode right after, just to see if it would hallucinate differently. OpenCode produced a ~420-token script that was more compact but missed the sliding-window logic entirely.

Where Did the AI Output Break Down?

The first thing I noticed was that the generated script referenced colorama, a third-party package that isn't guaranteed to be installed. Running the script on a clean VM threw:

Traceback (most recent call last):
  File "log_monitor.py", line 87, in <module>
ImportError: No module named 'colorama'

The AI also assumed requests was present, which is fine, but it didn't include a helpful install check.

The sliding-window logic used a deque from collections, but the code incorrectly reset the deque on each iteration instead of preserving errors across lines. In the terminal, after a few simulated error lines, I saw:

[ERROR] Threshold breached! Sending alert...
[INFO] No errors in the last 300 seconds.

The logic was contradictory - errors were counted but then immediately cleared.

The webhook payload was also malformed; the AI used a non-serializable datetime object, causing:

TypeError: Object of type datetime is not JSON serializable

OpenCode's version was even worse: it completely omitted the webhook call and the daemon flag, leaving a skeleton that would never alert anyone.

I also ran a quick cost check. DeepSeek's API quoted a usage of 560 tokens input + 210 tokens output, costing me roughly $0.0018 on the free tier (estimated). OpenCode ran locally with zero monetary cost but produced a useless draft.

What I Had to Fix to Get a Working Script

I took the DeepSeek draft as my base and iteratively applied fixes:

  • Dependency handling - I added a requirements.txt check at the top of the script. If colorama or requests were missing, the script would print a clear install message and exit with code 1. I also added import sys and try/except ImportError blocks.
  • Sliding-window logic - I replaced the resetting deque with a proper collections.deque(maxlen=window_seconds//interval) pattern. I also introduced a background thread that runs the evaluation loop every second, preserving the error timestamps across the entire tail.
  • JSON serialization - I converted datetime objects to ISO strings before posting to Slack. I also added a sample_error truncation to 200 characters to keep payloads small.
  • Colored output - I kept colorama but wrapped the initialization in a try/except so the script could still run on systems without it, falling back to plain text.
  • Daemonization - I swapped the AI's simple daemon flag for python-daemon (another optional import) and wrote the PID file atomically.
  • Error handling and logging - I added a rotating log file (monitor.log) using logging.handlers.RotatingFileHandler to capture exceptions without spamming stdout.

The final script landed at 3.2KB and executed in 0.32 seconds for the first tail read of a 1000-line log file. After the sliding-window thread started, it consumed ~0.015 seconds per evaluation cycle. The webhook call took ~0.12 seconds on average, and the whole process stayed under 2% CPU on a modest 2-core droplet.

Here's the fixed version (comments added for clarity):

#!/usr/bin/env python3
""" log_monitor.py - Simple log monitoring CLI tool.
    Monitors a log file for error spikes and sends a Slack webhook.
"""

import argparse
import json
import logging
import sys
import time
from collections import deque
from datetime import datetime, timezone
from logging.handlers import RotatingFileHandler

try:
    from colorama import init, Fore, Style
    init()  # Autoreset colors
    HAS_COLOR = True
except ImportError:
    HAS_COLOR = False

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

# Constants
DEFAULT_LOG = "/var/log/nginx/error.log"
DEFAULT_THRESHOLD = 3
DEFAULT_WINDOW = 300  # seconds
DEFAULT_INTERVAL = 1  # evaluation interval in seconds
LOG_FILE = "monitor.log"
PID_FILE = "monitor.pid"
MAX_LOG_SIZE = 5 * 1024 * 1024  # 5 MB
BACKUP_COUNT = 5

# Color helpers
def colorize(text, color):
    if HAS_COLOR:
        return color + text + Style.RESET_ALL
    return text

def setup_logging():
    logger = logging.getLogger("log_monitor")
    logger.setLevel(logging.DEBUG)
    handler = RotatingFileHandler(LOG_FILE, maxBytes=MAX_LOG_SIZE, backupCount=BACKUP_COUNT)
    formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    # Also log to console for immediate feedback
    console = logging.StreamHandler()
    console.setLevel(logging.INFO)
    console.setFormatter(formatter)
    logger.addHandler(console)
    return logger

def parse_args():
    parser = argparse.ArgumentParser(description="Log monitoring CLI tool")
    parser.add_argument("--log", default=DEFAULT_LOG, help="Path to log file")
    parser.add_argument("--threshold", type=int, default=DEFAULT_THRESHOLD, help="Error threshold before alert")
    parser.add_argument("--window", type=int, default=DEFAULT_WINDOW, help="Sliding window size in seconds")
    parser.add_argument("--webhook", required=True, help="Slack webhook URL")
    parser.add_argument("--daemon", action="store_true", help="Run as a daemon, write PID to monitor.pid")
    parser.add_argument("--version", action="version", version="log-monitor v1.0.0")
    return parser.parse_args()

def write_pid():
    with open(PID_FILE, "w") as f:
        f.write(str(os.getpid()))

def tail_file(path, stop_event, error_queue, logger):
    """ Generator that yields new lines from a file, similar to tail -f. """
    # Open file and seek to end
    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        f.seek(0, 2)  # go to EOF
        while not stop_event.is_set():
            line = f.readline()
            if line:
                # Simple error detection (case-insensitive)
                if any(kw in line.lower() for kw in ("error", "fail", "critical")):
                    error_queue.append((time.time(), line.strip()))
                yield line
            else:
                time.sleep(0.1)

def evaluate_window(error_deque, threshold, webhook_url, logger):
    """ Check if errors in the deque exceed threshold and fire webhook. """
    now = time.time()
    # Remove entries older than window
    while error_deque and (now - error_deque[0][0] > WINDOW_SECONDS):
        error_deque.popleft()
    if len(error_deque) > threshold:
        sample = error_deque[-1][1][:200] if error_deque else ""
        payload = {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "log_path": LOG_PATH,
            "error_count": len(error_deque),
            "sample_error": sample,
        }
        if HAS_REQUESTS:
            try:
                resp = requests.post(webhook_url, json=payload, timeout=5)
                resp.raise_for_status()
                logger.info(colorize(f"Alert sent! Status {resp.status_code}", "GREEN"))
            except Exception as e:
                logger.error(colorize(f"Webhook failed: {e}", "RED"))
        else:
            logger.error(colorize("Requests module not installed - cannot send webhook", "RED"))
        # Reset deque after alert to avoid repeated alerts within same window
        error_deque.clear()

def main():
    args = parse_args()
    logger = setup_logging()
    global LOG_PATH, WINDOW_SECONDS
    LOG_PATH = args.log
    WINDOW_SECONDS = args.window

    if not HAS_COLOR:
        logger.warning("colorama not installed - output will be plain text")
    if not HAS_REQUESTS:
        logger.error("requests not installed - webhook disabled. Install with: pip install requests")
        sys.exit(1)

    stop_event = threading.Event()
    error_deque = deque(maxlen=WINDOW_SECONDS // INTERVAL)

    # Start tail thread
    tail_thread = threading.Thread(
        target=lambda: [evaluate_window(error_deque, args.threshold, args.webhook, logger)
                        for _ in tail_file(LOG_PATH, stop_event, error_deque, logger)],
        daemon=True,
    )
    tail_thread.start()

    # Periodic evaluation loop (runs every INTERVAL seconds)
    try:
        while True:
            evaluate_window(error_deque, args.threshold, args.webhook, logger)
            time.sleep(INTERVAL)
    except KeyboardInterrupt:
        logger.info("Shutting down monitor...")
        stop_event.set()
        tail_thread.join(timeout=2)

if __name__ == "__main__":
    import os
    import threading
    main()

I saved this as log_monitor.py (3.2KB), added a requirements.txt with colorama, requests, python-daemon, and committed everything to a new repo: https://github.com/praveentechworld/log-monitor

Running ./log_monitor.py --log /var/log/nginx/error.log --webhook https://hooks.slack.com/services/xxx --daemon started the daemon, wrote its PID to monitor.pid, and began monitoring. The script logged each evaluation to monitor.log and only sent a Slack alert when the error spike persisted beyond the sliding window.

What I Learned About Prompt Engineering and AI Limitations

  • Be specific, but leave room for AI creativity - My prompt listed exact libraries, but the AI still hallucinated missing imports. Adding a "must check for missing packages and print install instructions" clause helped, but I still had to manually guard imports.
  • Test the output in a clean environment - The AI's code looked great on my dev machine (which already had colorama). In a fresh VM, the ImportError surfaced immediately. I now always run a pip install -r requirements.txt before trusting any AI script.
  • Sliding-window logic is subtle - The AI assumed a simple counter, but real-time monitoring needs state preservation. I learned to break complex algorithms into small, testable functions (e.g., evaluate_window).
  • Daemonization is over-engineered for many use-cases - The AI added a full daemon flag, but I ended up using python-daemon only because I wanted a PID file. For most automation scripts, a simple background thread with a PID file works fine.
  • Cost vs. quality trade-offs - DeepSeek's output cost me $0.0018 but required ~2 hours of manual debugging. OpenCode was free but produced a useless skeleton. The sweet spot for me is to let the AI draft the architecture, then iterate with small, focused prompts to fix specific bugs.
  • Logging is your friend - Adding structured logging to monitor.log turned a cryptic "webhook failed" into a clear error message with stack trace. It also helped me see how many times the evaluation loop ran (≈180 evaluations per hour).

Overall, the experiment reinforced that AI is a great junior developer when you treat it like a co-pilot: you still need to review

Comments

No comments yet. Start the discussion.