如何手动测量记录SQL Server存储过程执行时间(不使用SQL Profiler)
方案可行性判断
你提出的封装SqlConnection/SqlCommand的方案完全可行,属于低侵入性埋点,非常适合遗留系统的改造需求。需要注意的是.NET Framework 2.0不支持C# 3.0才引入的扩展方法,因此你构思的扩展方法实现需要调整为包装类实现。
包装类实现示例
1. 埋点SqlCommand封装
// 自定义用户信息类,存储你需要的用户、部门字段 public class UserInfo { public string UserId { get; set; } public string DepartmentId { get; set; } public string UserName { get; set; } } public class TracedSqlCommand : IDisposable { private readonly SqlCommand _innerCommand; private readonly UserInfo _currentUser; public TracedSqlCommand(string commandText, SqlConnection connection, UserInfo currentUser) { _innerCommand = new SqlCommand(commandText, connection); _currentUser = currentUser; // 默认指定是存储过程类型,可根据你的业务调整 _innerCommand.CommandType = CommandType.StoredProcedure; } // 封装原有ExecuteNonQuery方法,注入计时逻辑 public int ExecuteNonQuery() { Stopwatch stopwatch = Stopwatch.StartNew(); try { return _innerCommand.ExecuteNonQuery(); } finally { stopwatch.Stop(); // 上报埋点数据 TraceReporter.Submit(_innerCommand.CommandText, stopwatch.ElapsedMilliseconds, _currentUser); } } // 按同样逻辑封装其他执行方法 public SqlDataReader ExecuteReader() { Stopwatch stopwatch = Stopwatch.StartNew(); try { return _innerCommand.ExecuteReader(); } finally { stopwatch.Stop(); TraceReporter.Submit(_innerCommand.CommandText, stopwatch.ElapsedMilliseconds, _currentUser); } } public object ExecuteScalar() { Stopwatch stopwatch = Stopwatch.StartNew(); try { return _innerCommand.ExecuteScalar(); } finally { stopwatch.Stop(); TraceReporter.Submit(_innerCommand.CommandText, stopwatch.ElapsedMilliseconds, _currentUser); } } // 封装Parameters属性,原有给参数赋值的代码无需修改 public SqlParameterCollection Parameters => _innerCommand.Parameters; public void Dispose() { _innerCommand?.Dispose(); } }
2. 埋点上报工具类
public static class TraceReporter { public static void Submit(string storedProcedureName, long elapsedMs, UserInfo user) { string logLine = string.Format("{0}|{1}|{2}|{3}|{4}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), storedProcedureName, elapsedMs, user.DepartmentId, user.UserId); // 可根据需求调整为写本地日志/异步上报到监控看板后端/写入专用监控表 File.AppendAllText(@"C:\LDA_Monitor\trace.log", logLine + Environment.NewLine); } }
3. 原有代码改造量
仅需要把原有new SqlCommand的逻辑替换为new TracedSqlCommand即可,所有业务逻辑、参数赋值代码完全不需要调整,侵入性极低:
using (SqlConnection conn = new SqlConnection(ConnString)) { conn.Open(); // 仅需要修改这一行初始化代码 using (TracedSqlCommand cmd = new TracedSqlCommand("SP_XXX_Operate", conn, currentUserObject)) { cmd.Parameters.AddWithValue("@Id", 123); cmd.ExecuteNonQuery(); } }
WinForm层面全局埋点方案
如果不想修改任何业务代码,还可以通过全局事件挂钩的方式实现全量按钮点击埋点,可以和上面的SQL埋点联动,定位到具体是哪个表单的哪个按钮触发的慢查询,排查效率更高:
// Program.cs入口修改 static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // 注册全局消息过滤器,捕获所有Form加载事件 Application.AddMessageFilter(new ButtonTraceFilter()); Application.Run(new MainForm()); } // 全局消息过滤器 public class ButtonTraceFilter : IMessageFilter { private const int WM_SHOWWINDOW = 0x0018; public bool PreFilterMessage(ref Message m) { // 捕获窗口显示事件 if (m.Msg == WM_SHOWWINDOW && m.WParam.ToInt32() == 1) { Control control = Control.FromHandle(m.HWnd); if (control is Form form) { AttachButtonTrace(form); } } return false; } // 递归给当前Form下所有按钮绑定统一点击埋点 private void AttachButtonTrace(Control parent) { foreach (Control ctl in parent.Controls) { if (ctl is Button btn) { btn.Click += (sender, e) => { // 把当前点击的表单、按钮信息存入线程上下文,SQL埋点可以直接读取 TraceContext.CurrentFormName = parent.FindForm()?.Name; TraceContext.CurrentButtonText = btn.Text; TraceContext.TraceId = Guid.NewGuid().ToString(); }; } // 递归遍历容器内的子控件 if (ctl.Controls.Count > 0) { AttachButtonTrace(ctl); } } } } // 线程静态上下文,保证多线程下数据隔离 public static class TraceContext { [ThreadStatic] public static string TraceId; [ThreadStatic] public static string CurrentFormName; [ThreadStatic] public static string CurrentButtonText; }
该方案完全不需要修改原有业务代码,仅需要在应用入口增加几十行代码即可实现全量按钮点击埋点,和SQL层埋点结合后可以实现全链路耗时统计。
内容的提问来源于stack exchange,提问作者Achilles_VN
相关产品推荐
相关产品推荐

