Week 10 of 16

Build: Add Logging Throughout the App

Create a centralized logger and instrument every operation in the Prompt Vault — adds, searches, deletes, and errors

Day 49 75 minutes Build

Day 49 of 80

Centralized Logging

In a multi-file app, you don't want to configure logging in every single module. Instead, you create one function — get_logger(name) — that any module can call to get a properly configured logger. The configuration (where to write, what format, what level) lives in one place.

One Logger Factory, Many Named Loggers

Python's logging system is hierarchical. All named loggers descend from the root logger. When you call logging.getLogger("prompt_vault.models"), you get a logger that belongs to the prompt_vault namespace. The name appears in every log line, so you can see at a glance which module wrote each message:

2026-04-06 14:23:01 [INFO] prompt_vault.models: Prompt created: [Kling] Golf swing 2026-04-06 14:23:02 [INFO] prompt_vault.vault: Loaded 5 prompts from disk 2026-04-06 14:23:05 [WARNING] prompt_vault.vault: Search found 0 results for "pika"

Step 1: Create logger.py

This file has one job: give any module a ready-to-use, properly configured logger. The if not logger.handlers check prevents duplicate handlers if get_logger is called multiple times with the same name.

logger.py Python
import logging
from pathlib import Path


def get_logger(name):
    """Return a configured logger for the given module name.

    Usage in any module:
        from logger import get_logger
        logger = get_logger(__name__)
    """
    logger = logging.getLogger(name)

    # Guard: don't add handlers if they already exist
    if not logger.handlers:
        logger.setLevel(logging.DEBUG)

        # Ensure the logs/ directory exists
        log_dir = Path("logs")
        log_dir.mkdir(exist_ok=True)

        # File handler: DEBUG and above → logs/vault.log
        fh = logging.FileHandler(log_dir / "vault.log")
        fh.setLevel(logging.DEBUG)
        fh.setFormatter(logging.Formatter(
            "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
            datefmt="%Y-%m-%d %H:%M:%S"
        ))

        # Console handler: WARNING and above → terminal
        ch = logging.StreamHandler()
        ch.setLevel(logging.WARNING)
        ch.setFormatter(logging.Formatter(
            "[%(levelname)s] %(message)s"
        ))

        logger.addHandler(fh)
        logger.addHandler(ch)

    return logger
Every module that calls get_logger(__name__) will log to the same logs/vault.log file. The Path("logs").mkdir(exist_ok=True) creates the logs directory automatically — no manual setup needed. The if not logger.handlers guard is critical: without it, importing a module multiple times (which Python does in test runners) would add duplicate handlers and print each message twice.

Step 2: Add Logging to PromptLibrary

Now update vault.py (or wherever your PromptLibrary class lives) to import the logger and log every meaningful operation.

vault.py (updated) Python
import json
from pathlib import Path
from models import Prompt
from exceptions import StorageError
from logger import get_logger

logger = get_logger(__name__)   # module-level logger


class PromptLibrary:
    def __init__(self, filepath="prompts.json"):
        self.filepath = Path(filepath)
        self.prompts = self._load()

    def _load(self):
        if not self.filepath.exists():
            logger.info(f"No file at {self.filepath}, starting empty")
            return []
        try:
            with self.filepath.open() as f:
                data = json.load(f)
            logger.info(f"Loaded {len(data)} prompts from {self.filepath}")
            return [Prompt(**p) for p in data]
        except json.JSONDecodeError as e:
            logger.error(f"Corrupt JSON in {self.filepath}: {e}")
            raise StorageError(f"Cannot read {self.filepath}: corrupt JSON")

    def add(self, platform, shot, prompt):
        p = Prompt(platform, shot, prompt)   # raises InvalidPlatformError/EmptyFieldError if invalid
        self.prompts.append(p)
        self._save()
        logger.info(f"Added prompt: [{p.platform}] {p.shot}")
        return p

    def delete(self, index):
        if 0 <= index < len(self.prompts):
            removed = self.prompts.pop(index)
            self._save()
            logger.info(f"Deleted prompt #{index}: [{removed.platform}] {removed.shot}")
            return removed
        else:
            logger.warning(f"Delete failed: index {index} out of range ({len(self.prompts)} prompts)")
            return None

    def search(self, query):
        q = query.lower()
        results = [
            p for p in self.prompts
            if q in p.shot.lower() or q in p.prompt.lower()
        ]
        logger.debug(f"Search '{query}': {len(results)} results")
        return results

    def _save(self):
        try:
            with self.filepath.open("w") as f:
                json.dump([vars(p) for p in self.prompts], f, indent=2)
            logger.debug(f"Saved {len(self.prompts)} prompts to {self.filepath}")
        except OSError as e:
            logger.error(f"Failed to save {self.filepath}: {e}")
            raise StorageError(f"Cannot write {self.filepath}")
Notice how each logging call tells the story: INFO for things that succeeded normally, WARNING for things that failed but are recoverable (like a bad index), ERROR for actual failures, and DEBUG for fine-grained details (like save confirmations) that you don't need to see every time.

Step 3: Run the App and Read the Log

Add some prompts, do some searches, try to delete an out-of-range index. Then open logs/vault.log and read the timeline.

2026-04-06 14:23:01 [INFO] __main__: Starting Prompt Vault CLI 2026-04-06 14:23:01 [INFO] vault: No file at prompts.json, starting empty 2026-04-06 14:23:04 [INFO] vault: Added prompt: [Kling] Golf swing follow-through 2026-04-06 14:23:04 [DEBUG] vault: Saved 1 prompts to prompts.json 2026-04-06 14:23:07 [INFO] vault: Added prompt: [Runway] Sunset timelapse 2026-04-06 14:23:07 [DEBUG] vault: Saved 2 prompts to prompts.json 2026-04-06 14:23:12 [DEBUG] vault: Search 'golf': 1 results 2026-04-06 14:23:15 [WARNING] vault: Delete failed: index 9 out of range (2 prompts) 2026-04-06 14:23:18 [INFO] vault: Deleted prompt #0: [Kling] Golf swing follow-through 2026-04-06 14:23:18 [DEBUG] vault: Saved 1 prompts to prompts.json

That log tells a complete story: startup, two adds, a search, a failed delete (with the reason), and a successful delete. If something breaks in production, you open this file and read what happened — in order, with exact timestamps.

This Is What Professional Software Looks Like

Every serious Python application has a log file. When a bug gets reported — "it failed on Tuesday around 2pm" — you open the log, go to Tuesday 2pm, and read exactly what the app was doing. Without logging, you're debugging blind. With logging, you have a complete record. You've just given your Prompt Vault that superpower.

End of Day Checklist

Tomorrow

Day 50 is the Week 10 Review — and a milestone. You've reached Day 50 of 80: the halfway point of the entire course. You'll review what you built this week and look ahead at Week 11, where you'll replace JSON storage with a real SQLite database.