Sandbox99 Chronicles

Persistent Log Management: Dump JSON Logs into SQLite

Authelia Log Monitor

Written by Jose Mendez

Hi, I’m Jose Mendez, the creator of sandbox99.cc. with a passion for technology and a hands-on approach to learning, I’ve spent more than fifteen years navigating the ever-evolving world of IT.

Published May 10, 2025 | Last updated on May 28, 2025 at 7:09AM

Reading Time: 4 minutes

Introduction

In the world of self-hosted applications and microservices, structured logging is essential for debugging, monitoring, and long-term storage. JSON logs, like those generated by Authelia, are machine-readable but can become difficult to manage as they grow over time. Instead of relying on flat files or rotating logs manually, converting logs into a searchable, structured database format is a better long-term solution. The sandbox99.cc blog platform hosts multiple containerized web applications, including VS Code Server, Webtop (a Docker-based Linux environment), and Portainer. All these services are secured behind Authelia’s two-factor authentication system for enhanced protection.

In this guide, we’ll show how to build a lightweight Python script that monitors a JSON-based authelia.log file and stores unique entries into a SQLite database. This approach is ideal for small to mid-sized setups where running a full logging stack like ELK or Loki is overkill.

Why Store Logs in a Database?

  • 🔍 Searchability: Easily query logs using SQL.
  • 📅 Persistence: Avoid accidental deletion or loss from log rotation.
  • 🔢 Structure: Enforce consistent fields such as timestamp, level, username, and IP.
  • 📊 Analysis Ready: Export, analyze, or visualize logs more easily.

Log Format Assumption

This guide assumes that your authelia.log file consists of newline-delimited JSON entries, each containing fields like:

{"error":"read tcp 10.20.107.206:9091-\u003e10.20.107.120:59396: i/o timeout","level":"error","method":"GET","msg":"Request timeout occurred while handling request from client.","path":"/","remote_ip":"10.20.107.120","status_code":408,"time":"2025-05-08T23:28:29+08:00"}
{"error":"read tcp 10.20.107.206:9091-\u003e10.20.107.120:59364: i/o timeout","level":"error","method":"GET","msg":"Request timeout occurred while handling request from client.","path":"/","remote_ip":"10.20.107.120","status_code":408,"time":"2025-05-08T23:28:29+08:00"}
{"error":"user not found","level":"error","method":"POST","msg":"Error occurred getting details for user with username input 'superboi' which usually indicates they do not exist","path":"/api/firstfactor","remote_ip":"103.108.174.36","time":"2025-05-11T00:14:27+08:00"}
{"error":"user not found","level":"error","method":"POST","msg":"Error occurred getting details for user with username input 'superboi' which usually indicates they do not exist","path":"/api/firstfactor","remote_ip":"103.108.174.36","time":"2025-05-11T00:14:34+08:00"}
{"error":"user not found","level":"error","method":"POST","msg":"Error occurred getting details for user with username input 'aquaman' which usually indicates they do not exist","path":"/api/firstfactor","remote_ip":"103.108.174.36","time":"2025-05-11T00:14:45+08:00"}
{"error":"user not found","level":"error","method":"POST","msg":"Error occurred getting details for user with username input 'aquaman' which usually indicates they do not exist","path":"/api/firstfactor","remote_ip":"103.108.174.36","time":"2025-05-11T00:14:51+08:00"}

Project Structure

log_to_sqlite/
├── authelia_log_monitor.log
├── authelia_logs.db
└── authelia_log_monitor.py

The Python Script

Our script does the following:

  1. Initializes the SQLite database with the following schema:
  2. Every minute, reads the log file.
  3. Parses each JSON line.
  4. Checks for duplicates based on timestamp and message.
  5. Inserts only new entries. (No duplicate entry)
  6. Logs activity into authelia_log_monitor.log (but only when changes occur).
#!/usr/bin/env python3
# Author: Jose Mendez
# Date: 05/10/2025
# Blog site: sandbox99.cc
# Description: This script monitors the Authelia log file for new entries,
#              inserts them into an SQLite database, and avoids duplicates.
# This script is designed to run in a loop, checking the log file every minute.
# It requires Python 3 and the sqlite3 module.
# Usage: nohup sudo python3 authelia_log_monitor.py &

# --- Required Libraries ---
# pip install sqlite3

# --- Note ---
# This script assumes that the Authelia log file is in JSON format and
# that each line is a valid JSON object. Adjust the parsing logic if necessary.

# --- Import Libraries ---
import sqlite3
import time
import json
import os
import logging
from datetime import datetime

# --- Configuration ---
LOG_FILE_PATH = "/PATH_OF_YOUR_AUTHELIA/authelia.log"
DB_FILE_PATH = "/authelia_logs.db"
MONITOR_LOG_PATH = "/authelia_log_monitor.log"

# --- Logging Setup ---
logging.basicConfig(
    filename=MONITOR_LOG_PATH,
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)


# --- Initialize the SQLite Database ---
def initialize_db():
    try:
        conn = sqlite3.connect(DB_FILE_PATH)
        cursor = conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS logs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT NOT NULL,
                level TEXT,
                remote_ip TEXT,
                msg TEXT,
                raw_json TEXT,
                UNIQUE(timestamp, msg)
            )
        ''')
        conn.commit()
        conn.close()
        logging.info("Database initialized successfully.")
    except Exception as e:
        logging.error(f"Failed to initialize database: {e}")


# --- Fetch Existing Logs ---
def fetch_existing_logs():
    try:
        conn = sqlite3.connect(DB_FILE_PATH)
        cursor = conn.cursor()
        cursor.execute("SELECT timestamp, msg FROM logs")
        existing = set(cursor.fetchall())
        conn.close()
        return existing
    except Exception as e:
        logging.error(f"Error fetching existing logs: {e}")
        return set()


# --- Read Logs from File ---
def read_log_file():
    if not os.path.exists(LOG_FILE_PATH):
        logging.warning("Log file not found.")
        return []

    try:
        with open(LOG_FILE_PATH, 'r') as file:
            lines = file.readlines()
        return [json.loads(line.strip()) for line in lines if line.strip()]
    except Exception as e:
        logging.error(f"Error reading log file: {e}")
        return []


# --- Insert Unique Logs ---
def insert_new_logs(logs, existing_logs):
    try:
        conn = sqlite3.connect(DB_FILE_PATH)
        cursor = conn.cursor()
        new_count = 0

        for log in logs:
            timestamp = log.get("time")
            msg = log.get("msg")
            level = log.get("level")
            remote_ip = log.get("remote_ip")

            # Set placeholders if still None
            remote_ip = remote_ip if remote_ip else "-"

            if (timestamp, msg) not in existing_logs:
                try:
                    cursor.execute('''
                        INSERT INTO logs (timestamp, level, remote_ip, msg, raw_json)
                        VALUES (?, ?, ?, ?, ?)
                    ''', (timestamp, level, remote_ip, msg, json.dumps(log)))
                    new_count += 1
                except sqlite3.IntegrityError:
                    continue  # Skip duplicates

        conn.commit()
        conn.close()

        if new_count > 0:
            logging.info(f"Inserted {new_count} new log(s).")
    except Exception as e:
        logging.error(f"Error inserting logs: {e}")


# --- Main Monitoring Loop ---
def monitor_log_file():
    initialize_db()
    while True:
        logs = read_log_file()
        existing_logs = fetch_existing_logs()
        insert_new_logs(logs, existing_logs)
        time.sleep(60)


if __name__ == "__main__":
    monitor_log_file()

Running the Script

You can run the script in the background with:

nohup sudo python3 authelia_log_monitor.py &

This way, it continuously monitors the log file every minute without requiring Docker, cron, or external tools.

Querying the Logs

Once logs are inside SQLite, querying becomes easy:

sqlite3 authelia_logs.db
sqlite> .headers on
sqlite> .mode column
sqlite> SELECT timestamp,level,remote_ip,msg FROM logs WHERE level = 'error' ORDER BY timestamp DESC LIMIT 10;

Checking authelia_log_monitor.log

cat authelia_log_monitor.log

Final Thoughts

Managing and analyzing logs doesn’t always require complex infrastructure. With just Python and SQLite, you can automate the process of capturing structured logs and storing them in a searchable format. This method provides a reliable, low-overhead way to preserve important security and access events — especially from tools like Authelia that emit well-structured JSON.

Whether you’re running a small homelab or a lightweight Docker-based authentication gateway, storing logs in SQLite can offer a good balance of simplicity, portability, and efficiency. Consider extending this script to support filtering, rotation, or even integration with web dashboards in future iterations.

Calendar

September 2025
S M T W T F S
 123456
78910111213
14151617181920
21222324252627
282930  

Related Post