info@techdevops.com | 437-991-3573 | Data Engineering Services
TechDevOps.com
Resources Tools
Experts in Microsoft SQL Server on Windows, Linux, Containers | Clusters, Always On, FCI | Migrations, Cloud, Performance



SQL Replication Monitor
by BF (Principal Consultant; Architecture; Engineering)
2017-09-09








SQL Replication Monitor - Version 1:

Captures, Logs, Alerts & Reports for Microsoft SQL Server Transaction Replication Distribution Metrics.

The below T-SQL script will output the Transactional Replication Distribution Metrics per Publication per Distribution Agents. We recommend you add this Stored Procedure in a SQL Agent Job at run on at least a 15 min basis - if you have a busy database system. This object has parameters and also added the ability for Warnings and also emails a HTML generated report. The report has row-level markers that differentiate each type Replication State for easy display to the user indicating the items needing attention. We also include a permanent table for logging of all of the metrics. If there is a Warning Threshold Passed - Log and Notify. If there is no Warning Threshold Passed - Log only.


T-SQL Solution:


Create Logging Table:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[LOG_REPLICATION_DISTRIBUTION_MONITOR](
[LogID] [int] IDENTITY(1,1) NOT NULL,
[State] [int] NULL,
[Publication] [varchar](150) NULL,
[Distribution System] [varchar](250) NULL,
[Agent_ID] [int] NULL,
[Time] [datetime] NULL,
[SinceLastStats_ElapsedTime] [varchar](10) NULL,
[SinceLastStats_Work] [varchar](10) NULL,
[SinceLastStats_Cmds] [varchar](10) NULL,
[SinceLastStats_CmdsPerSec] [varchar](10) NULL,
[SinceLastStats_Reader_Fetch] [varchar](10) NULL,
[SinceLastStats_Reader_Wait] [varchar](10) NULL,
[SinceLastStats_Writer_Write] [varchar](10) NULL,
[SinceLastStats_Write_Wait] [varchar](10) NULL,
[InsertedDate] [datetime] NULL,
[PeriodEnding] [datetime] NULL,
CONSTRAINT [PK_LOG_REPLICATION_DISTRIBUTION_MONITOR] PRIMARY KEY CLUSTERED
(
[LogID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO



Create Stored Procedure:


Create Procedure [dbo].[Admin_Server_Replication_Monitor_Distribution]
@PublicationName varchar(150),
@WarningThresholdPercent decimal(18,0) = 0, --Future
@WarningThresholdValue decimal(18,0) = 0, --Future
@AlertThresholdPercent decimal(18,0) = 0, --Future
@AlertThresholdValue decimal(18,0) = 0, --Future
@AlertStateChange int = 1,
@EmailReport int = 1,
@Debug int = 0
as

Set Nocount ON

/*
Reference: www.TechDevOps.com
*/

/*
Debug:
Exec dbo.Admin_Server_Replication_Monitor_Distribution 'EnterPublicationName',0,0,0,0,1,1,1

Email if Warning:
Exec dbo.Admin_Server_Replication_Monitor_Distribution 'EnterPublicationName',0,0,0,0,1,1,0
*/

Declare @PeriodEnding datetime;
Declare @IsWarnings bit = 0;
Declare @IsAlerts bit = 0;
Declare @tableHTML nvarchar(max);
Declare @TotalDistroAgents int = 0
Set @PeriodEnding = GetDate();

Declare @agent_id int
Declare @timeindays int = 1

Create Table #TMP ([State] int, [Distribution System] varchar(250), Agent_ID int, [Time] datetime, SinceLastStats_ElapsedTime varchar(10), SinceLastStats_Work varchar(10), SinceLastStats_Cmds varchar(10), SinceLastStats_CmdsPerSec varchar(10),
SinceLastStats_Reader_Fetch varchar(10), SinceLastStats_Reader_Wait varchar(10), SinceLastStats_Writer_Write varchar(10), SinceLastStats_Write_Wait varchar(10))

Create Table #AGENTS (AgentID int)
Insert into #AGENTS(AgentID) Select ID from Distribution.dbo.MSdistribution_agents where Publication = @PublicationName order by ID ASC;
Set @TotalDistroAgents = (Select count(*) from #AGENTS)

While Exists (Select 1 from #AGENTS)
Begin

Set @Agent_id = (Select top 1 AgentID from #AGENTS order by AgentID ASC);

;with cte_stats as(
select h.agent_id, ag.name, h.time, cast(h.comments as xml) comments
from Distribution.dbo.MSdistribution_history h with (nolock)
inner join Distribution.dbo.MSdistribution_agents ag with (nolock) on h.agent_id = ag.id
where
(h.[comments] like '<stats state="1"%' or h.[comments] like '<stats state="2"%')
and h.agent_id = @Agent_id
and h.time > getdate() - @timeindays
)
Insert into #TMP
select
c.value('(/stats/@state)[1]', 'bigint') AS [State]
,cte_stats.Name as 'Distribution System'
,cte_stats.Agent_ID
,cte_stats.[Time]
--,c.value('(/stats/@work)[1]', 'bigint') AS [work]
--,c.value('(/stats/@idle)[1]', 'bigint') AS [idle]
--,c.value('(/stats/reader/@fetch)[1]', 'bigint') AS [reader_fetch]
--,c.value('(/stats/reader/@wait)[1]', 'bigint') AS [reader_wait]
--,c.value('(/stats/writer/@write)[1]', 'bigint') AS [writer_write]
--,c.value('(/stats/writer/@wait)[1]', 'bigint') AS [writer_wait]
,c.value('(/stats/sincelaststats/@elapsedtime)[1]', 'bigint') AS [sincelaststats_elapsedtime]
,c.value('(/stats/sincelaststats/@work)[1]', 'bigint') AS [sincelaststats_work]
,c.value('(/stats/sincelaststats/@cmds)[1]', 'bigint') AS [sincelaststats_cmds]
,c.value('(/stats/sincelaststats/@cmdspersec)[1]', 'float') AS [sincelaststats_cmdspersec]
,c.value('(/stats/sincelaststats/reader/@fetch)[1]', 'bigint') AS [sincelaststats_reader_fetch]
,c.value('(/stats/sincelaststats/reader/@wait)[1]', 'bigint') AS [sincelaststats_reader_wait]
,c.value('(/stats/sincelaststats/writer/@write)[1]', 'bigint') AS [sincelaststats_writer_write]
,c.value('(/stats/sincelaststats/writer/@wait)[1]', 'bigint') AS [sincelaststats_writer_wait]
--,cte_stats.[comments]
from cte_stats
cross apply cte_stats.comments.nodes('/stats[@state="1"]') b(c)

union all

select
c.value('(/stats/@state)[1]', 'int') AS [State]
,cte_stats.Name as 'Distribution System'
,cte_stats.Agent_ID
,cte_stats.[Time]
--,c.value('(/stats/@work)[1]', 'bigint') AS [work]
--,c.value('(/stats/@idle)[1]', 'bigint') AS [idle]
--,c.value('(/stats/reader/@fetch)[1]', 'bigint') AS [reader_fetch]
--,c.value('(/stats/reader/@wait)[1]', 'bigint') AS [reader_wait]
--,c.value('(/stats/writer/@write)[1]', 'bigint') AS [writer_write]
--,c.value('(/stats/writer/@wait)[1]', 'bigint') AS [writer_wait]
,c.value('(/stats/sincelaststats/@elapsedtime)[1]', 'bigint') AS [sincelaststats_elapsedtime]
,c.value('(/stats/sincelaststats/@work)[1]', 'bigint') AS [sincelaststats_work]
,c.value('(/stats/sincelaststats/@cmds)[1]', 'bigint') AS [sincelaststats_cmds]
,c.value('(/stats/sincelaststats/@cmdspersec)[1]', 'float') AS [sincelaststats_cmdspersec]
,c.value('(/stats/sincelaststats/reader/@fetch)[1]', 'bigint') AS [sincelaststats_reader_fetch]
,c.value('(/stats/sincelaststats/reader/@wait)[1]', 'bigint') AS [sincelaststats_reader_wait]
,c.value('(/stats/sincelaststats/writer/@write)[1]', 'bigint') AS [sincelaststats_writer_write]
,c.value('(/stats/sincelaststats/writer/@wait)[1]', 'bigint') AS [sincelaststats_writer_wait]
--,cte_stats.[comments]
from cte_stats
cross apply cte_stats.comments.nodes('/stats[@state="2"]') b(c)
order by cte_stats.agent_id, cte_stats.name asc, cte_stats.time desc;

Insert into [dbo].[LOG_REPLICATION_DISTRIBUTION_MONITOR]
([State],[Publication],[Distribution System],[Agent_ID],[Time],[SinceLastStats_ElapsedTime],[SinceLastStats_Work],
[SinceLastStats_Cmds],[SinceLastStats_CmdsPerSec],[SinceLastStats_Reader_Fetch],[SinceLastStats_Reader_Wait],
[SinceLastStats_Writer_Write],[SinceLastStats_Write_Wait],[InsertedDate],[PeriodEnding])
Select top 5 --############ Get Lastest 15 minutes of Distribution Stats Logs ############
[State],@PublicationName,[Distribution System],[Agent_ID],[Time],[SinceLastStats_ElapsedTime],ISNULL(SinceLastStats_Work,'-'),
ISNULL(SinceLastStats_Cmds,'-'),ISNULL(SinceLastStats_CmdsPerSec,'-'),ISNULL(SinceLastStats_Reader_Fetch,'-'),ISNULL(SinceLastStats_Reader_Wait,'-'),
ISNULL(SinceLastStats_Writer_Write,'-'),ISNULL(SinceLastStats_Write_Wait,'-'), GetDate(), @PeriodEnding
from #TMP order by Agent_ID ASC, [Distribution System] ASC, [Time] DESC

Delete from #AGENTS where AgentID = @agent_id
Set @Agent_id = ''
Truncate Table #TMP
End

--Begin Warnings:
--State Change:
IF @AlertStateChange = 1
Begin
IF EXISTS(Select 1 from [dbo].[LOG_REPLICATION_DISTRIBUTION_MONITOR] where PeriodEnding = @PeriodEnding and [State] = 2)
Begin
Set @IsWarnings = 1
Select 'Warning[!] Replication Distribution: State = ' + Convert(varchar(5),[State]) + ' on ' + [Distribution System] as 'Message' from [dbo].[LOG_REPLICATION_DISTRIBUTION_MONITOR] where PeriodEnding = @PeriodEnding and [State] = 2
End
Else
Begin
Select 'No Warnings' as 'Message Warnings:'
End
End

--Begin Notifications:
If (@IsWarnings = 1 and @EmailReport = 1) or @Debug = 1
Begin

SET @tableHTML =
N'<br/><H4><Font Color=black><font face="Arial, Verdana, sans-serif;"><u>REPORT TITLE:</u> REPLICATION DISTRIBUTION MONITOR - WARNINGS & ALERTS</H4>' +
N'<H4><Font Color=black><font face="Arial, Verdana, sans-serif;">' + 'Source: ' + @@SERVERNAME + + '; Publication: ' +
@PublicationName + '; Total Distribution Agents:' + convert(nvarchar(25),@TotalDistroAgents) + '</H4></font>' +
N'<H4><Font Color=black><font face="Arial, Verdana, sans-serif;">' + 'Report Runtime: ' + convert(nvarchar(25),@PeriodEnding) + '</H4></font><br/>' +
N'<table border="1" style=”font-family:arial;color:black;font-size:11pt;”> ' +
N'<tr align="left">
<th align="left" bgcolor="black" style="color:white;">State</th>
<th align="left" bgcolor="black" style="color:white;">Distribution System (Publisher-PublisherDB-Publication-Subscriber)</th>
<th align="left" bgcolor="black" style="color:white;">Occurrence</th>
<th align="left" bgcolor="black" style="color:white;">SinceLastStats(s)</th>
<th align="left" bgcolor="black" style="color:white;">Agent Working(s)</th>
<th align="left" bgcolor="black" style="color:white;">Commands</th>
<th align="left" bgcolor="black" style="color:white;">CommandsPerSec</th>
<th align="left" bgcolor="black" style="color:white;">Reader Fetch(s)</th>
<th align="left" bgcolor="black" style="color:white;">Reader Wait(s)</th>
<th align="left" bgcolor="black" style="color:white;">Writer Write(s)</th>
<th align="left" bgcolor="black" style="color:white;">Writer Wait(s)</th>
</tr>' +
CAST ((

Select
Case
When [State] = 1 then '#d9d9d9' Else '#ffff33' end AS [@bgcolor],
td = [State], '',
td = [Distribution System], '',
td = CONVERT(VARCHAR, [Time], 20), '',
td = [SinceLastStats_ElapsedTime], '',
td = [SinceLastStats_Work], '',
td = [SinceLastStats_Cmds], '',
td = [SinceLastStats_CmdsPerSec], '',
td = [SinceLastStats_Reader_Fetch], '',
td = [SinceLastStats_Reader_Wait], '',
td = [SinceLastStats_Writer_Write], '',
td = [SinceLastStats_Reader_Wait], ''
From
(
Select
*
from [dbo].[LOG_REPLICATION_DISTRIBUTION_MONITOR]
where PeriodEnding = @PeriodEnding
) t

FOR XML PATH('tr'), TYPE
) AS NVARCHAR(MAX) ) +
N'</table>' ;

Set @tableHTML = @tableHTML + '</br></br>' + '<font size=2>- Data is the latest Distribution Stats logged per Distribution Agent'

Set @tableHTML = @tableHTML + '</br></br>' + '<font size=2>- Interval: 15mins; If Warnings or Alerts - Log + Email; If No Warnings or Alerts - Log Only'

Set @tableHTML = @tableHTML + '</br></br>' + '<font size=2>- Source: Admin_Server_Replication_Monitor_Distribution'

Set @tableHTML = @tableHTML + '</br></br>' + '- Delivered by DBMail on SRV-NYC-01' +
+
'</font>'

If @tableHTML <> '' and @tableHTML IS NOT NULL
Begin
Declare @ProfileName as varchar(100)
Set @ProfileName = (Select value from ops.dbo.config where configid = 1)
Declare @Recipients as varchar(100)
Set @Recipients = (Select value from ops.dbo.config where configid = 2)

EXEC msdb.dbo.sp_send_dbmail
@profile_name = @ProfileName,
@recipients= 'ops@xyz.ca',
@subject = 'MSSQL - Replication Distribution Monitor - Warnings & Alerts - SRV-NYC-01',
@body = @tableHTML,
@body_format = 'HTML' ;
End
End
--Select * from [dbo].[LOG_REPLICATION_DISTRIBUTION_MONITOR] Order By Agent_ID ASC, [Distribution System] ASC, [Time] ASC

Drop Table #TMP
Drop Table #AGENTS