不能直接用print或raiserror记审计日志,因其输出不持久化:print仅送客户端,raiserror默认为临时消息,均无法落盘;审计日志必须持久、可查、带完整上下文,故需建专用表并显式insert,字段至少含id、proc_name、login_name(用original_login)、exec_time(datetime2(3))、params_json、affected_rows及error_info,且日志插入须脱离主事务以避免回滚丢失,推荐用sp_getapplock保障并发安全写入。

为什么不能直接用 PRINT 或 RAISERROR 记日志
因为 PRINT 输出只到客户端,不落盘;RAISERROR 默认是临时消息,不写入任何表。审计日志必须持久化、可查、带上下文——所以得自己建表、显式 INSERT,且不能依赖会话级变量(比如 @@SPID 不足以标识用户)。
审计表该包含哪些最小必要字段
别一上来就加 JSON 大字段或冗余列。生产环境最常漏掉的是权限上下文和事务边界支持:
-
id(BIGINT IDENTITY,避免 GUID 影响插入性能) -
proc_name(NVARCHAR(128),存OBJECT_NAME(@@PROCID),不是硬编码字符串) -
login_name(用ORIGINAL_LOGIN(),不是SUSER_SNAME(),后者可能被EXECUTE AS误导) -
exec_time(DATETIME2(3),比GETDATE()更准,且避免时区歧义) -
params_json(NVARCHAR(MAX),用FOR JSON PATH序列化输入参数,别拼字符串) -
affected_rows(INT,记录@@ROWCOUNT,方便事后核对是否真执行了) -
error_info(NVARCHAR(4000),仅在CATCH块里填ERROR_MESSAGE()+ERROR_LINE())
在存储过程中怎么安全插入日志(尤其含事务时)
最大陷阱:把日志 INSERT 写在主事务里 —— 一旦回滚,日志也消失,审计就断档。正确做法是用自治事务模拟(SQL Server 没原生自治事务,但可用 sp_getapplock + 独立连接规避):
DECLARE @log_id BIGINT; EXEC sp_getapplock @Resource = 'AuditLogInsert', @LockMode = 'Exclusive', @LockOwner = 'Session', @LockTimeout = 5000; BEGIN TRY INSERT INTO dbo.AuditLog (proc_name, login_name, exec_time, params_json, affected_rows) VALUES (OBJECT_NAME(@@PROCID), ORIGINAL_LOGIN(), SYSDATETIME(), @json_params, @@ROWCOUNT); SELECT @log_id = SCOPE_IDENTITY(); END TRY BEGIN CATCH -- 这里不 ROLLBACK,因为不在主事务中 END CATCH EXEC sp_releaseapplock @Resource = 'AuditLogInsert', @LockOwner = 'Session';
注意:sp_getapplock 是为了防并发写冲突,不是为事务隔离——日志表本身必须用 WITH (TABLOCKX) 或单独文件组保证写入原子性。
怎么自动捕获参数而不手动拼 JSON
别在每个存储过程里手写 '{"id":' + CAST(@id AS NVARCHAR) + ', ...}'。SQL Server 2016+ 支持动态构造:
DECLARE @json_params NVARCHAR(MAX) = ( SELECT @id AS id, @name AS name, @status AS status FOR JSON PATH, WITHOUT_ARRAY_WRAPPER );
如果参数太多或类型复杂(比如 XML、GEOMETRY),先转成字符串再进 JSON,否则 FOR JSON 会报错;另外,空值默认被忽略,如需保留,加 INCLUDE_NULL_VALUES。
真正难的是识别“敏感参数”——比如密码、身份证号,得在日志前做脱敏,不能靠事后过滤。










