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:
- Initializes the SQLite database with the following schema:
- Every minute, reads the log file.
- Parses each JSON line.
- Checks for duplicates based on timestamp and message.
- Inserts only new entries. (No duplicate entry)
- 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.
0 Comments