Decoupled JSON Log
This module allows decoupled applications and mobile apps to send frontend error logs directly to Drupal. It provides a private, self-hosted logging solution without third-party dependencies, storing logs as entities that can be managed with Drupal's existing tools like Views. The module includes features like rate limiting and payload size restrictions to prevent abuse and manage resources effectively.
Overview
Collect frontend error logs from your decoupled/mobile app directly into Drupal to realize self-hosted, private logging with no third-party dependencies or per-event billing.
This module is intended for small-scale apps so that you can handle your own logging and users' crash data never leaves your server. Also, it might save you a little money.
The module provides a JSON Log entity type (machine name: log_json) and REST/JSON:API endpoints for uploading the logs. You can log the device info and a log payload (errors are included by default, but you can add whatever you want).
Features
- Send a JSON payload via REST or JSON:API from your frontend (mobile app) to log data in Drupal.
- Non admins can only create logs; secure configuration by default.
- Logs are just entities, so you can use Views, bulk operations, and other Drupal contrib modules (e.g., ECA) for emails and other alerts.
- Categorize logs by creating new entity types (e.g.,
warning,notification) to fit your logging needs. - Endpoint rate limiting and payload size limiting can be customized to your situation.
Dependencies
This module stores log entries as JSON, so it uses the JSON Field module.
Post-Installation
Go to /admin/people/permissions and give the Create json logs permission to all roles that should be able to create logs.
Determine what you want to log. By default, this module provides an error bundle for logging errors. You can add other log types at /admin/structure/log_json_types.
Go to /admin/config/decoupled_json_log and confirm the rate limit and payload size settings, described below.
Configuration
Rate limiting
To stop a buggy or malicious frontend from flooding your site with log entries, each user is limited to a maximum number of log entries per rolling time interval:
-
Count for anonymous users (
rate_limit.count_anon, default500): the maximum number of entries the anonymous user can create during the interval. Because every anonymous visitor shares the anonymous account, this limit is shared across all of them, so it is usually set higher than the authenticated limit. -
Count for authenticated users (
rate_limit.count_auth, default50): the maximum number of entries each authenticated user can create during the interval. -
Interval (
rate_limit.interval_seconds, default86400, i.e. one day): the length of the rolling window, in seconds, over which entries are counted.
The author (uid) and creation time (created) of each entry are always stamped by the server, so clients cannot evade the rate limit by spoofing them.
Payload size limits
To stop oversized entries from bloating your database, the two JSON fields are capped by byte size. An entry that exceeds the limit is rejected, and the oversized payload is recorded in the module's log channel.
-
Maximum log size (
max_payload_bytes.log, default65536, i.e. 64KB): the maximum size in bytes of thelogfield. -
Maximum device info size (
max_payload_bytes.device_info, default8192, i.e. 8KB): the maximum size in bytes of thedevice_infofield.
Assumptions
This module assumes you want to let users log events but limit access to managing those logged events to admins.
For this reason, you can only POST log entities via REST and JSON:API; you cannot GET, PATCH, or DELETE them. If you object to this, open a feature request and I will make it configurable.
Sample TS Code
You can create log entries in the standard way with JSON:API.
/**
* Stringifies an Error object, since error.toString() doesn't.
*
* https://stackoverflow.com/a/30604852
*/
const stringifyError = (error: Error) =>
JSON.stringify(error, [
'message',
'arguments',
'type',
'name',
'cause',
]);
// Use a function in your frontend to get device info.
// For example, capacitor has a nice function:
// https://capacitorjs.com/docs/apis/device#getinfo
const deviceInfo = await Device.getInfo();
// Log the error with axios.
axiosInstance({
url: 'https://www.example.com/jsonapi/log_json--error',
data: {
data: {
type: 'error',
attributes: {
label: 'FrontendFatalError',
log: stringifyError(error),
device_info: deviceInfo,
}
}
},
headers: {
Accept: application/vnd.api+json,
'Content-Type': application/vnd.api+json,
'X-CSRF-Token': 'you_must_get_csrf_token_from_drupal',
},
method: 'POST',
})
Supporting This Module
All contributions are welcome. Please submit MRs, not patches, so that the CI tests are run. Please add tests if possible.
AI Development Disclosure
I use AI agents (Claude Code and Codex) to develop this module. It was originally written by hand, but most of the tests and some security features were implemented with AI assistance.