---
url: /reference/generators/audit-log.md
---
# Audit log

```sh
bin/rails g trek:audit_log
bin/rails db:migrate
```

Records who changed what in the admin, one row per request, and lists the rows to admins. The generator installs:

* the `AuditLog` model, including `Trek::AuditLog`, and its `audit_logs` table
* `config/initializers/audit_log.rb`, which includes [`Trek::Auditable`](#what-is-recorded) in the admin controllers, the sign-in and the password reset
* the admin list at `/admin/audit_logs` (controller, views, policy and route), open to the `admin` role only
* its specs, `spec/models/audit_log_spec.rb` and `spec/requests/admin/audit_logs_spec.rb`

To reach the list from the admin menu, add `AuditLog` to `MODELS_IN_MENU` in `app/models/admin/dashboard.rb`.

## What is recorded

Each mutation (any request but GET and HEAD) writes a row:

| Column | Value |
| --- | --- |
| `actor` | `Current.user`, nil when nobody is signed in (a failed sign-in, a request redirected to the sign-in page) |
| `auditable` | the record the action worked on: `@object` once saved, or once destroyed. For a sign-in, the user signed in to or tried, so an account's failed sign-ins can be listed |
| `action` | `controller#action`, e.g. `admin/pages#update` |
| `http_method`, `path`, `http_status` | the request and its response status: a failed sign-in answers 422, a throttled one 429, so both count as failures |
| `params` | the request's parameter names; their values are replaced with `[FILTERED]`, except for `id` and `role` (see `audit_param_values` below), so the log doesn't copy what the forms send. `config.filter_parameters` (passwords, tokens, emails…) still applies |
| `ip_address`, `user_agent`, `request_id` | where the request came from |
| `error_class` | the class of the error the request raised, if any (`http_status` is then nil) |

Rows are append-only: a saved row raises `ActiveRecord::ReadOnlyRecord` when updated. Writing one never breaks the request: a failure is reported through `Rails.error`.

The admin list filters on `action`, `actor_type`/`actor_id` and `auditable_type`/`auditable_id` (e.g. `/admin/audit_logs?filters[auditable_type]=Page&filters[auditable_id]=12`), and lists failures only (an error status or a raised error) with `?scopes[failures]=1`. `Trek::AuditLogsHelper#audit_log_path_for(record)` links to a record's rows, for a button on its admin page.

## Customizing

Include `Trek::Auditable` in any other controller to audit it too, e.g. the front office's `ApplicationController`. Override its hooks there:

```ruby
class Api::BaseController < ActionController::API
  include Trek::Auditable

  # Record reads as well as mutations
  self.audit_reads = true

  # Actions that only read although they're not a GET, skipped as reads are (default: none),
  # e.g. the chips hotwire_combobox requests with a POST
  self.audit_read_actions = %i[chips]

  # The parameters whose values are recorded, the others by name only (default: id and role)
  self.audit_param_values = %w[id role status]

  private

  # Who made the request (default: Current.user)
  def audit_actor = Current.api_client

  # The record the action worked on (default: @object, once saved or destroyed)
  def audit_target = @order

  # The parameters to record (default: their names, with the values of audit_param_values)
  def audit_params = super.except("signature")

  # Values for the app's own columns, added by a migration of its own
  def audit_attributes = { shop_id: Current.shop&.id }
end
```

A read showing sensitive data can be recorded explicitly, whatever the HTTP method, with `audit!(name, target)`; the row's action is then `controller#name`:

```ruby
def show
  super
  audit!(:show_medical_record, @object)
end
```

## Switching it off

`FEATURE_AUDIT_LOG=false` stops recording, e.g. if writing the rows slows down a busy app. It is read on each request.

## Purging old rows

`Trek::AuditLog::PurgeJob` deletes the rows older than 6 months, the low end of the [CNIL's recommendation](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000044272396) for logs (6 months to 1 year, up to 3 years for a particularly sensitive processing). The generator schedules it every night in GoodJob's cron, in the `config/initializers/good_job.rb` the installer writes. With another Active Job backend, it says so, and the job is to schedule daily with that backend. An app that installed the audit log before the generator did this adds the entry to its `config.good_job.cron` by hand:

```ruby
purge_audit_logs: {
  cron: "0 4 * * *",
  class: "Trek::AuditLog::PurgeJob",
  description: "Delete the audit log rows past the retention window"
},
```

To keep the rows longer or shorter, set the retention in an initializer: `Trek::AuditLog::PurgeJob.retention = 1.year`.

## Personal data

The rows hold personal data: the author, the IP address, the user agent, and whatever the request's parameters contain. Recording them to secure the app is allowed under the GDPR (its article 32, and recital 49 for people who aren't signed in), on the conditions the [CNIL's recommendation on logs](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000044272396) sets out:

* **Inform the people recorded**: the admin users (e.g. in their onboarding or the admin's legal notice), and the site's visitors if the front office is audited (its privacy policy): what is recorded, why, and for how long. List the processing in the app's record of processing activities.
* **Use the rows for security only**: investigating an incident or a misuse, not, for instance, evaluating the people recorded.
* **Keep them for a limited time**: 6 months to 1 year (see [Purging old rows](#purging-old-rows)).
* **Limit who reads them**: the list is open to the `admin` role only; keep that role to the few people who need it.
* **Record no more than needed**: the log keeps the parameters' names, not their values; add a parameter to `audit_param_values` only when its value matters to security (a role, a status), and never one carrying sensitive data such as health data.

## Moving from an app's own audit log

The `audit_logs` table uses the column names of the audit logs some apps wrote themselves (`actor`, `auditable`, `action`, `http_method`, `path`, `http_status`, `params`, `ip_address`, `user_agent`, `request_id`, `error_class`, `created_at`), so existing rows carry over. In such an app, skip the migration and replace the app's own files:

1. Run `bin/rails g trek:audit_log`, delete the generated `db/migrate/*_trek_audit_logs.rb`, and let the generator overwrite the model, controller, views and policy.
2. Delete the app's `Auditable` concern, its initializer and its purge job, and update the job's cron entry to `Trek::AuditLog::PurgeJob`.
3. Move app-specific columns (e.g. a request duration, or a denormalized foreign key) to `audit_attributes`, and their scopes and associations to the `AuditLog` model.
