We have a few reporting servers where we have SQL Server Log Shipping setup and we are responsible for making sure the data is in sync for reporting users to generate reports. If there is an issue we need to be notified in order to fix the issue ASAP. In this tip I will demonstrate how we can automate SQL Server Log Shipping monitoring and keep the stakeholders informed.
SolutionLet's jump right in and walk through how to setup SQL Server Log Shipping monitoring.
Setup SQL Server Linked Servers for Log Shipping Monitoring Step 1Create a Linked Server to each server which is setup for Log Shipping on your monitoring server and ensure you test each Linked Server.
Setup SQL Server Table for Log Shipping Monitoring Step 2Create a table for centralized data and monitoring.
CREATE TABLE [dbo].[LSServers]([SN] [int] IDENTITY(1,1) NOT NULL,
[PRIMARY_SERVER] [sysname] NULL,
[SECONDARY_SERVER] [sysname] NULL,
[SQLVersion] [varchar](20) NULL
) ON [PRIMARY]
GO Insert Records into the SQL Server Log Shipping Monitoring Table
Step 3:Insert records of all your PRIMARY and SECONDARY servers in the monitoring table as shown below:
Image may be NSFW.
Clik here to view.

SQL Server Stored Procedure to Capture Log Shipping Status
Step 4:Create the below stored procedure on your monitoring server to pull the SQL Server Log Shipping status from the respective primary and secondary servers based on the SQL Server version. At a high level, this code captures the Log Shipping status based on the SQL Server version from the corresponding system tables in the MSDB database then inserts the data into the centralized monitoring table created in step 2. Then the status for each record in the monitoring table is updated. The code finishes with sending a status email.
USE [DBA]GO
CREATE PROCEDURE [dbo].[usp_GetLogShippingStatus]
@mode BIT = 0
AS
BEGIN
SET NOCOUNT ON
SET XACT_ABORT ON
DECLARE @Recipients VARCHAR(275)
DECLARE @MailSubject VARCHAR(275)
DECLARE @Xml VARCHAR(MAX)
DECLARE @Mailtext VARCHAR(MAX)
DECLARE @Server VARCHAR(25)
DECLARE @Curdate DATETIME
DECLARE @MailString VARCHAR(MAX)
DECLARE @Note NVARCHAR(1000)
DECLARE @Filedate DATETIME
DECLARE @Dbname SYSNAME
DECLARE @Latency SYSNAME
DECLARE @Filename NVARCHAR (500)
DECLARE @Tempname NVARCHAR (500)
DECLARE @Primary SYSNAME
DECLARE @Secondary SYSNAME
DECLARE @SQLVersion VARCHAR(20)
DECLARE @Sql VARCHAR(500)
SET @Curdate = GETDATE()
SET @Server = @@SERVERNAME
SET @Recipients = 'Atul.Gaikwad@SQLDBAExperts.com'
IF OBJECT_ID('tempdb..#TABLE_LS_MONITOR') IS NOT NULL
DROP TABLE #TABLE_LS_MONITOR
CREATE TABLE #TABLE_LS_MONITOR
(
SN INT IDENTITY(1,1)
,PRIMARY_SERVER SYSNAME NULL
,PRIMARY_DATABASE SYSNAME NULL
,SECONDARY_SERVER SYSNAME NULL
,SECONDARY_DATABASE SYSNAME NULL
,LSSTATUS VARCHAR(10)
,LAST_BACKUP_FILE NVARCHAR(500) NULL
,BACKUP_THRESHOLD INT NULL
,LAST_BACKUP_TIME DATETIME
,TIME_SINCE_LAST_BACKUP INT NULL
,LAST_RESTORED_FILE NVARCHAR(500) NULL
,RESTORE_THRESHOLD INT NULL
,LAST_RESTORE_TIME DATETIME
,TIME_SINCE_LAST_RESTORE INT NULL
,LAST_RESTORED_LATENCY INT NULL
)
DECLARE LSServers CURSOR FOR
SELECT PRIMARY_SERVER, SECONDARY_SERVER, SQLVersion FROM LSServers;
OPEN LSServers
FETCH NEXT FROM LSServers
INTO @Primary, @Secondary, @SQLVersion
WHILE @@FETCH_STATUS = 0
BEGIN
IF @SQLVersion in ('SQL2016','SQL2014','SQL2012','SQL2008','SQL2008R2','SQL2005')
BEGIN
SET @Sql = 'SELECT p.primary_server,p.primary_database, s.secondary_server,
s.secondary_database, p.last_backup_file, p.backup_threshold,
p.last_backup_date, s.last_restored_file, s.restore_threshold, s.last_restored_date,
s.last_restored_latency
FROM ' + @Primary + '.msdb.dbo.log_shipping_monitor_primary p INNER JOIN '
+ @Secondary + '.msdb.dbo.log_shipping_monitor_secondary s
ON p.primary_server = s.primary_server and p.primary_database = s.primary_database'
INSERT INTO #TABLE_LS_MONITOR (PRIMARY_SERVER, PRIMARY_DATABASE, SECONDARY_SERVER,
SECONDARY_DATABASE, LAST_BACKUP_FILE, BACKUP_THRESHOLD, LAST_BACKUP_TIME,
LAST_RESTORED_FILE, RESTORE_THRESHOLD, LAST_RESTORE_TIME, LAST_RESTORED_LATENCY)
EXEC(@Sql)
END
IF @SQLVersion = 'SQL2000'
BEGIN
SET @Sql = 'SELECT p.primary_server_name,p.primary_database_name,
s.secondary_server_name, s.secondary_database_name, p.last_backup_filename,
p.backup_threshold, p.last_updated, s.last_loaded_filename, s.out_of_sync_threshold,
s.last_loaded_last_updated
FROM ' + @Primary + '.msdb.dbo.log_shipping_primaries p INNER JOIN '
+ @Primary + '.msdb.dbo.log_shipping_secondaries s
ON p.primary_id = s.primary_id'
INSERT INTO #TABLE_LS_MONITOR (PRIMARY_SERVER, PRIMARY_DATABASE, SECONDARY_SERVER,
SECONDARY_DATABASE, LAST_BACKUP_FILE, BACKUP_THRESHOLD,
LAST_BACKUP_TIME, LAST_RESTORED_FILE, RESTORE_THRESHOLD, LAST_RESTORE_TIME)
EXEC(@Sql)
END
FETCH NEXT FROM LSServers
INTO @Primary, @Secondary, @SQLVersion
END
CLOSE LSServers;
DEALLOCATE LSServers;
UPDATE #TABLE_LS_MONITOR SET TIME_SINCE_LAST_BACKUP = DATEDIFF(mi, LAST_BACKUP_TIME,
@Curdate), TIME_SINCE_LAST_RESTORE = DATEDIFF(mi, LAST_RESTORE_TIME, @Curdate)
UPDATE #TABLE_LS_MONITOR SET LAST_BACKUP_FILE = SUBSTRING(LAST_BACKUP_FILE,
LEN(LAST_BACKUP_FILE) - CHARINDEX('\', REVERSE(LAST_BACKUP_FILE))+2, LEN(LAST_BACKUP_FILE)),
LAST_RESTORED_FILE = SUBSTRING(LAST_RESTORED_FILE, LEN(LAST_RESTORED_FILE) - CHARINDEX('\',
REVERSE(LAST_RESTORED_FILE))+2, LEN(LAST_RESTORED_FILE))
WHERE CHARINDEX(N'\',LAST_BACKUP_FILE)!=0 OR CHARINDEX(N'\',LAST_RESTORED_FILE)!=0
DECLARE LSCURSOR CURSOR FOR
SELECT PRIMARY_DATABASE, LAST_RESTORED_FILE
FROM #TABLE_LS_MONITOR WHERE LAST_RESTORED_LATENCY IS NULL;
OPEN LSCURSOR
FETCH NEXT FROM LSCURSOR
INTO @Dbname, @Filename
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT @Tempname = RIGHT (@Filename, LEN (@Filename) - (LEN(@Dbname) + LEN ('_tlog_')))
IF (CHARINDEX ('.',@Tempname,0) > 0)
SELECT @Tempname = LEFT (@Tempname, CHARINDEX ('.',@Tempname,0) - 1)
SELECT @Filedate = CONVERT (DATETIME,SUBSTRING (@Tempname, 1,8),112)
IF (LEN (@Tempname) = 12)
BEGIN
SELECT @Filedate = DATEADD (hh, CONVERT (INT, SUBSTRING (@Tempname,9,2)),@Filedate)
SELECT @Filedate = DATEADD (mi, CONVERT (INT, SUBSTRING (@Tempname,11,2)),@Filedate)
END
UPDATE #TABLE_LS_MONITOR SET LAST_RESTORED_LATENCY = datediff(mi, @Filedate,
LAST_RESTORE_TIME) WHERE LAST_RESTORED_LATENCY IS NULL
FETCH NEXT FROM LSCURSOR
INTO @Dbname, @Filename
END
CLOSE LSCURSOR;
DEALLOCATE LSCURSOR;
UPDATE #TABLE_LS_MONITOR SET LSSTATUS = CASE
WHEN TIME_SINCE_LAST_BACKUP > BACKUP_THRESHOLD THEN 'BAD'
WHEN TIME_SINCE_LAST_RESTORE > RESTORE_THRESHOLD THEN 'BAD'
WHEN LAST_RESTORED_LATENCY > RESTORE_THRESHOLD THEN 'BAD'
ELSE 'GOOD' END
SET @Mailtext ='<html>
<style type="text/css">
table.gridtable {
font-family: verdana,arial,sans-serif;
font-size:12px;
color:#000000;
border-width: 1px;
border-color: #666666;
border-collapse: collapse;
}
table.gridtable th {
border-width: 1px;
font-size:11px;
border-style: solid;
border-color: #666666;
}
table.gridtable td {
border-width: 1px;
font-size:11px;
border-style: solid;
border-color: #666666;
}
</style>
<body>
<table class="gridtable">
<tr bgcolor = ''''#808080''''>
<th> Primary_Server </th> <th> Primary_DB </th> <th> Secondary_Server </th>
<th> Secondary_DB </th> <th> Status </th> <th> Last Backup File</th>
<th> Backup Threshold </th> <th> TimeSince LastBackup </th>
<th> Last Restored File </th> <th> Restore Threshold </th>
<th> TimeSince