/*
AUTHOR : BRAHMANAND SHUKLA
DATE : 18-MAY-2018
PURPOSE : Generic trigger for maintaining the Audit Log
STEPS FOR IMPLEMENTATION:
STEP 1: Create the following tables on the same database or on seperate Audit Log Database.
I have used seperate database for Audit Log with name "Audit_Log".
CREATE TABLE [AUDIT_LOG_TABLE_PRIMARY_KEY]
(
[ID] NUMERIC NOT NULL IDENTITY(1, 1) CONSTRAINT PK_AUDIT_LOG_TABLE_PRIMARY_KEY_ID PRIMARY KEY CLUSTERED
, [TABLE_SCHEMA] SYSNAME NOT NULL
, [TABLE_NAME] SYSNAME NOT NULL
, [PRIMARY_KEY_COLUMN_NAME] SYSNAME NOT NULL
, CONSTRAINT UQ_AUDIT_LOG_TABLE_PRIMARY_KEY_TABLE_SCHEMA_TABLE_NAME UNIQUE ([TABLE_SCHEMA], [TABLE_NAME])
)
CREATE TABLE [AUDIT_LOG]
(
[ID] NUMERIC NOT NULL IDENTITY(1, 1)
, [TABLE_NAME] SYSNAME NOT NULL
, [PRIMARY_KEY_COLUMN_NAME] SYSNAME NOT NULL
, [PRIMARY_KEY_COLUMN_VALUE] NVARCHAR(MAX)
, [AUDIT_ACTION] CHAR(1) NOT NULL
, [HOST_NAME] NVARCHAR(100) NOT NULL
, [APP_NAME] NVARCHAR(100) NOT NULL
, [AUDIT_USERID] NVARCHAR(50) NOT NULL
, [AUDIT_DATETIME] DATETIME NOT NULL CONSTRAINT DF_AUDIT_LOG_DATETIME DEFAULT(GETDATE())
, [COLUMN_NAME] SYSNAME NOT NULL
, [COLUMN_OLD_VALUE] NVARCHAR(MAX)
, [COLUMN_NEW_VALUE] NVARCHAR(MAX)
)
STEP 2: Change the table name in the trigger name and on clause with the desired table name.
STEP 3: Change the database name and schema name of the "AUDIT_LOG_TABLE_PRIMARY_KEY" and "AUDIT_LOG" tables in the trigger.
STEP 4: Create the trigger on the desired table.
STEP 5: If your table doesn't has the Primary Key column then add an entry in "AUDIT_LOG_TABLE_PRIMARY_KEY" table.
STEP 6: You are all set. Check "AUDIT_LOG" tables for Audit Logs by performing INSERT, UPDATE and DELETE on the source table.
IMPORTANT NOTE:
1) The trigger will write the Old & New values in "AUDIT_LOG" table in case of UPDATE and only if there is change in value.
2) The trigger will write the columns and their values as New value in "AUDIT_LOG" table in case of INSERT.
3) The trigger will write the columns and their values as Old value in "AUDIT_LOG" table in case of DELETE.
*/
CREATE TRIGGER TR_Audit_Log_tablename
ON tablename
FOR INSERT, UPDATE, DELETE
NOT FOR REPLICATION
AS
BEGIN TRY
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
IF OBJECT_ID('tempdb..#COLUMNS') IS NOT NULL DROP TABLE #COLUMNS;
CREATE TABLE #COLUMNS
(
[COLUMN_NAME] SYSNAME
, [ORDINAL_POSITION] INT PRIMARY KEY CLUSTERED
)
IF OBJECT_ID('tempdb..#inserted') IS NOT NULL DROP TABLE #inserted;
IF OBJECT_ID('tempdb..#deleted') IS NOT NULL DROP TABLE #deleted;
DECLARE @TABLE_SCHEMA SYSNAME
DECLARE @TABLE_NAME SYSNAME
DECLARE @AUDIT_ACTION CHAR(1)
DECLARE @PRIMARY_KEY_COLUMN_NAME SYSNAME
DECLARE @PRIMARY_KEY_COLUMN_VALUE NVARCHAR(MAX)
DECLARE @PARAMETER_DEFINITION NVARCHAR(MAX)
DECLARE @SQL_QUERY NVARCHAR(MAX)
DECLARE @COLUMN_NAME SYSNAME
DECLARE @ORDINAL_POSITION INT
IF EXISTS(SELECT 1 FROM inserted)
AND EXISTS(SELECT 1 FROM deleted)
BEGIN
SET @AUDIT_ACTION = 'U';
END
ELSE
BEGIN
IF EXISTS (SELECT 1 FROM deleted)
BEGIN
SET @AUDIT_ACTION = 'D';
END
ELSE IF EXISTS(SELECT 1 FROM inserted)
BEGIN
SET @AUDIT_ACTION = 'I';
END
END
IF @AUDIT_ACTION IS NULL RETURN;
SELECT @TABLE_SCHEMA = OBJECT_SCHEMA_NAME(parent_id)
, @TABLE_NAME = OBJECT_NAME(parent_id)
FROM sys.triggers
WHERE object_id = @@PROCID;
-- Get the Primary Key column name
BEGIN
SELECT @PRIMARY_KEY_COLUMN_NAME = KEY_COLUMN_USAGE.COLUMN_NAME
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE
ON TABLE_CONSTRAINTS.CONSTRAINT_TYPE = 'PRIMARY KEY'
AND TABLE_CONSTRAINTS.CONSTRAINT_NAME = KEY_COLUMN_USAGE.CONSTRAINT_NAME
WHERE TABLE_CONSTRAINTS.TABLE_SCHEMA = @TABLE_SCHEMA
AND TABLE_CONSTRAINTS.TABLE_NAME = @TABLE_NAME
IF @PRIMARY_KEY_COLUMN_NAME IS NULL
BEGIN
SELECT @PRIMARY_KEY_COLUMN_NAME = [PRIMARY_KEY_COLUMN_NAME]
FROM Audit_Log.dbo.[AUDIT_LOG_TABLE_PRIMARY_KEY]
WHERE [TABLE_SCHEMA] = @TABLE_SCHEMA
AND [TABLE_NAME] = @TABLE_NAME
END
END
SELECT * INTO #inserted FROM inserted;
SELECT * INTO #deleted FROM deleted;
-- Get all the columns of the table
INSERT INTO #COLUMNS (COLUMN_NAME, ORDINAL_POSITION)
SELECT [COLUMN_NAME], [ORDINAL_POSITION]
FROM INFORMATION_SCHEMA.COLUMNS
WHERE [TABLE_SCHEMA] = @TABLE_SCHEMA
AND [TABLE_NAME] = @TABLE_NAME;
-- Iterate through the columns and maintain the Audit Log Detail (Old and New values of the column against the supplied Primary Key).
SET @ORDINAL_POSITION = 1;
WHILE EXISTS (SELECT 1 FROM #COLUMNS WHERE [ORDINAL_POSITION] = @ORDINAL_POSITION)
BEGIN
SELECT @COLUMN_NAME = [COLUMN_NAME]
FROM #COLUMNS
WHERE ORDINAL_POSITION = @ORDINAL_POSITION;
SET @SQL_QUERY = NULL;
IF (@AUDIT_ACTION = 'U')
BEGIN
SET @SQL_QUERY = 'SELECT ''' + @TABLE_NAME + ''' AS [TABLE_NAME]
, ''' + @PRIMARY_KEY_COLUMN_NAME + ''' AS [PRIMARY_KEY_COLUMN_NAME]
, INS.[' + @PRIMARY_KEY_COLUMN_NAME + '] AS [PRIMARY_KEY_COLUMN_VALUE]
, ''' + @AUDIT_ACTION + ''' AS [AUDIT_ACTION]
, HOST_NAME() AS HOST_NAME
, APP_NAME() AS APP_NAME
, SUSER_SNAME() AS AUDIT_USERID
, GETDATE() AS AUDIT_DATETIME
, ''' + @COLUMN_NAME + ''' AS [COLUMN_NAME]
, CAST(DEL.[' + @COLUMN_NAME + '] AS NVARCHAR(MAX)) AS [COLUMN_OLD_VALUE]
, CAST(INS.[' + @COLUMN_NAME + '] AS NVARCHAR(MAX)) AS [COLUMN_NEW_VALUE]
FROM #deleted DEL
INNER JOIN #inserted INS
ON INS.[' + @PRIMARY_KEY_COLUMN_NAME + '] = DEL.[' + @PRIMARY_KEY_COLUMN_NAME + ']
WHERE ISNULL(CAST(DEL.[' + @COLUMN_NAME + '] AS NVARCHAR(MAX)), '''') ISNULL(CAST(INS.[' + @COLUMN_NAME + '] AS NVARCHAR(MAX)), '''')';
END
ELSE IF (@AUDIT_ACTION = 'I')
BEGIN
SET @SQL_QUERY = 'SELECT ''' + @TABLE_NAME + ''' AS [TABLE_NAME]
, ''' + @PRIMARY_KEY_COLUMN_NAME + ''' AS [PRIMARY_KEY_COLUMN_NAME]
, INS.[' + @PRIMARY_KEY_COLUMN_NAME + '] AS [PRIMARY_KEY_COLUMN_VALUE]
, ''' + @AUDIT_ACTION + ''' AS [AUDIT_ACTION]
, HOST_NAME() AS HOST_NAME
, APP_NAME() AS APP_NAME
, SUSER_SNAME() AS AUDIT_USERID
, GETDATE() AS AUDIT_DATETIME
, ''' + @COLUMN_NAME + ''' AS [COLUMN_NAME]
, NULL AS [COLUMN_OLD_VALUE]
, CAST(INS.[' + @COLUMN_NAME + '] AS NVARCHAR(MAX)) AS [COLUMN_NEW_VALUE]
FROM #inserted INS';
END
ELSE IF (@AUDIT_ACTION = 'D')
BEGIN
IF (@COLUMN_NAME = @PRIMARY_KEY_COLUMN_NAME)
BEGIN
SET @SQL_QUERY = 'SELECT ''' + @TABLE_NAME + ''' AS [TABLE_NAME]
, ''' + @PRIMARY_KEY_COLUMN_NAME + ''' AS [PRIMARY_KEY_COLUMN_NAME]
, DEL.[' + @PRIMARY_KEY_COLUMN_NAME + '] AS [PRIMARY_KEY_COLUMN_VALUE]
, ''' + @AUDIT_ACTION + ''' AS [AUDIT_ACTION]
, HOST_NAME() AS HOST_NAME
, APP_NAME() AS APP_NAME
, SUSER_SNAME() AS AUDIT_USERID
, GETDATE() AS AUDIT_DATETIME
, ''NA'' AS [COLUMN_NAME]
, ''NA'' AS [COLUMN_OLD_VALUE]
, ''NA'' AS [COLUMN_NEW_VALUE]
FROM #deleted DEL';
END
END
IF ISNULL(@SQL_QUERY, '') ''
BEGIN
SET @SQL_QUERY = 'INSERT INTO Audit_Log.dbo.[AUDIT_LOG]
(
[TABLE_NAME]
, [PRIMARY_KEY_COLUMN_NAME]
, [PRIMARY_KEY_COLUMN_VALUE]
, [AUDIT_ACTION]
, [HOST_NAME]
, [APP_NAME]
, [AUDIT_USERID]
, [AUDIT_DATETIME]
, [COLUMN_NAME]
, [COLUMN_OLD_VALUE]
, [COLUMN_NEW_VALUE]
)' + ISNULL(@SQL_QUERY, '');
END
EXECUTE sp_executesql @SQL_QUERY
SET @ORDINAL_POSITION = @ORDINAL_POSITION + 1;
END
END TRY
BEGIN CATCH
DECLARE @ErrorNumber INT = ERROR_NUMBER();
DECLARE @ErrorLine INT = ERROR_LINE();
DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
DECLARE @ErrorState INT = ERROR_STATE();
DECLARE @ErrorProcedure VARCHAR(500) = ERROR_PROCEDURE();
SET @ErrorMessage = ISNULL(@ErrorMessage, '')
+ ' Procedure Name: ' + ISNULL(@ErrorProcedure, '')
+ ' Error Number: ' + CAST(ISNULL(@ErrorNumber, 0) AS VARCHAR(10))
+ ' Line Number: ' + CAST(ISNULL(@ErrorLine, 0) AS VARCHAR(10));
RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);
END CATCH
Disclaimer : Audit Log trigger has performance overhead. It’s meant for table which is not frequently updated, deleted or inserted. Please take precaution when creating the trigger on the table having too many columns.
One thought on “Generic trigger for audit log – SQL Server”