Generic trigger for audit log – SQL Server

/*
	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

Leave a comment