Build a Python Log Analyzer CLI Tool

Build a CLI tool that parses server logs and generates summary reports.

Introduction

30 Second Summary

Every server generates logs. When something breaks at 2am, the first thing an engineer does is dig through those log files looking for clues. Doing that manually is slow and error-prone.

In this project, you will build a Python command-line tool that reads server log files and produces a structured report showing error counts, severity breakdowns, and the most common failures.

What You'll Build

A Python CLI tool that parses server log files using regular expressions and outputs an actionable analysis report directly in your terminal.

By the end of this project, you'll have:

  • A sample server log file with realistic entries across multiple severity levels.
  • A Python script (log_analyzer.py) that uses regular expressions and dictionaries to parse and summarize log data.
  • A working CLI interface with argument parsing so you can filter and analyze logs from the terminal.
  • Secret Mission: Add keyword filtering and export your report to a JSON file.

Are there any prerequisites?

You just need a basic understanding of what Python is. No prior coding experience is required. This project uses only Python's built-in standard library, so there are no extra packages to install.

Before We Start

Before We Start...

Set Up Your Environment

Before you can analyze any logs, you need a working Python environment and a dedicated folder to hold your project files.

In this step, you will confirm Python is installed, create your project workspace on the Desktop, and verify everything connects inside VS Code.

In this step, get ready to:
  • Verify Python is installed and accessible from the terminal.
  • Create the project folder with two starter files.
  • Confirm Python works inside VS Code's integrated terminal.
Verify Python installation

Python is the language you will use throughout this project. You need to confirm it is installed and that the version is 3.8 or higher.

  • Press the Windows key to open your search bar.
  • Type Windows Terminal and press Enter to open it.
  • Check your Python version by running this command:
python --version

You should see output like Python 3.13.5 (or any version 3.8 or above).

✔️ I see version 3.8 or higher

Your Python installation is ready. Move on to the next substep.

ⓧ I see a version lower than 3.8

You have an outdated Python version that will not support features used in this project.

  • Open your browser and go to https://www.python.org/downloads/.
  • Download the latest Python installer for Windows.
  • Run the installer. Check the box labelled Add python.exe to PATH before clicking Install Now.
  • Close and reopen Windows Terminal, then run python --version again to confirm the new version.

ⓧ Command not found

Python is not installed on your machine yet.

  • Open your browser and go to https://www.python.org/downloads/.
  • Download the latest Python installer for Windows.
  • Run the installer. Check the box labelled Add python.exe to PATH before clicking Install Now.
  • Close and reopen Windows Terminal, then run python --version to verify the installation.

Why Add python.exe to PATH?

Adding Python to your PATH lets you run python from any terminal window without typing the full file path every time. Without this, Windows will not know where to find the Python executable.

Set up the project folder

You need a dedicated folder on your Desktop to keep all your project files organized and easy to find.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir log-analyzer
cd log-analyzer
  • Create the two project files by running these commands:
New-Item -ItemType File -Path .\log_analyzer.py
New-Item -ItemType File -Path .\sample.log

What does this command do?

New-Item -ItemType File creates an empty file in the current folder. You are creating two files: log_analyzer.py for your Python script and sample.log for sample log data.

  • Confirm the files exist by running:
ls

You should see log_analyzer.py and sample.log listed in the output.

  • Open this folder in VS Code by running:
code .

VS Code opens with your log-analyzer folder loaded. You should see both files listed in the Explorer sidebar on the left.

VS Code not opening?

If you see an error saying the code command is not recognized, the VS Code installer did not add it to your PATH. Close Windows Terminal, reinstall VS Code from https://code.visualstudio.com, and make sure the Add to PATH option is checked.

Then reopen Windows Terminal and try code . again.

Help me add VS Code to my PATH on Windows

Verify Python runs from VS Code

VS Code has a built-in terminal so you can run commands without switching windows. You need to confirm Python works here too.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (the backtick key, above Tab).
  • Run this command to confirm Python is accessible:
python -c "print('Ready to build!')"

You should see Ready to build! printed in the terminal output.

Seeing an error instead?

If Python is not recognized, VS Code's terminal may be using a different shell than Windows Terminal. Click the dropdown arrow next to the + icon in the terminal panel and select PowerShell as your shell. Then run the command again.

Help me fix Python not being recognized in VS Code's terminal

Your environment is ready. Next up, you will create realistic sample log data to give your analyzer something to work with.

Create Sample Log Data

Your environment is ready to go. Now you need data to work with.

Before you can build a log analyzer, you need realistic log entries to feed into it. In this step, you'll create a Python script that generates sample server logs with different severity levels and messages.

In this step, get ready to:
  • Learn the standard server log format and severity levels.
  • Write and run a Python script that generates sample log data.
Understand the log format

Server logs follow a predictable structure so that both humans and tools can read them. The format you'll use in this project is:

YYYY-MM-DD HH:MM:SS LEVEL message

Each log entry has four parts: a date, a time, a severity level, and a message describing what happened.

What are severity levels?

Real servers use severity levels to categorize how serious an event is. From least to most severe:

  • INFO means everything is working normally (a user logged in, a request was processed).
  • WARNING means something unusual happened but the system is still running (high memory, slow response times).
  • ERROR means something failed (a database connection dropped, a payment could not be processed).
  • CRITICAL means the system is in serious trouble (out of memory, security breach detected).
Write a Python script to generate sample logs

Instead of writing 30 log entries by hand, you'll use Python's random and datetime modules to generate varied, realistic data automatically.

  • In the VS Code Explorer sidebar on the left, click the New File icon (the page with a plus sign) at the top of the file list.
  • Type generate_logs.py and press Enter to create the file.
  • Paste the following code into generate_logs.py:
import random
from datetime import datetime, timedelta

def generate_sample_logs(filename, num_entries=30):
    # Weight toward INFO so the log looks realistic
    levels = ["INFO", "INFO", "INFO", "WARNING", "WARNING", "ERROR", "ERROR", "CRITICAL"]

    # Sample messages grouped by severity
    messages = {
        "INFO": [
            "User login successful: user123",
            "API request processed in 120ms",
            "Database connection established",
            "Cache refreshed successfully",
            "Scheduled backup completed",
            "Health check passed",
        ],
        "WARNING": [
            "High memory usage detected: 85%",
            "API response time above threshold: 2500ms",
            "Disk space below 20%",
            "Rate limit approaching for client 10.0.0.5",
        ],
        "ERROR": [
            "Database connection failed: timeout after 30s",
            "Failed to process payment for order #12345",
            "Authentication service unavailable",
            "Database connection failed: connection refused",
            "File not found: /var/data/config.yaml",
        ],
        "CRITICAL": [
            "System out of memory - initiating emergency shutdown",
            "Database cluster unreachable - all nodes down",
            "Security breach detected: unauthorized access attempt",
        ],
    }

    # Start generating from a fixed base time
    start_time = datetime(2026, 6, 9, 8, 0, 0)

    with open(filename, "w") as f:
        for i in range(num_entries):
            # Each entry is 1-5 minutes after the previous one
            timestamp = start_time + timedelta(minutes=random.randint(1, 5) * (i + 1))
            level = random.choice(levels)
            message = random.choice(messages[level])
            log_line = f"{timestamp.strftime('%Y-%m-%d %H:%M:%S')} {level} {message}\n"
            f.write(log_line)

    print(f"Generated {num_entries} log entries in '{filename}'")


if __name__ == "__main__":
    generate_sample_logs("sample.log")

What does this code do?

  • levels is a list with repeated entries so that INFO and WARNING appear more often than CRITICAL. This mimics real servers where most activity is routine.
  • messages is a dictionary mapping each severity level to a list of realistic messages that a real server might produce.
  • random.choice() picks a random level and a random message for that level on each iteration.
  • timedelta spaces each entry 1-5 minutes apart so the log looks like it came from a running server.
  • if __name__ == "__main__" ensures the script only runs when you execute it directly, not when imported.
  • Save generate_logs.py by pressing Ctrl+S.
  • Run the script to populate sample.log by running this command in your VS Code terminal:
python generate_logs.py

You should see the message: Generated 30 log entries in 'sample.log'.

  • Click on sample.log in the Explorer sidebar to open it.
  • You should see 30 lines of log entries, each following the format YYYY-MM-DD HH:MM:SS LEVEL message.

Don't see 30 log entries?

  • If you see Generated 30 log entries in 'sample.log' but the file looks empty, close and reopen sample.log in VS Code. The editor sometimes shows a cached version.
  • If you get a syntax error, double check that you copied all the code including the closing parentheses and quotation marks.
  • If the terminal says python is not recognized, try python3 generate_logs.py instead.

Why isn't my generate_logs.py script working?

✔️ Awesome, I've got everything!

Great. Double check you've saved generate_logs.py and confirmed that sample.log now has 30 lines of data.

ⓧ I'd like to double check the full code

Here is the complete generate_logs.py file:

import random
from datetime import datetime, timedelta

def generate_sample_logs(filename, num_entries=30):
    levels = ["INFO", "INFO", "INFO", "WARNING", "WARNING", "ERROR", "ERROR", "CRITICAL"]

    messages = {
        "INFO": [
            "User login successful: user123",
            "API request processed in 120ms",
            "Database connection established",
            "Cache refreshed successfully",
            "Scheduled backup completed",
            "Health check passed",
        ],
        "WARNING": [
            "High memory usage detected: 85%",
            "API response time above threshold: 2500ms",
            "Disk space below 20%",
            "Rate limit approaching for client 10.0.0.5",
        ],
        "ERROR": [
            "Database connection failed: timeout after 30s",
            "Failed to process payment for order #12345",
            "Authentication service unavailable",
            "Database connection failed: connection refused",
            "File not found: /var/data/config.yaml",
        ],
        "CRITICAL": [
            "System out of memory - initiating emergency shutdown",
            "Database cluster unreachable - all nodes down",
            "Security breach detected: unauthorized access attempt",
        ],
    }

    start_time = datetime(2026, 6, 9, 8, 0, 0)

    with open(filename, "w") as f:
        for i in range(num_entries):
            timestamp = start_time + timedelta(minutes=random.randint(1, 5) * (i + 1))
            level = random.choice(levels)
            message = random.choice(messages[level])
            log_line = f"{timestamp.strftime('%Y-%m-%d %H:%M:%S')} {level} {message}\n"
            f.write(log_line)

    print(f"Generated {num_entries} log entries in '{filename}'")


if __name__ == "__main__":
    generate_sample_logs("sample.log")

You now have 30 lines of realistic server log data ready to analyze. Next up, you'll write the code that parses each line and extracts the individual components using regular expressions.

Parse Log Entries with Regular Expressions

You now have a log file full of realistic server entries. The next challenge is turning those raw text lines into structured data your Python code can work with.

Each log line follows a predictable pattern: a date, a time, a severity level, and a message. Regular expressions let you describe that pattern so Python can automatically extract each piece. In this step, you will write functions that parse individual lines and read the entire log file into a list of dictionaries.

In this step, get ready to:
  • Write a regex pattern that extracts date, time, level, and message from each log line.
  • Build a function that reads and parses an entire log file.
  • Add error handling so the script exits cleanly when a file does not exist.
Write the regex pattern to extract log components

Each line in sample.log has four pieces of information separated by spaces. A regex pattern lets you capture each piece into a named group so you can store them in a Python dictionary.

  • In VS Code, open log_analyzer.py from the Explorer sidebar.
  • Copy the following code into the file:
import re


def parse_line(line):
    # Define the pattern: date, time, level, message
    pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.+)'
    match = re.match(pattern, line.strip())
    if match:
        return {
            "date": match.group(1),
            "time": match.group(2),
            "level": match.group(3),
            "message": match.group(4),
        }
    return None

What does this code do?

The re module is Python's built-in library for working with regular expressions.

  • r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.+)' describes the exact shape of a log line. Each set of parentheses captures one piece: the date, time, severity level, and message.
  • re.match() tries to match the pattern against the beginning of the line. If it matches, you get a match object. If not, it returns None.
  • match.group(1) through match.group(4) pull out each captured piece, which you then store in a dictionary with clear key names.

What is a regex pattern?

A regular expression (regex) is a sequence of characters that defines a search pattern. Think of it like a template that says exactly what shape the text should have.

In this pattern, \d{4} means "exactly four digits", \w+ means "one or more word characters", and .+ means "one or more of any character". The hyphens, colons, and spaces between them match themselves literally.

  • Save log_analyzer.py.
  • Test your function by running this command in the VS Code terminal:
python -c "from log_analyzer import parse_line; print(parse_line('2026-06-09 08:05:00 INFO Health check passed'))"

You should see a dictionary printed with keys date, time, level, and message, each holding the corresponding part of the log line.

Seeing a ModuleNotFoundError?

  • Make sure your terminal is in the log-analyzer folder. Run cd ~/Desktop/log-analyzer if you are not sure.
  • Check that the file is saved as log_analyzer.py (with an underscore, not a hyphen).

Why can't Python find my log_analyzer module?

Read and parse the entire log file

A single-line parser is useful, but you need a function that opens a file and processes every line at once. This function also needs to handle problems gracefully. If the file does not exist, the script should print a clear message instead of crashing with a traceback.

  • In log_analyzer.py, add the following function below parse_line():
def parse_log_file(filepath):
    entries = []
    try:
        with open(filepath, "r") as f:
            for line in f:
                entry = parse_line(line)
                if entry:
                    entries.append(entry)
    except FileNotFoundError:
        print(f"Error: File '{filepath}' not found.")
        raise SystemExit(1)
    except Exception as e:
        print(f"Error reading file: {e}")
        raise SystemExit(1)
    return entries

What does this function do?

  • with open(filepath, "r") as f: opens the file safely. Python automatically closes it when the block ends, even if an error occurs.
  • The for line in f: loop reads one line at a time. Each line is passed to parse_line(). If the line matches the pattern, the result gets appended to the entries list. If not, it is skipped silently.
  • The try/except block catches errors. FileNotFoundError handles the specific case where the file does not exist. The generic Exception catch handles any other unexpected issue.
  • raise SystemExit(1) stops the script with an exit code of 1, signalling that something went wrong.
  • Save log_analyzer.py.
  • Test the full parsing pipeline by running this command:
python -c "from log_analyzer import parse_log_file; entries = parse_log_file('sample.log'); print(f'Parsed {len(entries)} entries')"

You should see output like Parsed 30 entries (the exact number depends on how many lines in your sample.log matched the expected format).

Seeing 0 entries parsed?

  • Open sample.log and check that each line follows the format YYYY-MM-DD HH:MM:SS LEVEL message. Extra spaces or missing fields will cause the regex to skip that line.
  • Make sure you ran generate_logs.py in the previous step. If sample.log is empty, re-run it with python generate_logs.py.

Why is parse_log_file returning an empty list?

Test the error handling

Good error handling means your tool gives a human-readable message when something goes wrong. The try/except block you just wrote catches missing files before Python throws a confusing traceback.

  • Test what happens when you point the parser at a file that does not exist by running:
python -c "from log_analyzer import parse_log_file; parse_log_file('nonexistent.log')"

You should see Error: File 'nonexistent.log' not found. printed cleanly in the terminal. No traceback, no confusing stack of error lines.

Why does this matter?

In real DevOps workflows, scripts run unattended (inside cron jobs, CI pipelines, or monitoring systems). A clean error message makes it easy to diagnose problems from logs. A raw Python traceback buried in a pipeline log is much harder to read.

Seeing a traceback instead of a clean error?

  • Check that the with open(filepath, "r") line is indented inside the try: block. If it sits outside the try, the except clause cannot catch the error.
  • Make sure the except line reads except FileNotFoundError: exactly. A typo in the exception name means Python will not catch it.

Why is my try/except not catching the FileNotFoundError?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete log_analyzer.py at this point:

import re


def parse_line(line):
    pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.+)'
    match = re.match(pattern, line.strip())
    if match:
        return {
            "date": match.group(1),
            "time": match.group(2),
            "level": match.group(3),
            "message": match.group(4),
        }
    return None


def parse_log_file(filepath):
    entries = []
    try:
        with open(filepath, "r") as f:
            for line in f:
                entry = parse_line(line)
                if entry:
                    entries.append(entry)
    except FileNotFoundError:
        print(f"Error: File '{filepath}' not found.")
        raise SystemExit(1)
    except Exception as e:
        print(f"Error reading file: {e}")
        raise SystemExit(1)
    return entries

Your parser can now turn raw log text into structured Python dictionaries and handle missing files without crashing. Next up, you will use these parsed entries to count severity levels, find the most common errors, and produce a formatted report.

Analyze and Summarize the Log Data

Your parser can now read raw log files and structure them into Python dictionaries. That's the foundation, but a list of dictionaries isn't useful on its own.

In this step, you'll write analysis functions that count entries by severity level, surface the most frequent errors, and format everything into a clean report you can scan at a glance.

In this step, get ready to:
  • Count log entries by severity level using Python's Counter class.
  • Identify the top 5 most common error messages.
  • Generate a formatted summary report in the terminal.
Count entries by severity level

Python's standard library includes a Counter class that tallies how many times each item appears in a collection. You'll use it to count how many log entries exist at each severity level.

  • In log_analyzer.py, find the import re line at the top of the file.
  • Add this new import line directly below it:
from collections import Counter
  • Now scroll down below the parse_log_file() function and add the analyze_logs() function by copying the following code:
def analyze_logs(entries):
    level_counts = Counter(entry["level"] for entry in entries)

    error_entries = [
        entry for entry in entries
        if entry["level"] in ("ERROR", "CRITICAL")
    ]
    top_errors = Counter(
        entry["message"] for entry in error_entries
    ).most_common(5)

    time_range = {
        "start": f"{entries[0]['date']} {entries[0]['time']}" if entries else "N/A",
        "end": f"{entries[-1]['date']} {entries[-1]['time']}" if entries else "N/A",
    }

    return {
        "total_entries": len(entries),
        "level_counts": dict(level_counts),
        "top_errors": top_errors,
        "time_range": time_range,
    }

What does this code do?

  • level_counts uses Counter to tally how many entries exist for each severity level (INFO, WARNING, ERROR, CRITICAL).
  • error_entries filters the full list down to only ERROR and CRITICAL entries. A second Counter then counts the messages and .most_common(5) returns the top 5 most frequent error messages.
  • time_range grabs the timestamp of the first and last entries so the report can show what time window the log covers.
  • The function returns a dictionary containing all four analysis results, ready for the reporting function to display.

What is Counter?

Counter is a dictionary subclass from Python's collections module. It stores elements as keys and their tallies as values. You pass it any iterable and it counts everything for you in one line.

  • Save log_analyzer.py.
  • Test that the analysis works by running this command in your terminal:
python -c "from log_analyzer import parse_log_file, analyze_logs; entries = parse_log_file('sample.log'); analysis = analyze_logs(entries); print(analysis)"

You should see a dictionary printed in the terminal containing keys like total_entries, level_counts, top_errors, and time_range.

Seeing a ModuleNotFoundError?

  • Make sure the import line reads from collections import Counter exactly. A typo in collections or Counter (capital C) will cause this error.
  • Make sure you saved the file before running the command.

Why is my analyze_logs function failing?

Generate a formatted summary report

A raw dictionary output is useful for debugging, but a formatted report is what you'd actually want to read. The print_report() function will display the analysis as a structured, scannable summary.

  • Below the analyze_logs() function in log_analyzer.py, add the print_report() function by copying this code:
def print_report(analysis):
    print("=" * 50)
    print("           LOG ANALYSIS REPORT")
    print("=" * 50)
    print(f"\nTotal entries analyzed: {analysis['total_entries']}")
    print(f"Time range: {analysis['time_range']['start']} to {analysis['time_range']['end']}")

    print("\n--- Severity Breakdown ---")
    for level, count in sorted(analysis["level_counts"].items()):
        bar = "#" * count
        print(f"  {level:<10} {count:>4}  {bar}")

    print("\n--- Top Errors ---")
    if analysis["top_errors"]:
        for i, (message, count) in enumerate(analysis["top_errors"], 1):
            print(f"  {i}. [{count}x] {message}")
    else:
        print("  No errors found.")

    print("\n" + "=" * 50)

What does this code do?

  • The header and footer use "=" * 50 to create visual separators so the report stands out in the terminal.
  • The severity breakdown loops through each level and prints a bar chart made of # characters. More entries means a longer bar, giving you a quick visual sense of the distribution.
  • f-string formatting like {level:<10} left-aligns the level name in a 10-character space, while {count:>4} right-aligns the number. This keeps columns neatly lined up.
  • The top errors section lists them numbered with their occurrence count, so you can spot recurring problems immediately.
  • Save log_analyzer.py.
  • Run the full analysis pipeline with the formatted report by running this command:
python -c "from log_analyzer import parse_log_file, analyze_logs, print_report; entries = parse_log_file('sample.log'); analysis = analyze_logs(entries); print_report(analysis)"

You should see a structured report with a header, total entry count, time range, a severity breakdown with bar chart, and a numbered list of top errors.

Report not printing correctly?

  • Check that the indentation inside print_report() is consistent. Python is sensitive to indentation, so mixed tabs and spaces will cause an IndentationError.
  • If you see a KeyError, double check that the dictionary keys in print_report() match exactly what analyze_logs() returns: total_entries, level_counts, top_errors, and time_range.

Why is my print_report function throwing an error?

✔️ Awesome, I've got everything!

Great. Double check you've saved log_analyzer.py and your report is printing cleanly in the terminal.

ⓧ I'd like to double check the full code

Here is the full log_analyzer.py file as it should look at the end of this step:

import re
from collections import Counter


def parse_line(line):
    pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.+)'
    match = re.match(pattern, line.strip())
    if match:
        return {
            "date": match.group(1),
            "time": match.group(2),
            "level": match.group(3),
            "message": match.group(4),
        }
    return None


def parse_log_file(filepath):
    entries = []
    try:
        with open(filepath, "r") as f:
            for line in f:
                entry = parse_line(line)
                if entry:
                    entries.append(entry)
    except FileNotFoundError:
        print(f"Error: File '{filepath}' not found.")
        raise SystemExit(1)
    except Exception as e:
        print(f"Error reading file: {e}")
        raise SystemExit(1)
    return entries


def analyze_logs(entries):
    level_counts = Counter(entry["level"] for entry in entries)

    error_entries = [
        entry for entry in entries
        if entry["level"] in ("ERROR", "CRITICAL")
    ]
    top_errors = Counter(
        entry["message"] for entry in error_entries
    ).most_common(5)

    time_range = {
        "start": f"{entries[0]['date']} {entries[0]['time']}" if entries else "N/A",
        "end": f"{entries[-1]['date']} {entries[-1]['time']}" if entries else "N/A",
    }

    return {
        "total_entries": len(entries),
        "level_counts": dict(level_counts),
        "top_errors": top_errors,
        "time_range": time_range,
    }


def print_report(analysis):
    print("=" * 50)
    print("           LOG ANALYSIS REPORT")
    print("=" * 50)
    print(f"\nTotal entries analyzed: {analysis['total_entries']}")
    print(f"Time range: {analysis['time_range']['start']} to {analysis['time_range']['end']}")

    print("\n--- Severity Breakdown ---")
    for level, count in sorted(analysis["level_counts"].items()):
        bar = "#" * count
        print(f"  {level:<10} {count:>4}  {bar}")

    print("\n--- Top Errors ---")
    if analysis["top_errors"]:
        for i, (message, count) in enumerate(analysis["top_errors"], 1):
            print(f"  {i}. [{count}x] {message}")
    else:
        print("  No errors found.")

    print("\n" + "=" * 50)

Your analyzer can now turn raw log data into a clear summary. Next up, you'll wrap this into a proper CLI tool so you can run it from the terminal with different options.

Add CLI Argument Parsing

Your log analyzer can parse entries, count severity levels, and surface top errors. Right now though, you'd need to hard-code the file path and manually edit the script every time you want to analyze a different log file.

In this step, you'll add a proper command-line interface using Python's built-in argparse module. This turns your script into a flexible tool you can run from the terminal with different file paths and filter options.

In this step, get ready to:
  • Import and configure argparse for command-line argument parsing.
  • Write a main() function that wires CLI arguments to your analysis pipeline.
  • Test the complete tool with different commands and options.
Set up argparse

Right now, your script has no way to accept input from the terminal. The argparse module (part of Python's standard library) lets you define what arguments your script accepts and automatically handles parsing them from the command line.

What is argparse?

Think of argparse as a receptionist for your script. When someone runs your script from the terminal, argparse checks what they typed, makes sure it's valid, and hands the values to your code in a clean format.

It also auto-generates help text, so anyone can run python log_analyzer.py --help to see what options are available.

  • In log_analyzer.py, find your imports at the top of the file:
import re
from collections import Counter
  • Add import argparse between the two existing import lines, so your imports now look like this:
import re
import argparse
from collections import Counter
Wire up the CLI to the analysis functions

With argparse imported, you can now write a main() function that creates a parser, defines the arguments your tool accepts, and calls your existing analysis functions with whatever the user provides.

  • Add this main() function and the if __name__ guard at the bottom of log_analyzer.py, after the print_report() function:
def main():
    # Create the argument parser with a description
    parser = argparse.ArgumentParser(
        description="Analyze server log files and generate summary reports."
    )
    # Positional argument: the log file path is required
    parser.add_argument("filepath", help="Path to the log file to analyze")
    # Optional argument: filter by severity level
    parser.add_argument(
        "--level",
        choices=["INFO", "WARNING", "ERROR", "CRITICAL"],
        help="Filter entries by severity level",
    )
    args = parser.parse_args()

    # Run the analysis pipeline with parsed arguments
    entries = parse_log_file(args.filepath)

    if not entries:
        print("No valid log entries found.")
        raise SystemExit(0)

    # Apply level filter if the user provided --level
    if args.level:
        entries = [e for e in entries if e["level"] == args.level]
        print(f"Filtered to {len(entries)} entries with level: {args.level}\n")

    # Analyze and display the final report
    analysis = analyze_logs(entries)
    print_report(analysis)


# Only run main() when the script is executed directly
if __name__ == "__main__":
    main()

What does this code do?

This function is the entry point for your CLI tool. Here's what each section handles:

  • argparse.ArgumentParser() creates a parser object with a description that shows up in help text.
  • add_argument("filepath") defines a positional argument. Positional means it's required and doesn't need a -- prefix.
  • add_argument("--level", choices=[...]) defines an optional flag. The choices parameter restricts valid values to the four severity levels.
  • if args.level: filters the parsed entries using a list comprehension before passing them to the analysis.
  • if __name__ == "__main__": ensures main() only runs when you execute the script directly, not when importing it as a module.
  • Save log_analyzer.py.

Seeing a syntax error?

Make sure the main() function starts at the same indentation level (no indent) as your other functions like parse_line() and print_report(). The code inside main() should be indented by 4 spaces.

Also check that if __name__ == "__main__": is at the very bottom of the file with no indentation, and main() beneath it is indented by 4 spaces.

Help me debug a syntax error in my log_analyzer.py main() function

Test the complete tool

Your CLI tool is ready. Time to run it with different arguments to confirm everything works together.

  • In your VS Code terminal, run the basic analysis on your sample log file:
python log_analyzer.py sample.log

You should see the full log analysis report with total entries, severity breakdown, and top errors.

  • Run the tool again with the --level filter to see only ERROR entries:
python log_analyzer.py sample.log --level ERROR

You should see a message showing how many entries matched, followed by a report scoped to only ERROR-level entries.

  • Test the error handling by pointing the tool at a file that doesn't exist:
python log_analyzer.py nonexistent.log

You should see your friendly error message instead of a Python traceback.

What should I see?

The first command prints the full report with all 30 entries. The second command prints a filtered report showing only ERROR entries. The third command prints Error: File 'nonexistent.log' not found. and exits cleanly.

Getting 'No such file or directory' from Python itself?

Make sure your terminal is in the log-analyzer folder. Run ls (macOS/Linux) or dir (Windows) to confirm you see log_analyzer.py and sample.log in the listing.

Help me figure out why Python can't find my log_analyzer.py script

✔️ Awesome, I've got everything!

Great. Double check you've saved log_analyzer.py and all three commands produced the expected output.

ⓧ I'd like to double check the full code

Here is the complete log_analyzer.py as it should look at the end of this step:

import re
import argparse
from collections import Counter


def parse_line(line):
    pattern = r'(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) (.+)'
    match = re.match(pattern, line.strip())
    if match:
        return {
            "date": match.group(1),
            "time": match.group(2),
            "level": match.group(3),
            "message": match.group(4),
        }
    return None


def parse_log_file(filepath):
    entries = []
    try:
        with open(filepath, "r") as f:
            for line in f:
                entry = parse_line(line)
                if entry:
                    entries.append(entry)
    except FileNotFoundError:
        print(f"Error: File '{filepath}' not found.")
        raise SystemExit(1)
    except Exception as e:
        print(f"Error reading file: {e}")
        raise SystemExit(1)
    return entries


def analyze_logs(entries):
    level_counts = Counter(entry["level"] for entry in entries)
    
    error_entries = [
        entry for entry in entries
        if entry["level"] in ("ERROR", "CRITICAL")
    ]
    top_errors = Counter(
        entry["message"] for entry in error_entries
    ).most_common(5)
    
    time_range = {
        "start": f"{entries[0]['date']} {entries[0]['time']}" if entries else "N/A",
        "end": f"{entries[-1]['date']} {entries[-1]['time']}" if entries else "N/A",
    }
    
    return {
        "total_entries": len(entries),
        "level_counts": dict(level_counts),
        "top_errors": top_errors,
        "time_range": time_range,
    }


def print_report(analysis):
    print("=" * 50)
    print("           LOG ANALYSIS REPORT")
    print("=" * 50)
    print(f"\nTotal entries analyzed: {analysis['total_entries']}")
    print(f"Time range: {analysis['time_range']['start']} to {analysis['time_range']['end']}")
    
    print("\n--- Severity Breakdown ---")
    for level, count in sorted(analysis["level_counts"].items()):
        bar = "#" * count
        print(f"  {level:<10} {count:>4}  {bar}")
    
    print("\n--- Top Errors ---")
    if analysis["top_errors"]:
        for i, (message, count) in enumerate(analysis["top_errors"], 1):
            print(f"  {i}. [{count}x] {message}")
    else:
        print("  No errors found.")
    
    print("\n" + "=" * 50)


def main():
    parser = argparse.ArgumentParser(
        description="Analyze server log files and generate summary reports."
    )
    parser.add_argument("filepath", help="Path to the log file to analyze")
    parser.add_argument(
        "--level",
        choices=["INFO", "WARNING", "ERROR", "CRITICAL"],
        help="Filter entries by severity level",
    )
    args = parser.parse_args()

    entries = parse_log_file(args.filepath)

    if not entries:
        print("No valid log entries found.")
        raise SystemExit(0)

    if args.level:
        entries = [e for e in entries if e["level"] == args.level]
        print(f"Filtered to {len(entries)} entries with level: {args.level}\n")

    analysis = analyze_logs(entries)
    print_report(analysis)


if __name__ == "__main__":
    main()

Your log analyzer is now a proper CLI tool. You can point it at any log file and get instant insights, filter by severity, and handle errors gracefully. Next up, you'll take this further in the Secret Mission by adding keyword search and JSON export.

Secret mission

Add Keyword Filtering and JSON Export

Your log analyzer works great for quick terminal reports. But what if you need to search for a specific keyword across all entries, or save the results to a file that another tool can read? In this mission, you'll add both capabilities to make your tool automation-ready.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files for future use or delete them entirely.

Resources you used:

  • The log-analyzer folder on your Desktop (contains log_analyzer.py, generate_logs.py, sample.log, and report.json).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your log analyzer or point it at real log files from your own projects.

  • Your log-analyzer folder stays on your Desktop, ready to use anytime.
  • Run python log_analyzer.py on any log file to analyze it.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files will be waiting for you whenever you come back.

  • Close VS Code to free up system memory.
  • Your project files remain in the log-analyzer folder on your Desktop.

Delete - I don't want to use this again

Remove all project files to start fresh. This deletes everything you built in this project.

  • Close VS Code.
  • Delete the project folder by running the command for your operating system below.

macOS

rm -rf ~/Desktop/log-analyzer

Windows

rmdir /s /q %USERPROFILE%\Desktop\log-analyzer

Nice Work!

Nice Work!

Nice work! You just built a fully functional Python CLI tool that turns messy server logs into clean, actionable reports.

You've learned how to:

  • Build a complete Python CLI tool that parses server logs using regular expressions and produces structured analysis reports.
  • Use core Python fundamentals (functions, file I/O, dictionaries, loops, conditionals) to solve a real-world DevOps problem.
  • Create a proper CLI interface with argparse, including positional arguments, optional flags, and collections.Counter for data analysis.
  • Secret Mission: Added keyword filtering and JSON export so your tool can slot into automated DevOps workflows.

Ready to quiz yourself?