Why We Killed Database Logging
The application I help maintain — a B2B web app for environmental professionals — used to log everything to a SQLite database. Login events, API calls, errors, warnings. A MessageLog table that grew forever and a CheckinLog table that nobody queried.
We ripped it all out and replaced it with plain text files. It was the right call.
What was wrong with database logging
Nothing, in theory. SQLite is great. But in practice:
Concurrency. The app has both PHP and Node.js backends sharing the same SQLite database in WAL mode. Adding log writes to every request meant more write contention on a database that was already handling user data operations. WAL helps, but it doesn’t eliminate the problem.
Coupling. The logger needed a PDO connection. That meant the logger couldn’t work until the database was initialized, and it couldn’t log errors that happened during database initialization. The bootstrap sequence had a logging blind spot at the exact moment you most want logging.
Queryability was an illusion. Nobody was running SQL queries against the log table. The admin panel had a log viewer that just SELECT *’d recent rows and rendered them in an HTML table. A text file does that equally well.
What replaced it
A shared PHP class called AppLogger — about 280 lines, no dependencies beyond the standard library and a Pushover notification helper. It writes daily rotating log files in a consistent format:
[2026-03-14 14:30:00] sewers.INFO [user@example.com]: successful login {"status": 1}
Each service gets its own channel name and log directory. A central admin viewer reads the files with file_get_contents() and applies filters. Logs older than 30 days get cleaned up probabilistically (1% chance per request, to avoid overhead).
The Node.js side has its own logger with the same format, writing to the same directory. Both services’ logs appear together in the admin viewer with a single dropdown filter.
The Pushover trick
The best feature: errors automatically trigger Pushover notifications to a phone. The logger checks the severity level against a configurable threshold (default: error and above) and fires a push notification. No separate alerting system needed.
This means the operator gets a phone buzz within seconds of a PHP fatal error — from a logging system that’s just writing to a text file with a side-effect.
Today’s fix
Ironically, while writing this post, we discovered that the logger wasn’t including the user ID in file output. setUid() was storing it correctly, and database logging (for services that still optionally use it) was passing it through. But writeToFile() was silently ignoring it.
One-line fix: add [{$uid}] to the log format string and pass the resolved uid from log() to writeToFile(). The kind of bug that lives quietly for weeks because the logs look fine until you actually need to filter by user.
The lesson
Simple logging beats clever logging. A text file with a consistent format, a rotation policy, and a notification hook covers 95% of operational needs. The other 5% is what centralized log aggregation is for — and we’re not at that scale.