Write audit_log_filter definitions¶
Audit log filters are JSON documents you pass to audit_log_filter_set_filter().
Every filter nests rules under a root filter object. For authoritative class, event, and field names that validation accepts, see Audit Log Filter definition fields.
| Benefit | Description |
|---|---|
| Smaller logs | Target only the events you need—less disk, simpler retention. |
| Lighter I/O | Fewer bytes per rotation window; less audit overhead on busy hosts. |
| Sharper security signal | Emphasize sensitive tables, account changes, and chosen DML/DDL instead of noise. |
| Faster investigations | Analysts skim fewer lines when irrelevant classes stay out of the file. |
| Compliance fit | Capture the evidence frameworks ask for without logging everything. |
| Lower resource burn | Trim CPU, memory, and disk spent on unwanted audit volume. |
Basic structure¶
A filter is a JSON document whose root is a single filter object. filter contains an optional global log flag, an optional id (used for dynamic filter swapping), and one or more class rules. Each class rule can carry its own log condition and an optional event list; each event block can carry log, abort, print, or a nested filter. Conditions inside log / abort / print are built from field, variable, function, and the and / or / not logical operators.
The tree below summarizes where each key legally nests:
filter # root object
├── log # optional boolean; global default
├── id # optional; referenced by activate / ref
└── class # one object or an array of class rules
├── name: <class> # "connection" | "general" | "table_access" | "message"
├── log # optional; boolean OR a condition
├── print # optional; field-replacement rule (redaction)
└── event # one object or an array of event rules
├── name: <subclass> # e.g. "connect", "status", "update", "insert", …
├── log # optional; boolean OR a condition
├── abort # optional; boolean OR a condition (blocks execution)
├── print # optional; field-replacement rule (redaction)
└── filter # optional; subfilter for dynamic swapping
condition := {
field : { name, value } # compare an event field
variable : { name, value } # compare a predefined server variable
function : { name, args? } # call a predefined function
and | or : [ condition, … ] # combine sub-conditions
not : condition # invert a sub-condition
}
Keep this shape in mind as you read the rest of the page: every subsequent example hangs something off one of these nodes.
A minimal skeleton:
{
"filter": {
"class": [
{
"name": "class_type",
"event": [
{ "name": "event_subclass" }
]
}
]
}
}
Replace class_type with a real class name (connection, general, table_access, message) and event_subclass with one of its subclasses. Add a log condition under the class or the event to narrow which matching events are actually written.
Practical example¶
The following filter logs only connection events whose user.str is admin or developer and whose host.str is 192.168.0.1:
{
"filter": {
"class": [
{
"name": "connection",
"log": {
"and": [
{ "field": { "name": "user.str", "value": ["admin", "developer"] } },
{ "field": { "name": "host.str", "value": "192.168.0.1" } }
]
}
}
]
}
}
Unpacking the shape:
"class"holds an array of class rules."name": "connection"selects theconnectionclass."log"under the class block is a condition; the class logs only the events that satisfy it.- The condition is an
andover twofieldcomparisons.user.strandhost.strare fields carried by everyconnectionevent (see Audit Log Filter definition fields).user.str’svalueis an array, so the comparison is true when the field matches any value in the array.
Rule-level conditions like this one narrow which events are logged — they do not determine which filter a session uses. Session-to-filter binding is separate, done with audit_log_filter_set_user() and stored in mysql.audit_log_user. A session only reaches this JSON after the server has chosen this filter for it.
Log all events¶
Toggle global logging with the top-level log flag:
{
"filter": { "log": true }
}
"log": true logs everything; "log": false logs nothing.
An empty filter object also logs everything:
{
"filter": { }
}
That is equivalent to "log": true.
Behavior summary:
| Option | Details |
|---|---|
Explicit log |
Honors true / false at that level. |
No log, no class/event rules |
Defaults to on (log all). |
| Class or event rules present | Each block can carry its own log override. |
Log specific event classes¶
To limit logging to one class, set class.name, for example connection:
{
"filter": {
"class": { "name": "connection" }
}
}
{ "name": "connection" } under filter.class audits connection events (connect, disconnect, change user—subject to event_mode and optional event narrowing).
The next example turns default logging off, then turns connection logging back on inside the class block:
{
"filter": {
"log": false,
"class": {
"log": true,
"name": "connection"
}
}
}
Log multiple classes or events¶
Log several classes either as separate objects in an array or as one array of names—equivalent when you do not need per-class options yet.
A list¶
Use a list when you will add per-class keys (user, event, …) later.
{
"filter": {
"class": [
{ "name": "connection" },
{ "name": "general" },
{ "name": "table_access" }
]
}
}
An array¶
Or combine class names in one array when rules stay uniform:
{
"filter": {
"class": [
{ "name": [ "connection", "general", "table_access" ] }
]
}
}
List of event and subclass options¶
Typical class and subclass pairs for authoring (subject to audit_log_filter.event_mode); validate exact spellings in Audit Log Filter definition fields.
| Class name | Event subclass | Details |
|---|---|---|
connection |
connect |
New sessions (success or failure) |
connection |
change_user |
CHANGE USER |
connection |
disconnect |
Session end |
general |
status |
Query / command completion (success vs failure) |
general |
command |
Command-level general events (FULL mode) |
table_access |
read |
Reads (SELECT, INSERT … SELECT, …) |
table_access |
delete |
Deletes / truncate-style operations |
table_access |
insert |
Inserts / REPLACE |
table_access |
update |
Updates |
message |
internal |
Internal audit API messages |
message |
user |
User-emitted messages (audit_api_message_emit_udf()) |
Mix and match subclasses under event to mirror your threat model.
Inclusive filters¶
Inclusive rules list what to log—pair classes with user, database, table, event, status, and similar fields until the stream matches your policy.
Basic structure¶
Spell out:
-
Which classes matter
-
Which accounts or objects to watch
-
Which subclasses or outcomes qualify
Typical uses: compliance evidence, privileged-user monitoring, schema-change tracking, incident response.
Tighter filters mean less noise and less I/O—always stage-test rules before production.
Inclusive filter example¶
Administrators often watch destructive table_access work—this skeleton logs update and delete only:
{
"filter": {
"class": [
{
"name": "table_access",
"event": [
{
"name": ["update", "delete"],
"log": {
"and": [
{ "field": { "name": "table_database.str", "value": "app_db" } },
{ "field": { "name": "table_name.str", "value": "sensitive_tbl" } }
]
}
}
]
}
]
}
}
The rule matches update and delete table_access events, and the nested log condition narrows logging to events whose table_database.str equals app_db and table_name.str equals sensitive_tbl. read and insert subclasses are not named, so they skip the rule entirely.
"class"— begins the class rule block."name": "table_access"— limits to table-access events (subclasses:read,insert,update,delete)."event"— selects theupdateanddeletesubclasses; other subclasses skip this rule."log"— a per-event condition.truelogs every match; afield/and/or/notstructure logs only matches that satisfy the condition."field"— compares an event field against a value.table_database.strandtable_name.strare fields carried by everytable_accessevent (see Audit Log Filter definition fields for the full list).
To widen or narrow the rule, change the field values, add or branches for multiple tables, or drop either field check to scope by database only or by table name only.
Log only UPDATE and DELETE on a specific table¶
To log update and delete against a single table — or a short list of tables inside one database — combine table_database.str with an or over table_name.str values:
{
"filter": {
"class": [
{
"name": "table_access",
"event": [
{
"name": ["update", "delete"],
"log": {
"and": [
{ "field": { "name": "table_database.str", "value": "app_db" } },
{
"or": [
{ "field": { "name": "table_name.str", "value": "sensitive_tbl" } },
{ "field": { "name": "table_name.str", "value": "audit_events" } }
]
}
]
}
}
]
}
]
}
}
To scope to one database only, omit the inner or and keep the table_database.str check. Note that table_access events do not carry a user field, so filtering by account must be done at assignment time with audit_log_filter_set_user().
Exclusive filters¶
Exclusive (or negated) rules drop noisy work. There are two idioms:
- Set
"log": trueat the filter level, then set"log": falseon a specific class or event to suppress just that slice. - Use a
"not"logical operator inside alogcondition to drop matches that satisfy an expression.
Suppress one class while logging everything else¶
{
"filter": {
"log": true,
"class": { "name": "general", "log": false }
}
}
This logs every event except general class events. log: true at the top enables logging globally, then the class-level log: false carves out one class.
Invert a field match with not¶
Wrap a condition in not inside a log item to drop the events that match it. Because inversion tests a field on the current event, use a field the class actually carries (for example, table_database.str on table_access; user.str on connection; general_user.str on general). The example below suppresses table_access events against the internal mysql schema:
{
"filter": {
"class": [
{
"name": "table_access",
"log": {
"not": {
"field": { "name": "table_database.str", "value": "mysql" }
}
}
}
]
}
}
This logs table_access events except those touching the mysql system schema. For a user-based suppression, apply the not against a field that exists on the class you’re filtering — user.str on connection, general_user.str on general — or scope the filter to specific accounts at assignment time with audit_log_filter_set_user().
Exclusive filter example¶
This filter logs everything except general events, and within connection events drops connect/disconnect subclasses (keeping only change_user):
{
"filter": {
"log": true,
"class": [
{
"name": "connection",
"event": [
{ "name": "connect", "log": false },
{ "name": "disconnect", "log": false }
]
},
{ "name": "general", "log": false }
]
}
}
- The top-level
"log": trueturns everything on by default. - The
connectionblock’seventlist silences the two noisy subclasses but leaveschange_useron. - The second class entry turns off the entire
generalclass.
Field-type rules (JSON numbers versus strings, connection_type symbolic constants such as "::tcp/ip", and which types are accepted for each field) are described under audit_log_filter_set_filter() and in Audit Log Filter definition fields.
Advanced filter constructs¶
The sections so far cover logging on or off at class/event granularity. The filter language also supports per-event conditions, execution blocking, references to server variables and functions, field-value replacement (redaction), and dynamic filter swapping.
Validate exact field names and types against Audit Log Filter definition fields before deploying any of these to production.
Test event field values¶
Inside an event block, a log item can carry a field comparison. The rule logs the event only when the field equals the given value.
{
"filter": {
"class": {
"name": "general",
"event": {
"name": "status",
"log": {
"field": { "name": "general_command.str", "value": "Query" }
}
}
}
}
}
The above logs general/status events only when general_command.str equals Query (dropping Execute, Quit, and Change user). String fields take string values; integer fields (such as status on connection, or general_error_code on general) take JSON numbers.
Fields available on each class are listed in Audit Log Filter definition fields. Examples:
-
connection:status,user.str,host.str,ip.str,database.str,connection_type -
general:general_error_code,general_user.str,general_command.str,general_query.str,general_sql_command.str,general_host.str,general_ip.str -
table_access:query.str,table_database.str,table_name.str
Combine conditions with logical operators¶
and, or, and not let you build compound conditions. They take an array (for and / or) or a single sub-condition (for not) and can be nested arbitrarily.
{
"filter": {
"class": {
"name": "general",
"event": {
"name": "status",
"log": {
"or": [
{
"and": [
{ "field": { "name": "general_command.str", "value": "Query" } },
{ "field": { "name": "general_command.length", "value": 5 } }
]
},
{
"and": [
{ "field": { "name": "general_command.str", "value": "Execute" } },
{ "field": { "name": "general_command.length", "value": 7 } }
]
}
]
}
}
}
}
}
This logs general/status events whose general_command is either Query (length 5) or Execute (length 7). Use not to invert any sub-expression — for example, { "not": { "field": { "name": "user.str", "value": "healthcheck" } } } matches everything except the healthcheck account (on a class that actually carries user.str).
Block execution with abort¶
An event block can carry an abort item that prevents matching statements from executing instead of (or in addition to) logging them. Because blocking is a policy-enforcement capability — not observability — it is covered on its own page: Block statements with an audit log filter.
Reference predefined variables¶
A log or abort condition can test a predefined variable with a variable item. The condition is true when the variable equals the given value.
{
"filter": {
"class": {
"name": "general",
"event": {
"name": "status",
"log": {
"variable": {
"name": "audit_log_connection_policy_value",
"value": "::none"
}
}
}
}
}
}
Predefined variables mirror the legacy-mode audit_log_*_policy system variables so operators can re-tune an active filter by changing a server variable rather than rewriting the JSON:
audit_log_connection_policy_value—0/"::none",1/"::errors",2/"::all".audit_log_policy_value—0/"::none",1/"::logins",2/"::all",3/"::queries".audit_log_statement_policy_value—0/"::none",1/"::errors",2/"::all".
Symbolic constants ("::none", "::all", …) are case-sensitive strings and are interchangeable with the numeric form.
Reference predefined functions¶
Use a function item to call a built-in in a log or abort condition. name holds the function name (no parentheses); args is an array of arguments, omitted when the function takes none.
{
"filter": {
"class": {
"name": "general",
"event": {
"name": "status",
"log": {
"function": {
"name": "find_in_include_list",
"args": [ { "string": [ { "field": "user.str" },
{ "string": "@" },
{ "field": "host.str" } ] } ]
}
}
}
}
}
}
The above logs general/status events only when the current account (built by concatenating user.str, @, and host.str) is present in audit_log_include_accounts.
Available functions:
audit_log_exclude_accounts_is_null()— true ifaudit_log_exclude_accountsisNULL. No arguments.audit_log_include_accounts_is_null()— true ifaudit_log_include_accountsisNULL. No arguments.find_in_exclude_list(account)— true ifaccountappears inaudit_log_exclude_accounts.find_in_include_list(account)— true ifaccountappears inaudit_log_include_accounts.query_digest([str])— with no argument, returns the current event’s statement digest (usable inreplace); with an argument, returns a Boolean comparing that argument to the current digest.string_find(text, substr)— true ifsubstris contained intext(case-sensitive).debug_sleep(millisec)— sleeps the given number of milliseconds. Debug builds only; used for performance measurement.
Replace event field values (redaction)¶
A print / replace item inside a class or event block rewrites statement text (general_query.str, query.str) as a query_digest before the event is logged, which keeps literal values out of the audit stream. This is a compliance/PII capability covered on its own page: Redact audit log fields.
Replace a filter dynamically¶
A filter can swap itself for a different rule set mid-session. Nest a filter block inside an event and give the outer filter an id; use activate to trigger the swap and ref to point back at the original.
{
"filter": {
"id": "main",
"class": {
"name": "table_access",
"event": {
"name": ["update", "delete"],
"log": false,
"filter": {
"class": {
"name": "general",
"event": { "name": "status",
"filter": { "ref": "main" } }
},
"activate": {
"or": [
{ "field": { "name": "table_name.str", "value": "temp_1" } },
{ "field": { "name": "table_name.str", "value": "temp_2" } }
]
}
}
}
}
}
}
How it behaves:
mainwaits fortable_accessupdateordeleteevents.log: falsemeans those events are not logged directly.- When one of those events touches
temp_1ortemp_2, the inner filter activates. - The inner filter waits for the next
general/statusevent (typically end of statement), logs it, then the nestedref: mainrestores the outer filter.
Net effect: you log one general/status entry per statement that touched temp_1 or temp_2, instead of many table_access rows. A single UPDATE temp_1, temp_3 SET ... emits one log entry rather than one per row touched.
activate is only valid inside a subfilter — using it on the top-level filter raises an error. id values are scoped to the filter definition only; they are unrelated to the audit_log_filter_id system variable.
Best practices¶
- Start wide, then narrow — begin with a noisy catch-all in staging, then peel away classes you do not need.
- Test combinations — overlapping rules and assignments surprise people; validate on a clone.
- Plan retention — pair filters with rotation, pruning, and disk budgets (Manage the Audit Log Filter files).
- Watch overhead — granular auditing costs CPU and I/O; ramp detail gradually and watch latency plus
audit_log_filter_*status counters.
Implement the filter¶
Typical workflow on Percona Server for MySQL 8.4:
Create a filter¶
Call audit_log_filter_set_filter(filter_name, json_definition):
SELECT audit_log_filter_set_filter('log_all', '{ "filter": { "log": true } }');
Assign filter to users¶
Bind accounts (or the default % row) with audit_log_filter_set_user('user@host', 'filter_name'):
SELECT audit_log_filter_set_user('%', 'log_all');
Example: Financial tracking filter¶
Create and assign a finance-focused filter. This filter logs DML on two tables in financial_db and all connection events; account scoping is applied at assignment time (table_access events do not carry a user field).
-- Create the filter
SELECT audit_log_filter_set_filter('financial_tracking', '{
"filter": {
"class": [
{
"name": "table_access",
"event": [
{
"name": ["insert", "update", "delete"],
"log": {
"and": [
{ "field": { "name": "table_database.str", "value": "financial_db" } },
{
"or": [
{ "field": { "name": "table_name.str", "value": "accounts" } },
{ "field": { "name": "table_name.str", "value": "transactions" } }
]
}
]
}
}
]
},
{
"name": "connection",
"event": [
{ "name": "connect" },
{ "name": "disconnect" }
]
}
]
}
}');
Field-value typing (JSON numbers for integer fields such as status, strings for *.str fields, and "::tcp/ip"-style symbolic constants for connection_type) is covered under audit_log_filter_set_filter().
Assign the filter to just the accounts that should be audited instead of to %:
-- Assign to specific accounts; other users keep their existing filter (or none).
SELECT audit_log_filter_set_user('admin@%', 'financial_tracking');
SELECT audit_log_filter_set_user('finance_team@%', 'financial_tracking');
The filter records:
-
insert/update/deleteonfinancial_db.accountsandfinancial_db.transactionsfor the two assigned accounts. -
connect/disconnectevents for the same accounts. -
Nothing for other users (they are not bound to this filter) and nothing outside the declared schema/tables unless you extend the JSON.
Inspect metadata directly:
-- Check created filters
SELECT * FROM mysql.audit_log_filter;
-- Check user assignments
SELECT * FROM mysql.audit_log_user;
Tail the audit file (default under the data directory) to confirm events stream as expected.