Service broker + trigger based data auditing

Service broker + trigger based data auditing

I was assigned task to build our own Data Audit mechanism in SQL Server 2005 last year. There were number of articles and scripts on the internet about asynchronous auditing with service broker. I decided to get the best out of those articles and scripts and create my own trigger + service broker based solution.
Each table assigned for auditing on production database has AFTER INSERT, UPDATE, DELETE trigger. This trigger converts the record(s) of inserted and deleted tables into XML and a message dialog is initiated from the production database. This message is received by the audit database and parsed into specific tables. There will not (or hardly) be any performance issue because of asynchronous messaging between production and audit database.
The example XML is listed below.
<AuditMsg>
  <SourceDb>ProductionDatabase</SourceDb>
  <SourceTable>UserTable</SourceTable>
  <UserId>UserID</UserId>
  <AppUserId />
  <DMLType>U</DMLType>
  <ChangedData>
    <t ID="8521" Name="Atif" Class="SQL Server" />
  </ChangedData>
  <NewRec>
    <t ID="8521" Name="Atif Sheikh" Class="SQL Server" />
  </NewRec>
</AuditMsg>
As you can see from the above XML, it has an element ChangedData which have another element t. t element have attributes depending upon the structure of the table (whose record is changed and the trigger sent the XML message via service broker) of the production database. Same is the case with the attribute NewRec. In the above XML, the UserTable generated this XML. You can see from the XML that the Name is changed from ‘Atif’ to ‘Atif Sheikh’. The XML also holds the information of the production database name and SQL Server UserID, who changed the data of the UserTable. This XML is parsed in audit database. The information in the attributes of ChangedData and NewRec elements is compared and placed in another table tblAudDetail for efficient queries and reports. The parsing of XML is discussed in detail later.
 
Outline of the solution
 The outline of the solution is;

1.     Create New Audit Database

2.     Activate Service Broker on New Audit Database and your production database

3.     Create stored procedure to apply triggers on tables for auditing

 
I will discuss each point in detail.

1.     Create New Audit Database

This database will hold new and old values of columns changed on the production database. Create a new database as AuditDatabase. It will have following Tables;

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[tblAud](
      [tID] [int] IDENTITY(1,1) NOT NULL,
      [xmlQuery] [xml] NULL,
      [Upd_Det] [bit],
      [tDate] [datetime] NULL CONSTRAINT [DF_tblAud_tDate]  DEFAULT (getdate()),
CONSTRAINT [PK_tblAud] PRIMARY KEY CLUSTERED
(
      [tID] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
 
GO

SET ANSI_PADDING OFF

 

Column xmlQuery of table tblAud will hold XML sent by the trigger of the production database table. Column upd_Det bit column will tell us if XML is parsed and information inserted in tblAudDetail. It will be 1 in case of XML is parsed and information is inserted in tblAudDetail. Column tDate will tell us the date when record was arrived in the tblAud. Considering the above XML example, tblAud will have data as,
  
Here is the structure of tblAudDetail;
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[tblAudDetail](
      [AudDetID] [int] IDENTITY(1,1) NOT NULL,
      [tid] [int] NULL,
      [dbName] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
      [TABLENAME] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
      [FieldName] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
      [Priorval] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
      [CurrVal] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 CONSTRAINT [PK_tblAudDetail] PRIMARY KEY CLUSTERED
(
      [AudDetID] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
 
Table tblAudDetail will hold the information of the attributes of elements ChangedData and NewRec from XML in tblAud. You can see from the schema of tblAudDetail that it has columns dbName. It means we can use one audit database for multiple production databases. Considering the above mentioned XML, the tblAudDetail will have following records;
As you can see from the above dataset, only record number 2 have different PriorValue and CurrValue. Secondly, we also need the ID of the UserTable to recognize the record which in row number 1. We don’t need the record number 3. So, according to the code (mentioned ahead), the recordset will be;
 
One record of ID to recognize the record and the second record of the changed information of Column ‘Name’ with prior value of ‘Atif’ and new value of ‘Atif Sheikh’.
 
Here is the structure of tblErrorlog,
 
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[tblErrorLog](
      [ErrorNumber] [int] NOT NULL,
      [ErrorSeverity] [int] NULL,
      [ErrorState] [int] NULL,
      [ErrorLine] [int] NULL,
      [ErrorProcedure] [nvarchar](126) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
      [ErrorMessage] [nvarchar](4000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
      [ErrorTime] [datetime] NULL CONSTRAINT [DF__tblErrorL__Error__4865BE2A]  DEFAULT (getdate()),
      [DbUserName] [sysname] COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
      [sysUserName] [sysname] COLLATE SQL_Latin1_General_CP1_CI_AS NULL
) ON [PRIMARY]
 
Table tblErrorlog will hold details of errors in messages so that XML can be debugged.
 
The audit database will also have three stored procedures.
[dbo].[uspInsertAuditRec]
[dbo].[uspUpdateDetail]
[dbo].[uspUpdateAuditDetailFromAuditTableID]
 
Script for uspInsertAuditRec;
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[uspInsertAuditRec]
AS
BEGIN
      SET NOCOUNT ON;  
 
      Begin Try
 
            Declare @ConversationHandle as uniqueidentifier
            Declare @MessageBody as varbinary(max)
            Declare @MessageXML as XML
            Declare @MessageType as sysname
 
            Declare @pBQuery nvarchar(max)     
 
            Begin TRANSACTION
            Print 'Started Receiving';
 
            WAITFOR(
            RECEIVE top (1)
               @MessageType = message_type_name,
               @ConversationHandle = conversation_handle,
               @MessageBody = message_body
            FROM BLOB_AdtQueue_Remote1
            ), TIMEOUT 1000;
           
            Select @pBQuery = convert(nvarchar(max),@MessageBody)
            Select @MessageXML = cast(@pBQuery as XML)
            Declare @sID as INT
            Declare @DBName as nvarchar(100)
            Declare @TableName as nvarchar(100)
           
            IF @MessageType = 'BLOB'
            BEGIN
                  select  @DBName = x.header.value('(//SourceDb)[1]', 'nvarchar(50)') ,
                              @TableName = x.header.value('(//SourceTable)[1]', 'nvarchar(50)')
                             
                  FROM @MessageXML.nodes('//AuditMsg') AS x(header)
                 
                  Insert into tblAud (xmlQuery,upd_det) values (cast(@pBQuery as XML),0)
                  Select @sID = Scope_identity()
 
            END
            ELSE
            BEGIN
                  INSERT INTO tblAud (xmlQuery,upd_det) values ('<a>aaa<a/>','<a>aaa<a/>')
            END
            END CONVERSATION @ConversationHandle
            COMMIT
      End Try
      Begin Catch
                  Rollback
                  print 'In Catch...'                
                  INSERT INTO DBO.tblErrorLog
                  SELECT
                  ERROR_NUMBER() AS ErrorNumber,
                  ERROR_SEVERITY() AS ErrorSeverity,
                  ERROR_STATE() AS ErrorState,
                  ERROR_LINE() AS ErrorLine,
                  ERROR_PROCEDURE() AS ErrorProcedure,
                  ERROR_MESSAGE() AS ErrorMessage,GETDATE(),
                  CONVERT(sysname ,USER_NAME())AS DBUSERNAME,
                  CONVERT(SYSNAME,SUSER_SNAME()) AS SYSUSERNAME
           
            Insert into dbo.tblErrorXML
                  Select @MessageBody
      End Catch
END
 
The above stored procedure uspInsertAuditRec gets the message from the receiving queue in AuditDatabase. It checks the validity of the message and inserts it into tblAud for further processing. Only BLOB (the message type that we will create soon) message types are processed.
The following stored procedures will parse XML and will mark the record in tblAud;
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        Atif Sheikh
-- Create date:  24-May-2010
-- Description:   Update Detail
-- =============================================
 
CREATE PROCEDURE [dbo].[uspUpdateDetail]
AS
BEGIN
      SET NOCOUNT ON;
      Declare @tid int
      Declare @DbName varchar(100)
      Declare @TableName varchar(100)
      Declare d1 Cursor for Select tid from tblAud where upd_det = 0
      Open d1
      Fetch Next from d1 into @tid
      while @@Fetch_Status = 0
      begin
            PRINT @tid
            select  @DbName = xmlQuery.value('(//SourceDb)[1]', 'varchar(50)') ,
                        @TableName = xmlQuery.value('(//SourceTable)[1]', 'varchar(50)')
            FROM tblAud where tid = @tid
            if @DbName is Not Null and @TableName is Not Null
                  exec uspUpdateAuditDetailFromAuditTableID @tid,@DbName,@TableName
 
            Fetch Next from d1 into @tid
      end  
      Deallocate d1
END
 
The above stored procedure gets record from tblAud that are not processes (Upd_Det = 0) and passes on to the stored procedure uspUpdateAuditDetailFromAuditTableID.
USE [AuditDatabase]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:        Atif-ullah Sheikh
-- Create date: 24-May-2008
-- Description:   Insert into tblAudDetail
-- =============================================
CREATE PROCEDURE [dbo].[uspUpdateAuditDetailFromAuditTableID]
(
      @pID int,
      @pDBName nvarchar(100),
      @pTableName nvarchar(100)    
)
AS
BEGIN
SET NOCOUNT ON;
      Begin Try
            Begin Tran
            Declare @ColName nvarchar(max)
            Declare @Chk1 nvarchar(max)
            Declare @Chk2 nvarchar(max)
            Declare @sSql varchar(max)
            Declare @SQL1 nvarchar(max)  
            Declare @SQL nvarchar(max)   
            Declare @pDef nvarchar(max)  
            Declare @sSql2 varchar(max)
            Declare @SelectCols varchar(max)
            Declare @SelectCols2 varchar(max)
            Declare @ObjId int
 
            Set @sql1 = N'Select @ObjId = Object_ID from ' + @pDBName + '.sys.objects where name = ''' + @pTableName + ''''
            Set @pDef = N'@ObjId varchar(100) OUTPUT'
            Exec sp_executesql @sql1 ,@pDef,@ObjId=@ObjId OUTPUT
           
            Set @sql = 'Declare col2 Cursor for Select name from ' + @pDBName + '.sys.columns where Object_Id = '+ cast(@ObjID as varchar(10))+ ' and system_type_id not in (34,35,99,241,165,173)'
            print(@sql)
            Exec (@sql)
            Open col2
 
            Declare @XMLvar   XML
            Select @XMLvar = xmlQuery from tblAud where tid = @pID
            Set @sql = ''
            Declare @SQL2 varchar(max)
            Declare @SQL3 varchar(max)
            Declare @SQL4 varchar(max)   
            Set @sql2 = ''
            Set @sql3 = ''
            Set @sql4 = ''
 
            Declare @cname varchar(100)
            Fetch next From Col2 into @cname
            Set @sql3 = '(Order By x.header.value(''@' + @cname + ''',''varchar(100)'')) '
            Set @sql = 'Declare @XMLvar   XML
                              Select @XMLvar = xmlQuery from tblAud where tid = ' + cast(@pID as varchar(10))+ '
                              Select * into ##ctbl from ( Select Row_Number() Over '
 
            while @@Fetch_Status = 0
            begin      
                  if @Sql2 <> '' Set @Sql2 = @Sql2 + ','
                  Set @Sql2 = @Sql2 + 'x.header.value(''@' + @cname + ''', ''varchar(50)'') ' + @cname + '_d'
 
                  if @Sql4 <> '' Set @Sql4 = @Sql4 + ','
                  Set @Sql4 = @Sql4 + 'x.header.value(''@' + @cname + ''', ''varchar(50)'') ' + @cname + '_i'  
                  Fetch next From Col2 into @cname
            end
 
            Set @sql = @sql + @sql3 + ' d_rid,' + @sql2
            Set @sql = @sql + ' FROM @XMLvar.nodes(''//ChangedData/t'') AS x(header)) xx
                                          Left OUter join (
                                          select  Row_number() over ' + @sql3 + ' i_rid,' + @sql4 + '
                                          FROM @XMLvar.nodes(''//NewRec/t'') AS x(header))yy
                                          on yy.i_rid = xx.d_rid '
            print(@sql)
            Deallocate Col2
            Exec (@sql)
           
            Declare @dval varchar(100)
            Declare @ival varchar(100)
            Declare @rid int
 
            Set @sql = 'Declare col2 Cursor SCROLL for Select name from ' + @pDBName + '.sys.columns where Object_Id = '+ cast(@ObjID as varchar(10))+ ' and system_type_id not in (34,35,99,241,165,173)'
            Exec (@sql)
            Open col2
            Declare @vRID int
            Declare Col3 Cursor for Select Case when i_RID is Null then d_RID else i_RID end from ##ctbl
            Open Col3
            Fetch Next from col3 into @rid
            while @@Fetch_Status = 0
            begin
                  Fetch next From Col2 into @cname
                  While @@Fetch_Status = 0
                  begin
                        Set @sql1 = ''
                        Set @sql1 = @sql1 + N' Select @dval1 = '+ @cname + '_d ,@ival1 = '+ @cname +'_i from ##ctbl where case when i_RID is Null then d_rid else i_rid end = ' + cast(@rid as varchar(10))
                        Set @pDef = N'@dval1 varchar(100) OUTPUT, @ival1 varchar(100) OUTPUT'
                        Set @vRID = 0
                        Select @vRID = i_rid from ##ctbl where case when i_RID is Null then d_rid else i_rid end = @rid
                        Exec sp_executesql @sql1 ,@pDef,@dval1=@dval OUTPUT,@ival1=@ival OUTPUT
                        if (IsNull(@dval,'') <> IsNull(@ival,'')) or @cname Like '%ID'
                        begin
                              Insert into tblAudDetail (tid,DBName,TableName,FieldName,PriorVal,CurrVal)
                                    Values (@pID,@pDBName,@pTableName,@cname,case when IsNull(@vRID,0) <> 0 then @dval else @ival end,case when IsNull(@vRID,0) <> 0 then @ival else @dval end)
                        end
                        Fetch next From Col2 into @cname
                  end
                  Fetch FIRST From Col2 into @cname
                  Fetch PRIOR From Col2 into @cname
                  Fetch Next from col3 into @rid
            end
            Deallocate Col2
            Deallocate Col3
            Drop table ##ctbl
            update tblAud set upd_det = 1 where tid = @pID
           
            Commit Tran
     
      End Try
      begin Catch
            Rollback
            INSERT INTO dbo.tblErrorLog
            SELECT
                  ERROR_NUMBER() AS ErrorNumber,
                  ERROR_SEVERITY() AS ErrorSeverity,
                  ERROR_STATE() AS ErrorState,
                  ERROR_LINE() AS ErrorLine,
                  ERROR_PROCEDURE() AS ErrorProcedure,
                  ERROR_MESSAGE() AS ErrorMessage,GETDATE(),
                  CONVERT(sysname ,USER_NAME())AS DBUSERNAME,
                  CONVERT(SYSNAME,SUSER_SNAME()) AS SYSUSERNAME  
      end catch
END
 
The code of uspUpdateAuditDetailFromAuditTableID needs a little bit of explanation. As you can see in the code, this stored procedure receives a parameter @pID which is the ID of tblAud. Another parameter is @pDBname which is the name of production database. Last parameter is @pTableName which is the name of the table from where it was initialized (production database table). Depending upon the parameters, uspUpdateAuditDetailFromAuditTableID parses the XML. The XML has  <ChangedData > and <NewData> elements. The <ChangedData> element holds the information of the original data (before change). <NewData> holds the information of new data (after change). Both these data sets are inserted into ##ctbl and then, checked column by column to get the changed information. Then <ChangedData> element and <NewRec> element holds the complete record. This is also illustrated in the above XML example. This is done via trigger using inserted and deleted memory tables. Here we can decide which column(s) are changed and initialize our next table tblAudDetail with the changed data only as discussed in the XML example above.  I am also inserting values of columns having ‘ID’ in their names. I am performing this to recognize the record based on the primary key. I am assuming here that the primary column name will have the ‘ID’ in its name.

2.     Activate Service Broker on New Audit Database and your own database

Next step is to activate service broker on both databases.
ALTER DATABASE MyProductionDB
SET ENABLE_BROKER
WITH ROLLBACK IMMEDIATE
GO
 
ALTER DATABASE AuditDatabase
SET ENABLE_BROKER
WITH ROLLBACK IMMEDIATE
GO
 
 
Run the following script to create service broker objects in MyProductionDB.
use MyProductionDB
drop master key
CREATE MASTER KEY
ENCRYPTION BY PASSWORD = 'onteuhoeu'
GO
 
The master key is created. According to MSDN, “The database master key is a symmetric key used to protect the private keys of certificates and asymmetric keys that are present in the database.”
CREATE MESSAGE TYPE BLOB
VALIDATION = NONE
GO
 
A message type defines the name of a message and the validation that Service Broker performs on messages that have that name. Both sides of a conversation must define the same message types. BLOB is the name of the message type we are defining.
 
CREATE CONTRACT BLOB_Contract
(BLOB SENT BY ANY)
GO
 
A contract defines the message types that are used in a Service Broker conversation and also determines which side of the conversation can send messages of that type. Each conversation follows a contract. The initiating service specifies the contract for the conversation when the conversation starts. The target service specifies the contracts that the target service accepts conversations for.
 
CREATE QUEUE BLOB_Queue_Init
GO
 
Queues store messages. When a message arrives for a service, Service Broker puts the message on the queue associated with the service.
 
CREATE SERVICE BLOB_Service_Init
ON QUEUE BLOB_Queue_Init
(BLOB_Contract)
 
A Service Broker service is a name for a specific task or set of tasks. Service Broker uses the name of the service to route messages, deliver messages to the correct queue within a database, and enforce the contract for a conversation.
 
 
Run the following script to create service broker objects in AuditDatabase. Same types of objects as above are created on AuditDatabase. But with different names;
 
Use AuditDatabase
GO
CREATE MASTER KEY
ENCRYPTION BY PASSWORD = 'onteuhoeu'
GO
 
CREATE MESSAGE TYPE BLOB
VALIDATION = NONE
GO
 
create CONTRACT BLOB_Contract
(BLOB SENT BY ANY)
GO
 
CREATE QUEUE BLOB_AdtQueue_Remote1
GO
CREATE SERVICE BLOB_AdtSrv_Remote1
 
ON QUEUE BLOB_AdtQueue_Remote1
(BLOB_Contract)
GO
 
 
We need to alter the queue BLOB_AdtQueue_Remote1 to set its status to ON and associate procedure to be executed when a message is inserted in a queue.
 
ALTER QUEUE BLOB_AdtQueue_Remote1
WITH ACTIVATION
(
    STATUS = ON,
    PROCEDURE_NAME = dbo.uspInsertAuditRec      ,
    MAX_QUEUE_READERS = 5,
    EXECUTE AS OWNER
)
GO
 
MAX_QUEUE_READERS specifies the maximum number of instances of the activation stored procedure that the queue starts simultaneously. The value of max_readers must be a number between 0 and 32767. I have set this value to 5 to avoid contention.
 
The security principal that owns the initiating service must have SEND permission on the target service. So we need to execute,
 
GRANT SEND ON SERVICE::BLOB_Service_Remote1 TO [Public];
GO
 
The SEND permissions are currently granted to the [Public]. Everyone can send message using this queue. I have done this for simplicity. You can specify a specific user to restrict the message sending via this service.
 
 

3.     Create stored procedure to apply triggers on tables for auditing

 

Next we will create stored procedures to apply triggers on the tables we need to Audit on the production database.

 
Use MyProductionDB
go
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
-- =============================================
-- Author: Atif Sheikh
-- Create date: 28-May-2010
-- Description:   Create Audit Trigger
-- =============================================
 
CREATE PROCEDURE [dbo].[uspCreateTriggerToAudit]     
      @TableName varchar(100)
AS
BEGIN
      SET NOCOUNT ON;
      Declare @SCHEMA varchar(100)
      Declare @colnames varchar(max)
      Declare @objid int
      Declare @sql varchar(max)
 
      Select @objid = object_id from sys.objects where name = @tableName
      Select @TABLENAME = table_name, @Schema = Table_Schema from information_schema.tables where table_name = @tableName
 
      Select @colnames = COALESCE(@colnames+', ','')+ '['+ sys.columns.name +']'
                                    from sys.columns where object_id = @objid
                                    and system_type_id not in (34,35,99,241,165,173)
     
      --Select * from sys.types
 
      SET @SQL='CREATE TRIGGER ['+@TABLENAME+'_AuditTrigger]
         ON  ['+@SCHEMA+'].['+@TABLENAME+']
         AFTER UPDATE, INSERT, DELETE
      AS
      BEGIN
 
 
            SET NOCOUNT ON;
 
 
            DECLARE @auditBody varchar(max)
            DECLARE @auditBody1 varchar(max)
            DECLARE @AppUserID varchar(max)
            DECLARE @Objectid bigint     
            DECLARE @DMLType CHAR(1)
            Set @auditBody1 = ''''
            Set @auditBody = ''''
 
            Set @AppUserID = ''''
           
            Select @Objectid = parent_object_id from sys.objects where name = ''' + @tableName + '_AuditTrigger''
            if Exists(Select column_id from sys.columns where object_id = @Objectid and name = ''UpdatedBy'')
            begin
                  --Select @AppUserID = UpdatedBy from inserted as ii
                  Set @AppUserID = ''''
            end              
 
 
            IF NOT EXISTS (SELECT * FROM inserted)
            BEGIN
                  SELECT      @auditBody = (select '+ @colnames +' FROM deleted AS t FOR XML AUTO)
                  Select @DMLType = ''D''
            END
            -- after update or insert statement
            ELSE
            BEGIN      
                  IF EXISTS (SELECT * FROM deleted)
                  begin
                        SELECT      @auditBody = (select '+ @colnames +' FROM deleted AS t FOR XML AUTO)
                        SELECT      @auditBody1 = (select '+ @colnames +' FROM Inserted AS t FOR XML AUTO)
                        SELECT      @DMLType = ''U''
                  end
                  ELSE
                  begin
                        SELECT      @auditBody = (select '+ @colnames +' FROM inserted AS t FOR XML AUTO)
                        SELECT      @DMLType = ''I''
                  end
            END  
            SELECT @auditBody =
                  ''<AuditMsg>
                        <SourceDb>'' + DB_NAME() + ''</SourceDb>
                        <SourceTable>' + @tableName + '</SourceTable>
                        <UserId>'' + SUSER_SNAME() + ''</UserId>
                        <AppUserId>'' + @AppUserID + ''</AppUserId>
                        <DMLType>'' + @DMLType + ''</DMLType>
                        <ChangedData>'' + CAST(@auditBody AS NVARCHAR(MAX)) + ''</ChangedData>''
            if @DMLType = ''U''
                        SELECT @auditBody = @auditBody + ''<NewRec>'' + @auditBody1 + ''</NewRec>''
 
            SELECT @auditBody = @auditBody + ''</AuditMsg>''                 
 
            if @auditBody <> ''''
            begin
                  DECLARE @h UNIQUEIDENTIFIER
                  Declare @CXml varchar(max)
                 
                  Set @CXml = @auditBody
 
                  BEGIN DIALOG CONVERSATION @h
                  FROM SERVICE BLOB_Service_Init
                  TO SERVICE ''BLOB_AdtSrv_Remote1''
                  ON CONTRACT BLOB_Contract
                  WITH ENCRYPTION=OFF     ;
                 
                  SEND ON CONVERSATION @h
                  MESSAGE TYPE BLOB (CONVERT(VARBINARY(max), @CXml))
            end        
      END'
      print @SQL
      set ANSI_NULLS ON
      set QUOTED_IDENTIFIER ON
      exec (@SQL)
END
 
The above stored procedure [dbo].[uspCreateTriggerToAudit] is created on the production database and it accepts parameter @TableName on which trigger is created on the fly. This stored procedure holds the generic dynamic sql. This sql will be used to create a trigger on the tables we need to audit. I will further explain later.
 
The following stored procedure on production database takes the comma separated list of object_id of tables we want to audit on Production database.
Use MyProductionDB
go
CREATE PROCEDURE [dbo].[uspGetAuditTables]
      @pObjectID  varchar(max)      ,
      @pAlias varchar(max) = NULL
AS
BEGIN
 
      SET NOCOUNT ON;
 
      Declare @val varchar(1000)
      Declare @val2 varchar(1000)
      Declare @tab as Table (ObjectID bigint, Alias nvarchar(1000))
 
      Insert into @tab
            Select * from tblAuditTables
 
      delete from tblAuditTables
     
      insert into tblAuditTables (ObjectID,Alias)
                  select a.value,b.value from dbo.fnSplit(@pObjectID,',') a, dbo.fnSplit(@pAlias,',') b
                  where a.tid = b.tid
     
      Declare C1 Cursor For select name from dbo.fnSplit(@pObjectID,',') inner join sys.objects on sys.objects.Object_id = value
      Open C1
      Fetch Next from C1 into @val
      while @@Fetch_Status = 0
      begin
            print(@val)
            if not Exists(Select object_id from sys.objects where name = rtrim(ltrim(@val)) + '_AuditTrigger' and Type = 'TR')
            begin
                  Set @val = rtrim(ltrim(@val))
                  exec uspCreateTriggerToAudit @val
            end
            Fetch Next from C1 into @val
      end
 
      Close C1
      Deallocate C1
 
      Declare C1 Cursor For
      Select sys.objects.name,sys.schemas.name from sys.objects inner join sys.schemas on sys.schemas.schema_id = sys.objects.schema_id where Object_id in
      (Select objectid from @tab
      Except
      Select objectid from tblAuditTables)
     
      Declare @sSql varchar(1000)
     
      Open C1
      Fetch Next from C1 into @val,@val2
      while @@Fetch_Status = 0
      begin      
            Set @sSql = 'Drop Trigger [' + @val2 + '].[' + @val + '_AuditTrigger]'       
            Exec (@sSql)
            Fetch Next from C1 into @val,@val2
      end  
      Close C1
      Deallocate C1
 
END
 
The above stored procedure gets the names of the tables we need to audit. Here we execute the dynamic sql in uspCreateTriggerToAudit according to the object_ID passed to it. For example we have a table UserTable (object_id = 1011235) in the production database which we need to audit. To apply the trigger on this table, we will call the above mentioned stored procedure as;
exec [dbo].[uspGetAuditTables] ‘1011235’, ‘UserTable’
 
On executing you will see the trigger on the UserTable.
The following table tblAuditTables holds the list of tables that we are auditing. I did this so that we can have a quick check of the tables we are auditing.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[tblAuditTables](
      [ObjectID] [int] NOT NULL,
      [Alias] [nvarchar](1000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 CONSTRAINT [PK_tblAuditTables] PRIMARY KEY CLUSTERED
(
      [ObjectID] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
 
This table only keeps the record of the tables that are being audited.
Now change any value in UserTable. It will be reflected in tblAud as XML in xmlQuery column. You can create reports on Audit data from Audit database according to your requirements. I have created the Insert / Update /Delete statements of the production database from the AuditDatabase.
 
Conclusion
After implementing the above solution, you will be able to track all the changes on the tables on which audit is applied. Also, you can filter out the tables of production database on which you don’t want to apply audit. Due to asynchronous messaging, performance will not be hurt due to the use of triggers. There is also no point of data loss as all the messages are maintained by SQL Server itself.
This long solution might have some drawbacks. I am currently using it on one of my systems in QA without any error (till now). And we are planning to ship it to production server. Any comments / suggestions are most welcome.
 
Leave a Comment
  • Please add 8 and 1 and type the answer here:
  • Post
Wiki - Revision Comment List(Revision Comment)
Sort by: Published Date | Most Recent | Most Useful
Comments
Page 1 of 1 (1 items)
Wikis - Comment List
Sort by: Published Date | Most Recent | Most Useful
Posting comments is temporarily disabled until 10:00am PST on Saturday, December 14th. Thank you for your patience.
Comments
Page 1 of 1 (2 items)