An audit log is a chronological record of security-relevant events: who did what, to which resource, when, from where, and whether it worked. NIST’s glossary (opens in a new tab) puts it more briefly: “a record providing documentary evidence of specific events.” In a SaaS app, it’s how you, and often your customers, answer “who changed this?” months later. The audit log examples below show what that looks like in practice.
This guide covers the events worth recording, the fields every entry needs, sample JSON entries, and how to keep the log trustworthy.
What is an audit log?
An audit log is a structured, append-only record of consequential actions, kept for accountability and investigation. It’s a specific kind of log. NIST SP 800-92 (opens in a new tab) describes logs in general as records of events in an organization’s systems, used for troubleshooting, performance, recording user actions and investigating malicious activity.
The difference is purpose. An audit log is written to be evidence, so it’s designed differently from the logs engineers read while debugging.
| Audit log | Application log | Product analytics | |
|---|---|---|---|
| Question it answers | Who did what, and did it work? | What went wrong in the code? | How do people use the product? |
| Main readers | Security, compliance, customer admins | Engineers | Product teams |
| Schema | Stable, versioned | Free-form or semi-structured | Event-specific |
| Retention | Defined period per category | Short | Varies |
| Can it be edited? | No | Usually rotated or overwritten | Often sampled or transformed |
What type of events does an audit log generally record?
An audit log generally records authentication, permission changes, sensitive data access and exports, administrative and security settings changes, billing changes and staff access. These are the events someone will ask about after an incident or a dispute.
Two primary sources line up on this. NIST SP 800-53 control AU-2 (opens in a new tab) lists event types such as password changes, failed logons, security attribute changes and administrative privilege usage. OWASP’s Logging Cheat Sheet (opens in a new tab) says to always log authentication successes and failures, authorization failures, user administration actions, access to sensitive data, data import and export, and use of break-glass accounts.
For a multi-tenant SaaS app, that translates into these categories:
| Category | Example event names |
|---|---|
| Authentication | user.login.succeeded, user.login.failed, mfa.disabled, password.reset.completed |
| Members and permissions | member.invited, member.role.changed, member.removed, sso.connection.updated |
| Keys and integrations | api_key.created, api_key.revoked, webhook.endpoint.created, integration.connected |
| Data access and export | report.exported, records.bulk_downloaded, document.viewed (for sensitive documents only) |
| Settings and security | settings.retention.changed, ip_allowlist.updated, audit_log.exported |
| Billing | subscription.plan.changed, payment_method.updated, invoice.refunded |
| Staff and support access | support_access.approved, support_session.started, support_session.ended |
| Data lifecycle | workspace.deleted, user.data.erased |
Don’t log every page view or click. OWASP warns that a blind checklist approach can create “alarm fog” that hides real problems. Pick events that are consequential, and review the list as the product changes. AU-2 asks for that review too.
What fields should every audit log entry have?
Every entry needs to say what happened, when, where, who did it, to what, and with what outcome. NIST SP 800-53 control AU-3 (opens in a new tab) lists the event type, time, location, source, outcome and the identity of any individuals or objects involved. OWASP frames the same idea (opens in a new tab) as “when, where, who and what”, plus an interaction identifier that links related events.
| Field | Why it’s there | Example |
|---|---|---|
id | Deduplicate and reference one event | evt_7Hq2 |
action | What happened, from a fixed list | member.role.changed |
occurred_at | When, in UTC ISO 8601 | YYYY-MM-DDThh:mm:ssZ |
actor | Who did it: a user, API key, service or staff member | { "type": "user", "id": "usr_41" } |
org_id | Which tenant it belongs to | org_acme |
target | What it was done to | { "type": "member", "id": "usr_88" } |
outcome | success, failure or denied, plus a reason code | denied / insufficient_role |
source | Where from: IP, user agent, client | { "ip": "203.0.113.7" } |
request_id | Links events from one request or job | req_5c1 |
changes | Before and after for configuration changes | { "role": ["member", "admin"] } |
schema_version | Lets the format evolve | 2 |
Use stable IDs rather than names, since names change. Emit the event from the server, after the change commits, not from a frontend success message.
Audit log examples: sample JSON entries
These audit log examples use the fields above. They’re an example schema for your own application, not a standard, and all values are fictional. Timestamps are shown as a format placeholder.
A failed sign-in. The actor isn’t authenticated yet, so record that rather than trusting the submitted identity.
{
"id": "evt_01",
"action": "user.login.failed",
"occurred_at": "YYYY-MM-DDThh:mm:ssZ",
"actor": { "type": "anonymous" },
"org_id": "org_acme",
"target": { "type": "user", "id": "usr_41" },
"outcome": "failure",
"reason": "invalid_credentials",
"source": { "ip": "203.0.113.7", "user_agent": "Mozilla/5.0" },
"request_id": "req_9a1"
}
A role change. Configuration changes carry before and after values.
{
"id": "evt_02",
"action": "member.role.changed",
"occurred_at": "YYYY-MM-DDThh:mm:ssZ",
"actor": { "type": "user", "id": "usr_12" },
"org_id": "org_acme",
"target": { "type": "member", "id": "usr_88" },
"changes": { "role": ["member", "admin"] },
"outcome": "success",
"request_id": "req_9a2"
}
A data export. Record what was exported and how much, never the exported data.
{
"id": "evt_03",
"action": "report.exported",
"occurred_at": "YYYY-MM-DDThh:mm:ssZ",
"actor": { "type": "user", "id": "usr_12" },
"org_id": "org_acme",
"target": { "type": "report", "id": "rpt_invoices" },
"details": { "format": "csv", "row_count": 1840 },
"outcome": "success",
"request_id": "req_9a3"
}
An API key created by an integration. Record the key’s ID or prefix, never the key.
{
"id": "evt_04",
"action": "api_key.created",
"occurred_at": "YYYY-MM-DDThh:mm:ssZ",
"actor": { "type": "api_key", "id": "key_admin_3f" },
"org_id": "org_acme",
"target": { "type": "api_key", "id": "key_9d2", "prefix": "sk_9d2" },
"outcome": "success",
"request_id": "req_9a4"
}
Should customers see your audit log?
For B2B SaaS, usually yes, for events in their own organization. Customer admins use a customer-facing audit log to answer their own questions without opening a ticket. GitHub’s organization audit log (opens in a new tab), for example, shows organization owners the events that affected their organization in the last 180 days. Slack’s Audit Logs API (opens in a new tab), available on its Enterprise plans, describes every event as an actor, an action, an entity and a context.
You’ll still need an internal log. Keep the two separate:
| Customer-facing audit log | Internal audit log | |
|---|---|---|
| Scope | One customer organization | All tenants and the platform |
| Includes | Members’ actions, API keys, settings, exports | Plus security signals, system events and staff activity |
| Detail | Readable action names, limited internal reasons | Full reason codes and investigation context |
| Access | Customer owners and admins | Security and a small set of engineers |
| Delivery | Dashboard view, export or API | Your log pipeline or security tooling |
Decide deliberately whether staff access to a customer’s data appears in that customer’s log. Microsoft, for instance, logs every Lockbox access request (opens in a new tab) and action, and makes them accessible through the Office 365 Management Activity API.
How should you log support and impersonation actions?
When staff act inside a customer’s account, record two identities: the staff actor who performed the action and the customer subject whose account was used. Add a reference to the support session so you can link each action to the approval behind it. The impersonation audit log guide covers the schema and how to reconcile access records with application events.
If you use Devora, it records the access side: requests, decisions, reasons, scope, timing, self-approval, revocation and termination events. It doesn’t see the business actions inside your app, so those audit events are yours to emit.
How long should you keep audit logs, and how do you protect them?
Keep audit logs for a defined period per category, then delete them on schedule. NIST’s AU-11 asks organizations to retain audit records for a period they set, to support after-the-fact investigations and meet regulatory and organizational requirements. OWASP adds the other half: logs mustn’t be destroyed before the retention period ends, or kept beyond it.
There’s no universal number. Set retention from your legal and contractual obligations, and publish the customer-facing period. As one reference point, Devora keeps request and session metadata (opens in a new tab) for 180 days after a session ends and its pseudonymous security audit for 365 days.
Protect integrity and confidentiality:
- Restrict writes. NIST’s AU-9 asks you to protect audit information from unauthorized access, modification and deletion, and to alert when it happens. Give your application role insert-only access to the audit table.
- Detect tampering. OWASP recommends tamper detection, copying logs to read-only storage quickly, and recording all access to the logs. A hash chain or write-once storage in a separate account both work.
- Keep secrets and personal data out. OWASP lists session IDs, access tokens, passwords, payment data and sensitive personal data as things to remove, mask, hash or encrypt rather than log directly.
- Don’t lose events. Write audit events in the same transaction as the change, or through a durable outbox, so a failure can’t silently drop them.
How do you check that your audit log works?
Run each scenario in staging and inspect the stored events. These are pass conditions to check, not results we’ve run.
| Scenario | Pass condition |
|---|---|
| Sign in with a wrong password | One failure event, with no password or token in it |
| Change a member’s role | One event with the actor, target, and before and after roles |
| A request is rejected for lack of permission | A denied event, and no success event |
| Export a report | Event records format and size, not the data |
| The audit store is unavailable during a change | The change fails, or the event is retried durably; it is never silently lost |
| The application tries to update an existing audit row | The database refuses |
| A customer admin opens their audit log | Only their organization’s events appear |
| An event passes its retention period | It’s deleted on schedule |
Start with the categories table and the field list, emit events from the server, and test the failure cases before the happy path. If support access to customer accounts is part of your audit story, create a Devora account and try one staging session, then line its access record up with your own audit events.
Sources
- NIST CSRC Glossary: audit log (opens in a new tab)
- NIST SP 800-92: Guide to Computer Security Log Management (opens in a new tab)
- NIST SP 800-53 Rev. 5: Security and Privacy Controls for Information Systems and Organizations (opens in a new tab)
- OWASP Logging Cheat Sheet (opens in a new tab)
- GitHub Docs: Reviewing the audit log for your organization (opens in a new tab)
- Slack: Using the Audit Logs API (opens in a new tab)
- Microsoft Learn: Microsoft 365 service engineer access control (opens in a new tab)
- Devora: Security
- Devora: Recording privacy and activity logs (opens in a new tab)