Here is a structure that is at least very close. There is no programmatic way to get the parameters (unfortunately). You need to format them into XML to pass in. The Login that initiates a SQL Agent job seems to only be logged in the `message` column of `msdb.dbo.sysjobhistory`, for `step_id = 0`. This value can probably be extracted if `ORIGINAL_LOGIN()` = `NT SERVICE\SQLSERVERAGENT`. Below is the schema and procs. The concept is to separate "init", "in process", and "completed (success or error)" logging. Records get updated via `@@SPID` and `DB_ID` and `ObjectID`. You get ObjectID to pass in from `@@PROCID`. **Schema, Tables, and Indexes** CREATE SCHEMA [Logging]; GO CREATE TABLE Logging.[Status] ( [StatusID] TINYINT NOT NULL CONSTRAINT [PK_Status] PRIMARY KEY CLUSTERED, [StatusName] VARCHAR(50) NOT NULL ); GO CREATE TABLE [Logging].[ProcessLog] ( ProcessLogID INT NOT NULL IDENTITY(-2147483648, 1) CONSTRAINT [PK_ProcessLog] PRIMARY KEY CLUSTERED, DatabaseID INT NOT NULL, ObjectID INT NULL, -- NULL = ad hoc query SessionID SMALLINT NOT NULL CONSTRAINT [DF_ProcessLog_SessionID] DEFAULT (@@SPID), Step TINYINT NOT NULL, -- if you have more than 255 steps, consult psychiatrist StatusID TINYINT NOT NULL CONSTRAINT [FK_ProcessLog_Status] FOREIGN KEY REFERENCES [Logging].[Status]([StatusID]), Remarks VARCHAR(MAX) NULL, -- or maybe NVARCHAR(MAX) Params XML NULL, RowsSelected INT NULL, RowsInserted INT NULL, RowsUpdated INT NULL, RowsDeleted INT NULL, StartedBy [sysname] NULL, StartedAt DATETIME2 NOT NULL CONSTRAINT [DF_ProcessLog_StartedAt] DEFAULT (SYSDATETIME()), UpdatedAt DATETIME2 NULL, -- use to show progress ; StoppedAt DATETIME2 NULL ); GO CREATE NONCLUSTERED INDEX [IX_ProcessLog_DB_Object_SPID] ON [Logging].[ProcessLog]([DatabaseID] ASC, [ObjectID] ASC, [SessionID] ASC, [StatusID] ASC); WHERE [StatusID] IN (1, 2); -- ignore closed-out entries (keeps index smaller / faster) GO **Stored Procedure to call at the very beginning of "logged" stored procedures** <!-- language: lang-sql --> CREATE PROCEDURE [Logging].[ProcessLog_Start] ( @DatabaseID INT, @ObjectID INT, @Params XML ) AS SET NOCOUNT ON; -- First, make sure that prior process didn't fail and skip CATCH block IF (EXISTS(SELECT * FROM [Logging].[ProcessLog] pl WHERE pl.[DatabaseID] = @DatabaseID AND pl.[ObjectID] = @ObjectID AND pl.[SessionID] = @@SPID AND pl.[StatusID] IN (1, 2) )) BEGIN UPDATE pl SET pl.[StatusID] = 4, -- failed pl.[Remarks] = 'Process never completed' -- ,pl.[StoppedAt] = SYSDATETIME() -- optional set ending time FROM [Logging].[ProcessLog] pl WHERE pl.[DatabaseID] = @DatabaseID AND pl.[ObjectID] = @ObjectID AND pl.[SessionID] = @@SPID AND pl.[StatusID] IN (1, 2); -- make sure to not update closed out entries END; -- Now it should be safe to create a new entry INSERT INTO [Logging].[ProcessLog] ([DatabaseID], [ObjectID], [Step], [StatusID], [Params], [StartedBy]) VALUES (@DatabaseID, @ObjectID, 0, 1, @Params, ORIGINAL_LOGIN()); GO **Stored Procedure to call after all but the final step** <!-- language: lang-sql --> CREATE PROCEDURE [Logging].[ProcessLog_Log] ( @DatabaseID INT, @ObjectID INT, @Step TINYINT, @RowsSelected INT = NULL, @RowsInserted INT = NULL, @RowsUpdated INT = NULL, @RowsDeleted INT = NULL ) AS SET NOCOUNT ON; UPDATE pl SET pl.[StatusID] = 2, -- In process pl.[Step] = @Step, pl.[UpdatedAt] = SYSDATETIME(), pl.[RowsSelected] = ISNULL(@RowsSelected, pl.[RowsSelected]), pl.[RowsInserted] = ISNULL(@RowsSelected, pl.[RowsInserted]), pl.[RowsUpdated] = ISNULL(@RowsSelected, pl.[RowsUpdated]), pl.[RowsDeleted] = ISNULL(@RowsSelected, pl.[RowsDeleted]) FROM [Logging].[ProcessLog] pl WHERE pl.[DatabaseID] = @DatabaseID AND pl.[ObjectID] = @ObjectID AND pl.[SessionID] = @@SPID AND pl.[StatusID] IN (1, 2); -- make sure to not update closed out entries IF (@@ROWCOUNT = 0) BEGIN RAISERROR('no initial or in-process record!', 16, 1); RETURN; END; GO **Stored Procedure to call after the final step OR in a CATCH block** <!-- language: lang-sql --> CREATE PROCEDURE [Logging].[ProcessLog_Stop] ( @DatabaseID INT, @ObjectID INT, @Step TINYINT, @StatusID TINYINT, @Remarks VARCHAR(MAX) = NULL, @RowsSelected INT = NULL, @RowsInserted INT = NULL, @RowsUpdated INT = NULL, @RowsDeleted INT = NULL ) AS SET NOCOUNT ON; UPDATE pl SET pl.[StatusID] = @StatusID, -- 3 = Success, 4 = Fail pl.[Step] = @Step, pl.[Remarks] = @Remarks, pl.[StoppedAt] = SYSDATETIME(), pl.[RowsSelected] = ISNULL(@RowsSelected, pl.[RowsSelected]), pl.[RowsInserted] = ISNULL(@RowsSelected, pl.[RowsInserted]), pl.[RowsUpdated] = ISNULL(@RowsSelected, pl.[RowsUpdated]), pl.[RowsDeleted] = ISNULL(@RowsSelected, pl.[RowsDeleted]) FROM [Logging].[ProcessLog] pl WHERE pl.[DatabaseID] = @DatabaseID AND pl.[ObjectID] = @ObjectID AND pl.[SessionID] = @@SPID AND pl.[StatusID] IN (1, 2); -- make sure to not update closed out entries IF (@@ROWCOUNT = 0) BEGIN RAISERROR('no initial or in-process record!', 16, 1); RETURN; END; GO **Demo stored procedure (input parameters are formatted as XML)** CREATE PROCEDURE [dbo].[ProcessLogDemo] ( @Param1 INT, @Param2 DATETIME, @Param3 NVARCHAR(50) = NULL ) AS SET NOCOUNT ON; DECLARE @DB_ID INT = DB_ID(), @Params XML; SET @Params = ( SELECT @Param1 AS [Param1], @Param2 AS [Param2], @Param3 AS [Param3] FOR XML PATH(N'Params') ); -- missing elements mean the value == NULL --SELECT @Params; EXEC [Logging].[ProcessLog_Start] @DatabaseID = @DB_ID, @ObjectID = @@PROCID, @Params = @Params; -- do something EXEC [Logging].[ProcessLog_Log] @DatabaseID = @DB_ID, @ObjectID = @@PROCID, @Step = 1, @RowsSelected = @@ROWCOUNT; -- do something else EXEC [Logging].[ProcessLog_Log] @DatabaseID = @DB_ID, @ObjectID = @@PROCID, @Step = 2, @RowsUpdated = @@ROWCOUNT; -- do final thingy EXEC [Logging].[ProcessLog_Stop] @DatabaseID = @DB_ID, @ObjectID = @@PROCID, @Step = 3, @StatusID = 3, -- success @RowsInserted = @@ROWCOUNT; GO NOTES: * I will try to incorporate the "initiating Login" for SQL Agent jobs tomorrow. * I realized in writing the "Demo" stored procedure that there is an even easier way of tracking the same Stored Procedure from start to finish in any particular process: create a GUID / `UNIQUEIDENTIFIER` via `NEWID()` at the very top (or it can even be generated in the "Start" stored procedure and passed back via an `OUTPUT` param) and pass that value into all of the "ProcessLog" proc calls. This will be slightly wider than the current combination of columns, but it will be less error-prone since it will be different for each process (`session_id` will not be unique in the history, hence the need to include the other columns in the look-up). A GUID is unique, and so even if the process aborts and never calls the "Stop" stored procedure, no other process will pass in the same "ProcessID". Meaning, no need for the `IF EXISTS` in the "Start" proc. I will add this column and related functionality tomorrow. * I will add the `TRY...CATCH` construct to the "Demo" proc tomorrow. * It might be possible to include the current TransactionID of the process as a means of cleaning up aborted entries since it is possible to query the system to see if that TransactionID is still valid (i.e. a long-running process) or if it is gone (i.e. an aborted process).