Scope: What This Covers, and What It Doesn't
Three categories, in SQL Server's own vocabulary:
- Login events — successful and failed authentication attempts.
- Database create/drop —
CREATE DATABASE,ALTER DATABASE,DROP DATABASE, server-scoped operations. - Metadata changes — schema-level DDL:
CREATE/ALTER/DROPon tables, views, procedures, permissions. Changes tosys.objects/sys.columns, not to the rows inside those tables.
Deliberately out of scope: row-level DML auditing (who changed this specific value in this specific row). That's a heavier, different problem, solved by temporal tables, Change Data Capture, or Audit's SELECT/INSERT/UPDATE/DELETE action groups, worth its own article rather than a bullet point in this one.
Method 1: AuditLevel and the Error Log (the Baseline Everyone Already Has)
SSMS's Server Properties → Security → "Login auditing" setting is really just a registry value, AuditLevel, read by SQL Server at startup: 0 = none, 1 = successful only, 2 = failed only (the default), 3 = both. Whatever it's set to, matching events land as plain text in the SQL Server error log, message 18453 (successful, non-trusted), 18454 (successful, trusted), 18456 (failed).
This costs nothing to turn on and is already partially on by default, which is exactly why it's the baseline, not the answer. It has no coverage for database create/drop or metadata changes at all, it's a text log subject to rotation, and it's not tamper-evident: anyone with access to the file (or a sysadmin session) can edit or delete it with nothing to say it happened.
Get-sqmErrorLog -FailedLogins -SuccessfulLogins queries exactly this, language-neutrally. See Every SQL Server Log for where this sits among everything else SQL Server writes.
Method 2: SQL Server Audit
The feature actually built for this, since SQL Server 2008, and in Standard Edition (not just Enterprise) since SQL Server 2016 SP1. Three pieces: a Server Audit (the destination, a binary .sqlaudit file, the Windows Application log, or the Windows Security log), a Server Audit Specification (server-scoped events, logins and CREATE/ALTER/DROP DATABASE among them), and optionally a Database Audit Specification per database (schema-level DDL, permission changes, scoped to that database).
-- 1. Destination
CREATE SERVER AUDIT AG_Compliance_Audit
TO FILE (FILEPATH = 'D:\Audit\', MAXSIZE = 500 MB, MAX_ROLLOVER_FILES = 20)
WITH (ON_FAILURE = FAIL_OPERATION, QUEUE_DELAY = 1000);
ALTER SERVER AUDIT AG_Compliance_Audit WITH (STATE = ON);
-- 2. Server-scoped events: logins, database create/drop, and who touches the audit itself
CREATE SERVER AUDIT SPECIFICATION AG_Server_Audit_Spec
FOR SERVER AUDIT AG_Compliance_Audit
ADD (FAILED_LOGIN_GROUP),
ADD (SUCCESSFUL_LOGIN_GROUP),
ADD (DATABASE_CHANGE_GROUP),
ADD (AUDIT_CHANGE_GROUP)
WITH (STATE = ON);
-- 3. Database-scoped: schema/metadata changes, per database
CREATE DATABASE AUDIT SPECIFICATION AG_Db_Audit_Spec
FOR SERVER AUDIT AG_Compliance_Audit
ADD (SCHEMA_OBJECT_CHANGE_GROUP)
WITH (STATE = ON);
Query the result with sys.fn_get_audit_file, or read it through SSMS's Log File Viewer:
SELECT event_time, action_id, succeeded, server_principal_name,
database_name, object_name, statement
FROM sys.fn_get_audit_file('D:\Audit\AG_Compliance_Audit*.sqlaudit', DEFAULT, DEFAULT)
ORDER BY event_time DESC;
AUDIT_CHANGE_GROUP above is not decoration: it's what answers the auditor's next question, who's watching the watcher, by recording changes to the audit configuration itself.
ON_FAILURE has three values: CONTINUE (keeps running, silently stops auditing, defeats the point), FAIL_OPERATION (blocks the audited action until the destination works again), and SHUTDOWN (stops the SQL Server service rather than let anything go unrecorded). SHUTDOWN is the strictest compliance posture and also means a full share, a broken permission, or a network blip on the audit destination takes the whole instance down with it. Choose it deliberately, with monitoring on the destination's free space and reachability, not as the "most secure-sounding" default.
Writing to the Windows Security log instead of a file needs two things most people forget on the first attempt: Object Access auditing enabled via local/group security policy, and the Generate security audits user right granted to the SQL Server service account. Skip either and the audit silently produces nothing there, with no error on the SQL Server side to say so.
Method 3: DDL Triggers and Logon Triggers
The pre-2008 approach, still fully current: a trigger that fires on the DDL event itself, inline with the transaction, with the DDL captured as XML via EVENTDATA(). Database-scoped DDL uses DDL_DATABASE_LEVEL_EVENTS; CREATE/ALTER/DROP DATABASE, being server-scoped, needs ON ALL SERVER FOR DDL_SERVER_LEVEL_EVENTS instead. Logins get their own separate mechanism, a logon trigger:
CREATE TRIGGER trg_AuditLogon
ON ALL SERVER FOR LOGON
AS
BEGIN
INSERT INTO master.dbo.LoginAuditLog (LoginName, LoginTime, ClientHost)
SELECT ORIGINAL_LOGIN(), GETDATE(), HOST_NAME();
END;
The one thing neither SQL Server Audit nor Extended Events can do: a logon trigger can ROLLBACK and actually reject the connection, and a DDL trigger can roll back the DDL statement it just watched happen. That makes triggers the only method on this list that's preventive rather than purely observational.
sqlcmd -A), which bypasses logon triggers specifically for this scenario. Test a logon trigger's failure path, not just its success path, before it goes anywhere near production.
Triggers also aren't independent oversight by default: anyone with enough permission to alter the trigger or the logging table can quietly remove their own trail, unless those objects are locked down with permissions separate from the DBA's own. SQL Server Audit's configuration objects have the same theoretical exposure, but Audit at least has AUDIT_CHANGE_GROUP to catch it happening; a hand-rolled trigger and log table have whatever protection you built yourself, and no more.
Method 4: Targeted Extended Events
Instead of one of Audit's broad action groups, a custom Extended Events session can subscribe to exactly the events wanted: sqlserver.login for successful connections, error_reported filtered to error number 18456 for failures, database_created/database_dropped for database-level DDL, and object_created/object_altered/object_deleted for schema-level metadata changes (filtered to ddl_phase = 1, since each fires twice per statement: once on start, once on commit or rollback).
This is the lowest-overhead structured option and works on every edition back to 2008, but it comes with none of Audit's packaging: no fn_get_audit_file equivalent (read the event file target's XML yourself), no separate audit-configuration permission model, and a session that silently stops existing after a restart unless STARTUP_STATE = ON was set when it was created, a common, quiet gap. One asymmetry worth knowing before relying on it: database_created/database_dropped cannot be scoped to a specific database name by a session-level predicate, at the moment either fires, neither the collected connection context nor the event's own field resolves to the database being created or dropped, so these two are always instance-wide; object_created/object_altered/object_deleted scope to specific databases correctly.
Register-sqmAuditSession sets up exactly this: -FailedLogins, -SuccessfulLogins, -DatabaseCreated, -DatabaseDropped, -MetadataChanges (optionally scoped with -TargetDatabase), each independently selectable, or -All. Reading it back needs no new function, Invoke-sqmExtendedEvents -Read -SessionName sqm_AuditSession already surfaces the right fields.
Comparison
| Aspect | AuditLevel | SQL Server Audit | DDL/Logon triggers | Extended Events |
|---|---|---|---|---|
| Edition requirement | All | Standard 2016 SP1+, or Enterprise | All, incl. Express | All, incl. Express |
| Captures failed logins | Yes (text only) | Yes, structured | Only if the trigger itself checks and rejects | Yes, via error_reported |
| Captures DB create/drop | No | Yes (DATABASE_CHANGE_GROUP) | Yes (server DDL trigger) | Yes, but always instance-wide (can't scope by DB name) |
| Captures schema/DDL changes | No | Yes (SCHEMA_OBJECT_CHANGE_GROUP) | Yes (database DDL trigger) | Yes (object_created/altered/deleted) |
| Can block the action, not just log it | No | No (FAIL_OPERATION blocks on write failure, not on content) | Yes | No |
| Built-in retention/query tooling | Log rotation only | Yes (fn_get_audit_file, MAX_ROLLOVER_FILES) | None, you build it | Event file rotation, no reader convenience |
| Misconfiguration risk | Low (just a logging gap) | SHUTDOWN can take the instance offline | A broken logon trigger can lock out all logins | Low, mainly a silent logging gap |
sqmSQLTool: What's Covered, What Isn't
Worth being precise here, because the word "audit" already means two different things in this module:
Get-sqmErrorLog (Method 1) or SQL Server Audit's login groups (Method 2). Same word, genuinely different question.
Get-sqmErrorLog -FailedLogins -SuccessfulLogins for Method 1. Register-sqmAuditSession for Method 4, set up and read back the exact categories this article covers without hand-writing the Extended Events DDL. Nothing yet wraps SQL Server Audit configuration/reading or DDL/logon trigger deployment directly, those are still run as plain T-SQL, same as the examples above. Full reference: sqmSQLTool commands.
Practical Guidance
- Compliance-grade requirement, with retention and evidence an auditor will actually ask for: SQL Server Audit. Decide the
ON_FAILUREtradeoff deliberately, and enableAUDIT_CHANGE_GROUPso the audit configuration itself is covered. - Need to actively prevent an action, not just record it, a decommissioned login that must never connect, a table that must never be dropped outside a change window: a logon or DDL trigger, alongside Audit, not instead of it. Test its failure path before production.
- Stuck on an edition or version without SQL Server Audit, or want the lowest possible overhead for a narrow set of events: targeted Extended Events, with
STARTUP_STATE = ONset from the start. - Never treat AuditLevel plus the error log as sufficient evidence on its own. It's a reasonable first line of visibility and exactly what most instances already have; it was never designed to be tamper-evident or complete.