Logging
Four built-in logs — actions, errors, queries and devices — each stored as a model, so you can see what happened in your app with almost no wiring.
The logs
| Log | Records |
|---|---|
ActionLog | User actions — who did what, and to which record. |
ErrorLog | PHP errors and uncaught exceptions. |
QueryLog | Slow database queries (past DB_LOG_TIME). |
DeviceLog | Devices added and removed on sign-in. |
Each one is read back the same way — a getList() / getTotal() pair fed by its
generated request — so wiring any of them to an admin screen looks alike.
Action logging
Actions are declared with attributes, so they are typed and translatable. Tag a class with
#[Section] to group its actions and each method with #[Action] — both take a name plus
per-language labels. Then record the action from inside that method with ActionLog::add(), passing the
generated Sec / Act cases (the current user and session are filled in for you):
use Framework\Log\Attr\Section;
use Framework\Log\Attr\Action;
use Framework\Log\ActionLog;
use Framework\System\Sec;
use Framework\System\Act;
#[Section("Products", en: "Products", es: "Productos")]
class ProductConstructor {
#[Action("Create", en: "Created a product", es: "Creó un producto")]
public static function create(ProductRequest $request): Response {
$productID = Product::create($request);
ActionLog::add(Sec::Products, Act::Create, dataID: $productID);
return Response::success("PRODUCT_CREATED");
}
}
Sec and Act enums are generated from these attributes, so
run ./framework build once after adding or changing a
#[Section] / #[Action] before you can reference Sec::Products or
Act::Create.Sessions & what is stored
Actions are not stored as a flat list — each one belongs to a session. When a user signs in the
framework opens a session row and keeps it open until they sign out, recording who it belongs to along with the
ip and userAgent they connected from. Every ActionLog::add() during that
sign-in is attached to it, so the log reads as a timeline of what happened in each visit — and you can see the
device and address behind a run of actions.
Each action row itself stores:
| Field | Holds |
|---|---|
sessionID | The sign-in session the action belongs to. |
credentialID / currentUser | Who performed it (and the impersonated user, when signed in as someone). |
module / action | The #[Section] and #[Action] it came from. |
dataID | The record it affected — what you pass as dataID. |
moduleName / actionName | The translated labels, ready to display. |
Viewing the log
To surface the log in an admin screen, expose a route that reads it back.
getAll() returns the filtered entries and getAmount() the total — hand both to
Response::data as a list and a total, which the
front end renders as a paginated table:
use Framework\Discovery\Attr\Route;
use Framework\System\Access;
use Framework\Log\ActionLog;
use Framework\Log\Schema\LogActionRequest;
use Framework\IO\Response;
class ActionLogController {
#[Route("/log/action/getAll", Access::Admin)]
public static function getAll(LogActionRequest $request): Response {
return Response::data([
"list" => ActionLog::getAll($request),
"total" => ActionLog::getAmount($request),
]);
}
}
LogActionRequest is the generated request for the log model, so its
fields — page, amount, credential, date range — filter and paginate the query straight from what the front end
sent. Pass a field => LogActionColumn map as a second argument to also filter by your own columns,
such as a client or organization.
Error log
PHP errors and uncaught exceptions are captured to the database on their own — you write nothing to record them.
Each entry stores the errorCode, errorLevel and errorText, the
file and line it happened on, a backtrace, and the
environment it came from. Repeats of the same error increment an
amount instead of piling up rows, so the list stays readable.
Expose them the same way as the action log, with getList() / getTotal(). Since an error
is something you fix, markResolved() clears an entry once it is handled:
use Framework\Discovery\Attr\Route;
use Framework\System\Access;
use Framework\Log\ErrorLog;
use Framework\Log\Schema\LogErrorRequest;
use Framework\IO\Response;
class ErrorLogController {
#[Route("/log/error/getAll", Access::Admin)]
public static function getAll(LogErrorRequest $request): Response {
return Response::data([
"list" => ErrorLog::getList($request),
"total" => ErrorLog::getTotal($request),
]);
}
#[Route("/log/error/markResolved", Access::Admin)]
public static function markResolved(LogErrorRequest $request): Response {
ErrorLog::markResolved($request->ids);
return Response::success("ERROR_LOG_RESOLVED");
}
}
Query log
Every database query slower than DB_LOG_TIME is recorded automatically, so a slow page has a trail
to follow. An entry stores the query expression, its environment, and the timings —
elapsedTime for the slowest run and totalTime across all of them. As with errors, the same
query increments an amount rather than adding a row, so you can sort by how often it hurts.
It is read and cleared exactly like the error log — getList() / getTotal() to list,
markResolved() once a query has been optimized:
use Framework\Discovery\Attr\Route;
use Framework\System\Access;
use Framework\Log\QueryLog;
use Framework\Log\Schema\LogQueryRequest;
use Framework\IO\Response;
class QueryLogController {
#[Route("/log/query/getAll", Access::Admin)]
public static function getAll(LogQueryRequest $request): Response {
return Response::data([
"list" => QueryLog::getList($request),
"total" => QueryLog::getTotal($request),
]);
}
#[Route("/log/query/markResolved", Access::Admin)]
public static function markResolved(LogQueryRequest $request): Response {
QueryLog::markResolved($request->ids);
return Response::success("QUERY_LOG_RESOLVED");
}
}
Device log
Each device a user adds or removes when signing in and out is recorded by
the framework through DeviceLog::added() and DeviceLog::removed(). An entry stores the
credentialID, the playerID of the device, the userAgent behind it, and a
wasAdded flag telling the two apart — so you can trace when a user started or stopped receiving
notifications on a device.
Expose the history with the same pair:
use Framework\Discovery\Attr\Route;
use Framework\System\Access;
use Framework\Log\DeviceLog;
use Framework\Log\Schema\LogDeviceRequest;
use Framework\IO\Response;
class DeviceLogController {
#[Route("/log/device/getAll", Access::Admin)]
public static function getAll(LogDeviceRequest $request): Response {
return Response::data([
"list" => DeviceLog::getList($request),
"total" => DeviceLog::getTotal($request),
]);
}
}
Retention
Every log grows forever if left alone, so each one has a deleteOld() that prunes entries past its
retention window. How long each log is kept is set in the environment:
| Key | Keeps |
|---|---|
ACTION_LOG_DELETE_DAYS | Action-log rows, in days. |
ERROR_LOG_DELETE_DAYS | Error-log rows. |
QUERY_LOG_DELETE_DAYS | Query-log rows. |
DEVICE_LOG_DELETE_DAYS | Device-log rows. |
Nothing prunes on its own — you schedule it. Expose one
console command that calls deleteOld() on each log:
use Framework\Discovery\Attr\ConsoleCommand;
use Framework\Log\ActionLog;
use Framework\Log\ErrorLog;
use Framework\Log\QueryLog;
use Framework\Log\DeviceLog;
class LogCommands {
#[ConsoleCommand("cleanLogs")]
public static function clean(): void {
ActionLog::deleteOld();
ErrorLog::deleteOld();
QueryLog::deleteOld();
DeviceLog::deleteOld();
}
}
Then run it from your server's cron once a day — the logs only need pruning at that granularity:
# crontab — daily at 05:00
0 5 * * * cd /srv/app && ./framework cleanLogs