feat(go.d.plugin/mssql): improve deadlock-info and error-info functions (#21652)
Ilya Mashchenko committed
Jan 27, 2026 at 22:27 UTC
afd66a9952a783b2cb518a84d110020e2f772aad
9 files changed
+653
-490
src/go/plugin/go.d/collector/mssql/collector.go
+32
@@ -80,6 +80,14 @@ type Config struct {
80
// Default: true
81
DeadlockInfoFunctionEnabled *bool `yaml:"deadlock_info_function_enabled,omitempty" json:"deadlock_info_function_enabled"`
82
83
+ // DeadlockInfoUseRingBuffer uses ring_buffer target instead of event_file for deadlock-info
84
+ // Uses pointer to distinguish "unset" from explicit "true":
85
+ // - nil (unset): Apply default of false (use event_file)
86
+ // - false: Use event_file target (faster, recommended for on-prem)
87
+ // - true: Use ring_buffer target (required for Azure SQL DB without blob storage)
88
+ // Default: false
89
+ DeadlockInfoUseRingBuffer *bool `yaml:"deadlock_info_use_ring_buffer,omitempty" json:"deadlock_info_use_ring_buffer"`
90
+
91
// ErrorInfoFunctionEnabled controls whether the error-info function is available
92
// Uses pointer to distinguish "unset" from explicit "false":
93
// - nil (unset): Apply default of true (enabled)
@@ -92,6 +100,14 @@ type Config struct {
100
// Default: "netdata_errors"
101
ErrorInfoSessionName string `yaml:"error_info_session_name,omitempty" json:"error_info_session_name,omitempty"`
102
103
+ // ErrorInfoUseRingBuffer uses ring_buffer target instead of event_file for error-info
104
+ // Uses pointer to distinguish "unset" from explicit "true":
105
+ // - nil (unset): Apply default of false (use event_file)
106
+ // - false: Use event_file target (faster, recommended for on-prem)
107
+ // - true: Use ring_buffer target (required for Azure SQL DB without blob storage)
108
+ // Default: false
109
+ ErrorInfoUseRingBuffer *bool `yaml:"error_info_use_ring_buffer,omitempty" json:"error_info_use_ring_buffer"`
110
+
111
// TopQueriesLimit is the maximum number of queries to return
112
TopQueriesLimit int `yaml:"top_queries_limit,omitempty" json:"top_queries_limit,omitempty"`
113
}
@@ -120,6 +136,14 @@ func (c *Config) GetDeadlockInfoFunctionEnabled() bool {
136
return *c.DeadlockInfoFunctionEnabled
137
}
138
139
+// GetDeadlockInfoUseRingBuffer returns whether to use ring_buffer target instead of event_file (default: false)
140
+func (c *Config) GetDeadlockInfoUseRingBuffer() bool {
141
+ if c.DeadlockInfoUseRingBuffer == nil {
142
+ return false
143
+ }
144
+ return *c.DeadlockInfoUseRingBuffer
145
+}
146
+
147
// GetErrorInfoFunctionEnabled returns whether the error-info function is enabled (default: true)
148
func (c *Config) GetErrorInfoFunctionEnabled() bool {
149
if c.ErrorInfoFunctionEnabled == nil {
@@ -136,6 +160,14 @@ func (c *Config) GetErrorInfoSessionName() string {
160
return c.ErrorInfoSessionName
161
}
162
163
+// GetErrorInfoUseRingBuffer returns whether to use ring_buffer target instead of event_file (default: false)
164
+func (c *Config) GetErrorInfoUseRingBuffer() bool {
165
+ if c.ErrorInfoUseRingBuffer == nil {
166
+ return false
167
+ }
168
+ return *c.ErrorInfoUseRingBuffer
169
+}
170
+
171
type Collector struct {
172
module.Base
173
Config `yaml:",inline" json:""`
src/go/plugin/go.d/collector/mssql/config_schema.json
+20
-2
@@ -57,6 +57,12 @@
57
"type": "boolean",
58
"default": true
59
},
60
+ "deadlock_info_use_ring_buffer": {
61
+ "title": "Use Ring Buffer for Deadlock Info",
62
+ "description": "Use ring_buffer target instead of event_file for system_health session. Enable for Azure SQL Database without blob storage. Note: ring_buffer can be slower with large buffers.",
63
+ "type": "boolean",
64
+ "default": false
65
+ },
66
"error_info_function_enabled": {
67
"title": "Enable Error Info Function",
68
"description": "Enable the error-info function. WARNING: error messages and query text may contain unmasked sensitive literals (PII). This function reads from a user-managed Extended Events session and requires VIEW SERVER STATE.",
@@ -65,9 +71,15 @@
71
},
72
"error_info_session_name": {
73
"title": "Error Info Session Name",
68
- "description": "Name of the Extended Events session that captures error_reported events for error-info. The session must be created by an administrator and include a ring_buffer target.",
74
+ "description": "Name of the Extended Events session that captures error_reported events for error-info. The session must be created by an administrator and include an event_file target.",
75
"type": "string",
76
"default": "netdata_errors"
77
+ },
78
+ "error_info_use_ring_buffer": {
79
+ "title": "Use Ring Buffer for Error Info",
80
+ "description": "Use ring_buffer target instead of event_file. Enable for Azure SQL Database without blob storage. Note: ring_buffer can be slower with large buffers.",
81
+ "type": "boolean",
82
+ "default": false
83
}
84
},
85
"required": [
@@ -100,11 +112,17 @@
112
"deadlock_info_function_enabled": {
113
"ui:help": "When enabled, the deadlock-info function becomes available in the Netdata dashboard. WARNING: query text may contain unmasked sensitive literals and requires VIEW SERVER STATE permission."
114
},
115
+ "deadlock_info_use_ring_buffer": {
116
+ "ui:help": "Enable for Azure SQL Database or environments without event_file access. Ring buffer can be slower with large buffers."
117
+ },
118
"error_info_function_enabled": {
119
"ui:help": "When enabled, the error-info function becomes available in the Netdata dashboard. WARNING: error messages and query text may include sensitive literals."
120
},
121
"error_info_session_name": {
107
- "ui:help": "The Extended Events session must be created by an administrator and include a ring_buffer target capturing sqlserver.error_reported with sql_text action."
122
+ "ui:help": "The Extended Events session must be created by an administrator and include an event_file target capturing sqlserver.error_reported with sql_text action."
123
+ },
124
+ "error_info_use_ring_buffer": {
125
+ "ui:help": "Enable for Azure SQL Database or environments without event_file access. Ring buffer can be slower with large buffers."
126
},
127
"ui:flavour": "tabs",
128
"ui:options": {
src/go/plugin/go.d/collector/mssql/func_deadlock_info.go
renamed
+337
-343
@@ -20,12 +20,208 @@ import (
20
)
21
22
const (
23
- deadlockInfoHelp = "Latest deadlock from the system_health Extended Events ring buffer. WARNING: query text may include unmasked sensitive literals; restrict dashboard access."
23
+ deadlockInfoHelp = "Latest deadlock from the system_health Extended Events session. WARNING: query text may include unmasked sensitive literals; restrict dashboard access."
24
deadlockParseErrorStatus = 561
25
)
26
27
const deadlockInfoMethodID = "deadlock-info"
28
29
+// deadlockRowData holds computed values for a single deadlock row.
30
+type deadlockRowData struct {
31
+ rowID string
32
+ deadlockID string
33
+ timestamp string
34
+ processID string
35
+ spid any
36
+ ecid any
37
+ isVictim string
38
+ queryText string
39
+ lockMode string
40
+ lockStatus string
41
+ waitResource string
42
+ database any
43
+}
44
+
45
+// deadlockColumn defines a column for the deadlock-info function.
46
+type deadlockColumn struct {
47
+ funcapi.ColumnMeta
48
+ Value func(*deadlockRowData) any
49
+}
50
+
51
+func deadlockColumnSet(cols []deadlockColumn) funcapi.ColumnSet[deadlockColumn] {
52
+ return funcapi.Columns(cols, func(c deadlockColumn) funcapi.ColumnMeta { return c.ColumnMeta })
53
+}
54
+
55
+var deadlockColumns = []deadlockColumn{
56
+ {
57
+ ColumnMeta: funcapi.ColumnMeta{
58
+ Name: "row_id",
59
+ Tooltip: "Unique identifier for this row",
60
+ Type: funcapi.FieldTypeString,
61
+ Sort: funcapi.FieldSortAscending,
62
+ Sortable: true,
63
+ Summary: funcapi.FieldSummaryCount,
64
+ Filter: funcapi.FieldFilterMultiselect,
65
+ UniqueKey: true,
66
+ Visible: false,
67
+ },
68
+ Value: func(r *deadlockRowData) any { return r.rowID },
69
+ },
70
+ {
71
+ ColumnMeta: funcapi.ColumnMeta{
72
+ Name: "timestamp",
73
+ Tooltip: "When the deadlock occurred",
74
+ Type: funcapi.FieldTypeTimestamp,
75
+ Sort: funcapi.FieldSortDescending,
76
+ Sortable: true,
77
+ Summary: funcapi.FieldSummaryMax,
78
+ Filter: funcapi.FieldFilterRange,
79
+ Visible: true,
80
+ Transform: funcapi.FieldTransformDatetime,
81
+ },
82
+ Value: func(r *deadlockRowData) any { return r.timestamp },
83
+ },
84
+ {
85
+ ColumnMeta: funcapi.ColumnMeta{
86
+ Name: "is_victim",
87
+ Tooltip: "Whether this process was rolled back to resolve the deadlock",
88
+ Type: funcapi.FieldTypeString,
89
+ Visualization: funcapi.FieldVisualPill,
90
+ Sort: funcapi.FieldSortAscending,
91
+ Sortable: true,
92
+ Summary: funcapi.FieldSummaryCount,
93
+ Filter: funcapi.FieldFilterMultiselect,
94
+ Visible: true,
95
+ },
96
+ Value: func(r *deadlockRowData) any { return r.isVictim },
97
+ },
98
+ {
99
+ ColumnMeta: funcapi.ColumnMeta{
100
+ Name: "query_text",
101
+ Tooltip: "The SQL statement being executed",
102
+ Type: funcapi.FieldTypeString,
103
+ Sort: funcapi.FieldSortAscending,
104
+ Sortable: false,
105
+ Sticky: true,
106
+ Summary: funcapi.FieldSummaryCount,
107
+ Filter: funcapi.FieldFilterMultiselect,
108
+ FullWidth: true,
109
+ Wrap: true,
110
+ Visible: true,
111
+ },
112
+ Value: func(r *deadlockRowData) any { return r.queryText },
113
+ },
114
+ {
115
+ ColumnMeta: funcapi.ColumnMeta{
116
+ Name: "database",
117
+ Tooltip: "Database where the deadlock occurred",
118
+ Type: funcapi.FieldTypeString,
119
+ Sort: funcapi.FieldSortAscending,
120
+ Sortable: true,
121
+ Summary: funcapi.FieldSummaryCount,
122
+ Filter: funcapi.FieldFilterMultiselect,
123
+ Visible: true,
124
+ },
125
+ Value: func(r *deadlockRowData) any { return r.database },
126
+ },
127
+ {
128
+ ColumnMeta: funcapi.ColumnMeta{
129
+ Name: "lock_mode",
130
+ Tooltip: "Type of lock (S=Shared, X=Exclusive, U=Update, etc.)",
131
+ Type: funcapi.FieldTypeString,
132
+ Sort: funcapi.FieldSortAscending,
133
+ Sortable: true,
134
+ Summary: funcapi.FieldSummaryCount,
135
+ Filter: funcapi.FieldFilterMultiselect,
136
+ Visible: true,
137
+ },
138
+ Value: func(r *deadlockRowData) any { return r.lockMode },
139
+ },
140
+ {
141
+ ColumnMeta: funcapi.ColumnMeta{
142
+ Name: "lock_status",
143
+ Tooltip: "Whether the lock was granted or still waiting",
144
+ Type: funcapi.FieldTypeString,
145
+ Visualization: funcapi.FieldVisualPill,
146
+ Sort: funcapi.FieldSortAscending,
147
+ Sortable: true,
148
+ Summary: funcapi.FieldSummaryCount,
149
+ Filter: funcapi.FieldFilterMultiselect,
150
+ Visible: true,
151
+ },
152
+ Value: func(r *deadlockRowData) any { return r.lockStatus },
153
+ },
154
+ {
155
+ ColumnMeta: funcapi.ColumnMeta{
156
+ Name: "wait_resource",
157
+ Tooltip: "The resource this process was waiting to acquire",
158
+ Type: funcapi.FieldTypeString,
159
+ Sort: funcapi.FieldSortAscending,
160
+ Sortable: false,
161
+ Summary: funcapi.FieldSummaryCount,
162
+ Filter: funcapi.FieldFilterMultiselect,
163
+ FullWidth: true,
164
+ Wrap: true,
165
+ Visible: true,
166
+ },
167
+ Value: func(r *deadlockRowData) any { return r.waitResource },
168
+ },
169
+ {
170
+ ColumnMeta: funcapi.ColumnMeta{
171
+ Name: "spid",
172
+ Tooltip: "Server Process ID (SQL Server session ID)",
173
+ Type: funcapi.FieldTypeInteger,
174
+ Sort: funcapi.FieldSortAscending,
175
+ Sortable: true,
176
+ Summary: funcapi.FieldSummaryCount,
177
+ Filter: funcapi.FieldFilterRange,
178
+ Visible: true,
179
+ Transform: funcapi.FieldTransformNumber,
180
+ },
181
+ Value: func(r *deadlockRowData) any { return r.spid },
182
+ },
183
+ {
184
+ ColumnMeta: funcapi.ColumnMeta{
185
+ Name: "ecid",
186
+ Tooltip: "Execution Context ID for parallel query threads",
187
+ Type: funcapi.FieldTypeInteger,
188
+ Sort: funcapi.FieldSortAscending,
189
+ Sortable: true,
190
+ Summary: funcapi.FieldSummaryCount,
191
+ Filter: funcapi.FieldFilterRange,
192
+ Visible: true,
193
+ Transform: funcapi.FieldTransformNumber,
194
+ },
195
+ Value: func(r *deadlockRowData) any { return r.ecid },
196
+ },
197
+ {
198
+ ColumnMeta: funcapi.ColumnMeta{
199
+ Name: "process_id",
200
+ Tooltip: "Internal process identifier from the deadlock graph",
201
+ Type: funcapi.FieldTypeString,
202
+ Sort: funcapi.FieldSortAscending,
203
+ Sortable: true,
204
+ Summary: funcapi.FieldSummaryCount,
205
+ Filter: funcapi.FieldFilterMultiselect,
206
+ Visible: true,
207
+ },
208
+ Value: func(r *deadlockRowData) any { return r.processID },
209
+ },
210
+ {
211
+ ColumnMeta: funcapi.ColumnMeta{
212
+ Name: "deadlock_id",
213
+ Tooltip: "Unique identifier for this deadlock event",
214
+ Type: funcapi.FieldTypeString,
215
+ Sort: funcapi.FieldSortAscending,
216
+ Sortable: true,
217
+ Summary: funcapi.FieldSummaryCount,
218
+ Filter: funcapi.FieldFilterMultiselect,
219
+ Visible: true,
220
+ },
221
+ Value: func(r *deadlockRowData) any { return r.deadlockID },
222
+ },
223
+}
224
+
225
func deadlockInfoMethodConfig() funcapi.MethodConfig {
226
return funcapi.MethodConfig{
227
ID: deadlockInfoMethodID,
@@ -64,108 +260,13 @@ func (f *funcDeadlockInfo) Handle(ctx context.Context, method string, params fun
260
}
261
f.router.collector.db = db
262
}
67
- return f.router.collector.collectDeadlockInfo(ctx)
263
+ return f.collectData(ctx)
264
}
265
266
func (f *funcDeadlockInfo) Cleanup(ctx context.Context) {}
267
72
-const (
73
- deadlockIdxRowID = iota
74
- deadlockIdxDeadlockID
75
- deadlockIdxTimestamp
76
- deadlockIdxProcessID
77
- deadlockIdxSpid
78
- deadlockIdxEcid
79
- deadlockIdxIsVictim
80
- deadlockIdxQueryText
81
- deadlockIdxLockMode
82
- deadlockIdxLockStatus
83
- deadlockIdxWaitResource
84
- deadlockIdxDatabase
85
- deadlockColumnCount
86
-)
87
-
88
-type mssqlDeadlockTxn struct {
89
- processID string
90
- spid string
91
- ecid string
92
- dbid string
93
- queryText string
94
- lockMode string
95
- lockStatus string
96
- waitResource string
97
-}
98
-
99
-type mssqlDeadlockParseResult struct {
100
- deadlockTime time.Time
101
- transactions []*mssqlDeadlockTxn
102
- victimProcessID string
103
- parseErr error
104
- found bool
105
-}
106
-
107
-type mssqlDeadlockGraph struct {
108
- XMLName xml.Name `xml:"deadlock"`
109
- VictimList mssqlDeadlockVictimList `xml:"victim-list"`
110
- ProcessList mssqlDeadlockProcessList `xml:"process-list"`
111
- ResourceList mssqlDeadlockResourceList `xml:"resource-list"`
112
-}
113
-
114
-type mssqlDeadlockResourceList struct {
115
- Resources []mssqlDeadlockResource `xml:",any"`
116
-}
117
-
118
-type mssqlDeadlockVictimList struct {
119
- Victims []mssqlDeadlockVictim `xml:"victimProcess"`
120
-}
121
-
122
-type mssqlDeadlockVictim struct {
123
- ID string `xml:"id,attr"`
124
-}
125
-
126
-type mssqlDeadlockProcessList struct {
127
- Processes []mssqlDeadlockProcess `xml:"process"`
128
-}
129
-
130
-type mssqlDeadlockProcess struct {
131
- ID string `xml:"id,attr"`
132
- SPID string `xml:"spid,attr"`
133
- ECID string `xml:"ecid,attr"`
134
- DBID string `xml:"dbid,attr"`
135
- LockMode string `xml:"lockMode,attr"`
136
- WaitResource string `xml:"waitresource,attr"`
137
- InputBuf string `xml:"inputbuf"`
138
-}
139
-
140
-type mssqlDeadlockResource struct {
141
- XMLName xml.Name
142
- DBID string `xml:"dbid,attr"`
143
- OwnerList mssqlDeadlockOwnerList `xml:"owner-list"`
144
- WaiterList mssqlDeadlockWaiterList `xml:"waiter-list"`
145
-}
146
-
147
-type mssqlDeadlockOwnerList struct {
148
- Owners []mssqlDeadlockResourceEntry `xml:"owner"`
149
-}
150
-
151
-type mssqlDeadlockWaiterList struct {
152
- Waiters []mssqlDeadlockResourceEntry `xml:"waiter"`
153
-}
154
-
155
-type mssqlDeadlockResourceEntry struct {
156
- ID string `xml:"id,attr"`
157
- Mode string `xml:"mode,attr"`
158
-}
159
-
160
-func (c *Collector) deadlockInfoParams(context.Context) ([]funcapi.ParamConfig, error) {
161
- if !c.Config.GetDeadlockInfoFunctionEnabled() {
162
- return nil, fmt.Errorf("deadlock-info function disabled in configuration")
163
- }
164
- return []funcapi.ParamConfig{}, nil
165
-}
166
-
167
-func (c *Collector) collectDeadlockInfo(ctx context.Context) *funcapi.FunctionResponse {
168
- if !c.Config.GetDeadlockInfoFunctionEnabled() {
268
+func (f *funcDeadlockInfo) collectData(ctx context.Context) *funcapi.FunctionResponse {
269
+ if !f.router.collector.Config.GetDeadlockInfoFunctionEnabled() {
270
return &funcapi.FunctionResponse{
271
Status: 503,
272
Message: "deadlock-info function has been disabled in configuration. " +
@@ -173,67 +274,82 @@ func (c *Collector) collectDeadlockInfo(ctx context.Context) *funcapi.FunctionRe
274
}
275
}
276
176
- deadlockTime, deadlockXML, err := c.queryLatestDeadlock(ctx)
277
+ deadlockTime, deadlockXML, err := f.queryLatestDeadlock(ctx)
278
if err != nil {
279
if errors.Is(err, context.DeadlineExceeded) {
179
- return c.deadlockInfoResponse(504, "deadlock query timed out", nil)
280
+ return f.buildResponse(504, "deadlock query timed out", nil)
281
}
282
if isDeadlockPermissionError(err) {
182
- return c.deadlockInfoResponse(403, deadlockPermissionMessage(), nil)
283
+ return f.buildResponse(403, deadlockPermissionMessage(), nil)
284
}
184
- c.Warningf("deadlock-info: query failed: %v", err)
185
- return c.deadlockInfoResponse(500, fmt.Sprintf("deadlock query failed: %v", err), nil)
285
+ f.router.collector.Warningf("deadlock-info: query failed: %v", err)
286
+ return f.buildResponse(500, fmt.Sprintf("deadlock query failed: %v", err), nil)
287
}
288
289
if deadlockXML == "" {
189
- return c.deadlockInfoResponse(200, "no deadlock found in system_health ring buffer", nil)
290
+ return f.buildResponse(200, "no deadlock found in system_health Extended Events", nil)
291
}
292
192
- dbNames, dbErr := c.queryDatabaseNames(ctx)
293
+ dbNames, dbErr := f.queryDatabaseNames(ctx)
294
if dbErr != nil {
194
- c.Debugf("deadlock-info: database name mapping failed: %v", dbErr)
295
+ f.router.collector.Debugf("deadlock-info: database name mapping failed: %v", dbErr)
296
dbNames = map[int]string{}
297
}
298
299
parseRes := parseDeadlockGraph(deadlockXML, deadlockTime)
300
if parseRes.parseErr != nil {
200
- c.Warningf("deadlock-info: parse failed: %v", parseRes.parseErr)
201
- return c.deadlockInfoResponse(deadlockParseErrorStatus, "deadlock graph could not be parsed", nil)
301
+ f.router.collector.Warningf("deadlock-info: parse failed: %v", parseRes.parseErr)
302
+ return f.buildResponse(deadlockParseErrorStatus, "deadlock graph could not be parsed", nil)
303
}
304
+
305
if !parseRes.found {
204
- return c.deadlockInfoResponse(200, "no deadlock found in system_health ring buffer", nil)
306
+ return f.buildResponse(200, "no deadlock found in system_health Extended Events", nil)
307
}
308
309
deadlockID := generateDeadlockID(parseRes.deadlockTime)
310
rows := buildDeadlockRows(parseRes, deadlockID, dbNames)
311
+
312
if len(rows) == 0 {
210
- return c.deadlockInfoResponse(200, "deadlock detected but no processes could be parsed", nil)
313
+ return f.buildResponse(200, "deadlock detected but no processes could be parsed", nil)
314
}
315
213
- return c.deadlockInfoResponse(200, "latest detected deadlock", rows)
316
+ return f.buildResponse(200, "latest detected deadlock", rows)
317
}
318
216
-func (c *Collector) deadlockInfoResponse(status int, message string, data [][]any) *funcapi.FunctionResponse {
217
- if data == nil {
218
- data = make([][]any, 0)
319
+func (f *funcDeadlockInfo) buildResponse(status int, message string, rowsData []deadlockRowData) *funcapi.FunctionResponse {
320
+ data := make([][]any, 0, len(rowsData))
321
+ for i := range rowsData {
322
+ row := make([]any, len(deadlockColumns))
323
+ for j, col := range deadlockColumns {
324
+ row[j] = col.Value(&rowsData[i])
325
+ }
326
+ data = append(data, row)
327
}
328
+
329
+ cs := deadlockColumnSet(deadlockColumns)
330
+
331
return &funcapi.FunctionResponse{
332
Status: status,
333
Help: deadlockInfoHelp,
334
Message: message,
224
- Columns: c.buildDeadlockColumns(),
335
+ Columns: cs.BuildColumns(),
336
Data: data,
337
DefaultSortColumn: "timestamp",
338
}
339
}
340
230
-func (c *Collector) queryLatestDeadlock(ctx context.Context) (time.Time, string, error) {
231
- qctx, cancel := context.WithTimeout(ctx, c.Timeout.Duration())
341
+func (f *funcDeadlockInfo) queryLatestDeadlock(ctx context.Context) (time.Time, string, error) {
342
+ qctx, cancel := context.WithTimeout(ctx, f.router.collector.Timeout.Duration())
343
defer cancel()
344
345
+ query := querySystemHealthLatestDeadlockEventFile
346
+ if f.router.collector.Config.GetDeadlockInfoUseRingBuffer() {
347
+ query = querySystemHealthLatestDeadlockRingBuffer
348
+ }
349
+
350
var deadlockTime sql.NullTime
351
var deadlockXML sql.NullString
236
- err := c.db.QueryRowContext(qctx, querySystemHealthLatestDeadlock).Scan(&deadlockTime, &deadlockXML)
352
+ err := f.router.collector.db.QueryRowContext(qctx, query).Scan(&deadlockTime, &deadlockXML)
353
if err != nil {
354
if errors.Is(err, sql.ErrNoRows) {
355
return time.Time{}, "", nil
@@ -251,11 +367,11 @@ func (c *Collector) queryLatestDeadlock(ctx context.Context) (time.Time, string,
367
return time.Now().UTC(), deadlockXML.String, nil
368
}
369
254
-func (c *Collector) queryDatabaseNames(ctx context.Context) (map[int]string, error) {
255
- qctx, cancel := context.WithTimeout(ctx, c.Timeout.Duration())
370
+func (f *funcDeadlockInfo) queryDatabaseNames(ctx context.Context) (map[int]string, error) {
371
+ qctx, cancel := context.WithTimeout(ctx, f.router.collector.Timeout.Duration())
372
defer cancel()
373
258
- rows, err := c.db.QueryContext(qctx, queryDatabaseNamesByID)
374
+ rows, err := f.router.collector.db.QueryContext(qctx, queryDatabaseNamesByID)
375
if err != nil {
376
return nil, err
377
}
@@ -276,215 +392,76 @@ func (c *Collector) queryDatabaseNames(ctx context.Context) (map[int]string, err
392
return names, nil
393
}
394
279
-func (c *Collector) buildDeadlockColumns() map[string]any {
280
- const (
281
- ftString = funcapi.FieldTypeString
282
- ftInteger = funcapi.FieldTypeInteger
283
- ftTimestamp = funcapi.FieldTypeTimestamp
395
+type mssqlDeadlockTxn struct {
396
+ processID string
397
+ spid string
398
+ ecid string
399
+ dbid string
400
+ queryText string
401
+ lockMode string
402
+ lockStatus string
403
+ waitResource string
404
+}
405
285
- trNone = funcapi.FieldTransformNone
286
- trNumber = funcapi.FieldTransformNumber
287
- trDatetime = funcapi.FieldTransformDatetime
406
+type mssqlDeadlockParseResult struct {
407
+ deadlockTime time.Time
408
+ transactions []*mssqlDeadlockTxn
409
+ victimProcessID string
410
+ parseErr error
411
+ found bool
412
+}
413
289
- visValue = funcapi.FieldVisualValue
290
- visPill = funcapi.FieldVisualPill
414
+type mssqlDeadlockGraph struct {
415
+ XMLName xml.Name `xml:"deadlock"`
416
+ VictimList mssqlDeadlockVictimList `xml:"victim-list"`
417
+ ProcessList mssqlDeadlockProcessList `xml:"process-list"`
418
+ ResourceList mssqlDeadlockResourceList `xml:"resource-list"`
419
+}
420
292
- sortAsc = funcapi.FieldSortAscending
293
- sortDesc = funcapi.FieldSortDescending
421
+type mssqlDeadlockResourceList struct {
422
+ Resources []mssqlDeadlockResource `xml:",any"`
423
+}
424
295
- summaryCount = funcapi.FieldSummaryCount
296
- summaryMax = funcapi.FieldSummaryMax
425
+type mssqlDeadlockVictimList struct {
426
+ Victims []mssqlDeadlockVictim `xml:"victimProcess"`
427
+}
428
298
- filterMulti = funcapi.FieldFilterMultiselect
299
- filterRange = funcapi.FieldFilterRange
300
- )
429
+type mssqlDeadlockVictim struct {
430
+ ID string `xml:"id,attr"`
431
+}
432
302
- return map[string]any{
303
- "row_id": funcapi.Column{
304
- Index: deadlockIdxRowID,
305
- Name: "Row ID",
306
- Type: ftString,
307
- Visualization: visValue,
308
- Sort: sortAsc,
309
- Sortable: true,
310
- Summary: summaryCount,
311
- Filter: filterMulti,
312
- UniqueKey: true,
313
- Visible: false,
314
- ValueOptions: funcapi.ValueOptions{
315
- Transform: trNone,
316
- },
317
- }.BuildColumn(),
318
- "deadlock_id": funcapi.Column{
319
- Index: deadlockIdxDeadlockID,
320
- Name: "Deadlock ID",
321
- Type: ftString,
322
- Visualization: visValue,
323
- Sort: sortDesc,
324
- Sortable: true,
325
- Sticky: true,
326
- Summary: summaryCount,
327
- Filter: filterMulti,
328
- Visible: true,
329
- ValueOptions: funcapi.ValueOptions{
330
- Transform: trNone,
331
- },
332
- }.BuildColumn(),
333
- "timestamp": funcapi.Column{
334
- Index: deadlockIdxTimestamp,
335
- Name: "Timestamp",
336
- Type: ftTimestamp,
337
- Units: "",
338
- Visualization: visValue,
339
- Sort: sortDesc,
340
- Sortable: true,
341
- Sticky: true,
342
- Summary: summaryMax,
343
- Filter: filterRange,
344
- Visible: true,
345
- ValueOptions: funcapi.ValueOptions{
346
- Transform: trDatetime,
347
- },
348
- }.BuildColumn(),
349
- "process_id": funcapi.Column{
350
- Index: deadlockIdxProcessID,
351
- Name: "Process ID",
352
- Type: ftString,
353
- Visualization: visValue,
354
- Sort: sortAsc,
355
- Sortable: true,
356
- Sticky: true,
357
- Summary: summaryCount,
358
- Filter: filterMulti,
359
- Visible: true,
360
- ValueOptions: funcapi.ValueOptions{
361
- Transform: trNone,
362
- },
363
- }.BuildColumn(),
364
- "spid": funcapi.Column{
365
- Index: deadlockIdxSpid,
366
- Name: "SPID",
367
- Type: ftInteger,
368
- Visualization: visValue,
369
- Sort: sortAsc,
370
- Sortable: true,
371
- Sticky: false,
372
- Summary: summaryCount,
373
- Filter: filterRange,
374
- Visible: true,
375
- ValueOptions: funcapi.ValueOptions{
376
- Transform: trNumber,
377
- },
378
- }.BuildColumn(),
379
- "ecid": funcapi.Column{
380
- Index: deadlockIdxEcid,
381
- Name: "ECID",
382
- Type: ftInteger,
383
- Visualization: visValue,
384
- Sort: sortAsc,
385
- Sortable: true,
386
- Sticky: false,
387
- Summary: summaryCount,
388
- Filter: filterRange,
389
- Visible: true,
390
- ValueOptions: funcapi.ValueOptions{
391
- Transform: trNumber,
392
- },
393
- }.BuildColumn(),
394
- "is_victim": funcapi.Column{
395
- Index: deadlockIdxIsVictim,
396
- Name: "Victim",
397
- Type: ftString,
398
- Visualization: visPill,
399
- Sort: sortAsc,
400
- Sortable: true,
401
- Summary: summaryCount,
402
- Filter: filterMulti,
403
- Visible: true,
404
- ValueOptions: funcapi.ValueOptions{
405
- Transform: trNone,
406
- },
407
- }.BuildColumn(),
408
- "query_text": funcapi.Column{
409
- Index: deadlockIdxQueryText,
410
- Name: "Query",
411
- Type: ftString,
412
- Visualization: visValue,
413
- Sort: sortAsc,
414
- Sortable: false,
415
- Sticky: false,
416
- Summary: summaryCount,
417
- Filter: filterMulti,
418
- FullWidth: true,
419
- Wrap: true,
420
- Visible: true,
421
- ValueOptions: funcapi.ValueOptions{
422
- Transform: trNone,
423
- },
424
- }.BuildColumn(),
425
- "lock_mode": funcapi.Column{
426
- Index: deadlockIdxLockMode,
427
- Name: "Lock Mode",
428
- Type: ftString,
429
- Visualization: visValue,
430
- Sort: sortAsc,
431
- Sortable: true,
432
- Sticky: false,
433
- Summary: summaryCount,
434
- Filter: filterMulti,
435
- Visible: true,
436
- ValueOptions: funcapi.ValueOptions{
437
- Transform: trNone,
438
- },
439
- }.BuildColumn(),
440
- "lock_status": funcapi.Column{
441
- Index: deadlockIdxLockStatus,
442
- Name: "Lock Status",
443
- Type: ftString,
444
- Visualization: visPill,
445
- Sort: sortAsc,
446
- Sortable: true,
447
- Sticky: false,
448
- Summary: summaryCount,
449
- Filter: filterMulti,
450
- Visible: true,
451
- ValueOptions: funcapi.ValueOptions{
452
- Transform: trNone,
453
- },
454
- }.BuildColumn(),
455
- "wait_resource": funcapi.Column{
456
- Index: deadlockIdxWaitResource,
457
- Name: "Wait Resource",
458
- Type: ftString,
459
- Visualization: visValue,
460
- Sort: sortAsc,
461
- Sortable: false,
462
- Sticky: false,
463
- Summary: summaryCount,
464
- Filter: filterMulti,
465
- FullWidth: true,
466
- Wrap: true,
467
- Visible: true,
468
- ValueOptions: funcapi.ValueOptions{
469
- Transform: trNone,
470
- },
471
- }.BuildColumn(),
472
- "database": funcapi.Column{
473
- Index: deadlockIdxDatabase,
474
- Name: "Database",
475
- Type: ftString,
476
- Visualization: visValue,
477
- Sort: sortAsc,
478
- Sortable: true,
479
- Sticky: false,
480
- Summary: summaryCount,
481
- Filter: filterMulti,
482
- Visible: true,
483
- ValueOptions: funcapi.ValueOptions{
484
- Transform: trNone,
485
- },
486
- }.BuildColumn(),
487
- }
433
+type mssqlDeadlockProcessList struct {
434
+ Processes []mssqlDeadlockProcess `xml:"process"`
435
+}
436
+
437
+type mssqlDeadlockProcess struct {
438
+ ID string `xml:"id,attr"`
439
+ SPID string `xml:"spid,attr"`
440
+ ECID string `xml:"ecid,attr"`
441
+ DBID string `xml:"dbid,attr"`
442
+ LockMode string `xml:"lockMode,attr"`
443
+ WaitResource string `xml:"waitresource,attr"`
444
+ InputBuf string `xml:"inputbuf"`
445
+}
446
+
447
+type mssqlDeadlockResource struct {
448
+ XMLName xml.Name
449
+ DBID string `xml:"dbid,attr"`
450
+ OwnerList mssqlDeadlockOwnerList `xml:"owner-list"`
451
+ WaiterList mssqlDeadlockWaiterList `xml:"waiter-list"`
452
+}
453
+
454
+type mssqlDeadlockOwnerList struct {
455
+ Owners []mssqlDeadlockResourceEntry `xml:"owner"`
456
+}
457
+
458
+type mssqlDeadlockWaiterList struct {
459
+ Waiters []mssqlDeadlockResourceEntry `xml:"waiter"`
460
+}
461
+
462
+type mssqlDeadlockResourceEntry struct {
463
+ ID string `xml:"id,attr"`
464
+ Mode string `xml:"mode,attr"`
465
}
466
467
func parseDeadlockGraph(deadlockXML string, deadlockTime time.Time) mssqlDeadlockParseResult {
@@ -612,9 +589,9 @@ func parseDeadlockGraph(deadlockXML string, deadlockTime time.Time) mssqlDeadloc
589
return result
590
}
591
615
-func buildDeadlockRows(parseRes mssqlDeadlockParseResult, deadlockID string, dbNames map[int]string) [][]any {
592
+func buildDeadlockRows(parseRes mssqlDeadlockParseResult, deadlockID string, dbNames map[int]string) []deadlockRowData {
593
timestamp := parseRes.deadlockTime.UTC().Format(time.RFC3339Nano)
617
- rows := make([][]any, 0, len(parseRes.transactions))
594
+ rows := make([]deadlockRowData, 0, len(parseRes.transactions))
595
596
for _, txn := range parseRes.transactions {
597
processID := strings.TrimSpace(txn.processID)
@@ -639,25 +616,24 @@ func buildDeadlockRows(parseRes mssqlDeadlockParseResult, deadlockID string, dbN
616
}
617
618
queryText := strmutil.TruncateText(strings.TrimSpace(txn.queryText), topQueriesMaxTextLength)
642
- lockMode := strings.TrimSpace(txn.lockMode)
619
+ lockMode := formatLockMode(strings.TrimSpace(txn.lockMode))
620
lockStatus := strings.TrimSpace(txn.lockStatus)
621
waitResource := strmutil.TruncateText(strings.TrimSpace(txn.waitResource), topQueriesMaxTextLength)
622
646
- row := make([]any, deadlockColumnCount)
647
- row[deadlockIdxRowID] = fmt.Sprintf("%s:%s", deadlockID, processID)
648
- row[deadlockIdxDeadlockID] = deadlockID
649
- row[deadlockIdxTimestamp] = timestamp
650
- row[deadlockIdxProcessID] = processID
651
- row[deadlockIdxSpid] = spid
652
- row[deadlockIdxEcid] = ecid
653
- row[deadlockIdxIsVictim] = isVictim
654
- row[deadlockIdxQueryText] = queryText
655
- row[deadlockIdxLockMode] = lockMode
656
- row[deadlockIdxLockStatus] = lockStatus
657
- row[deadlockIdxWaitResource] = waitResource
658
- row[deadlockIdxDatabase] = database
659
-
660
- rows = append(rows, row)
623
+ rows = append(rows, deadlockRowData{
624
+ rowID: fmt.Sprintf("%s:%s", deadlockID, processID),
625
+ deadlockID: deadlockID,
626
+ timestamp: timestamp,
627
+ processID: processID,
628
+ spid: spid,
629
+ ecid: ecid,
630
+ isVictim: isVictim,
631
+ queryText: queryText,
632
+ lockMode: lockMode,
633
+ lockStatus: lockStatus,
634
+ waitResource: waitResource,
635
+ database: database,
636
+ })
637
}
638
639
return rows
@@ -715,3 +691,21 @@ func isDeadlockPermissionError(err error) bool {
691
func deadlockPermissionMessage() string {
692
return "deadlock info requires VIEW SERVER STATE permission. Grant with: GRANT VIEW SERVER STATE TO [netdata_user];"
693
}
694
+
695
+func formatLockMode(mode string) string {
696
+ names := map[string]string{
697
+ "X": "Exclusive",
698
+ "S": "Shared",
699
+ "U": "Update",
700
+ "IX": "Intent Exclusive",
701
+ "IS": "Intent Shared",
702
+ "SIX": "Shared Intent Exclusive",
703
+ "Sch-S": "Schema Stability",
704
+ "Sch-M": "Schema Modification",
705
+ "BU": "Bulk Update",
706
+ }
707
+ if name, ok := names[mode]; ok {
708
+ return fmt.Sprintf("%s (%s)", name, mode)
709
+ }
710
+ return mode
711
+}
src/go/plugin/go.d/collector/mssql/func_deadlock_info_test.go
renamed
+28
-17
@@ -15,6 +15,11 @@ import (
15
"github.com/stretchr/testify/require"
16
)
17
18
+func newTestDeadlockHandler(c *Collector) *funcDeadlockInfo {
19
+ r := &funcRouter{collector: c}
20
+ return &funcDeadlockInfo{router: r}
21
+}
22
+
23
func TestConfig_GetDeadlockInfoFunctionEnabled(t *testing.T) {
24
tests := []struct {
25
name string
@@ -150,9 +155,9 @@ func TestParseDeadlockGraph_ThreeWayDeadlock(t *testing.T) {
155
156
victimCount := 0
157
for _, row := range rows {
153
- assert.Equal(t, deadlockID, row[deadlockIdxDeadlockID])
154
- assert.True(t, strings.HasPrefix(row[deadlockIdxRowID].(string), deadlockID+":"))
155
- if row[deadlockIdxIsVictim] == "true" {
158
+ assert.Equal(t, deadlockID, row.deadlockID)
159
+ assert.True(t, strings.HasPrefix(row.rowID, deadlockID+":"))
160
+ if row.isVictim == "true" {
161
victimCount++
162
}
163
}
@@ -197,15 +202,16 @@ func TestCollectDeadlockInfo_ParseError(t *testing.T) {
202
deadlockTime := time.Date(2026, time.January, 25, 12, 34, 56, 0, time.UTC)
203
deadlockRows := sqlmock.NewRows([]string{"deadlock_time", "deadlock_xml"}).
204
AddRow(deadlockTime, "<deadlock><broken>")
200
- mock.ExpectQuery("WITH xevents").WillReturnRows(deadlockRows)
205
+ mock.ExpectQuery("fn_xe_file_target_read_file").WillReturnRows(deadlockRows)
206
207
dbNameRows := sqlmock.NewRows([]string{"database_id", "name"})
208
mock.ExpectQuery("SELECT\\s+database_id").WillReturnRows(dbNameRows)
209
210
c := New()
211
c.db = db
212
+ handler := newTestDeadlockHandler(c)
213
208
- resp := c.collectDeadlockInfo(context.Background())
214
+ resp := handler.collectData(context.Background())
215
require.Equal(t, deadlockParseErrorStatus, resp.Status)
216
assert.Contains(t, strings.ToLower(resp.Message), "could not be parsed")
217
require.NoError(t, mock.ExpectationsWereMet())
@@ -216,13 +222,14 @@ func TestCollectDeadlockInfo_QueryError(t *testing.T) {
222
require.NoError(t, err)
223
defer db.Close()
224
219
- mock.ExpectQuery("WITH xevents").
225
+ mock.ExpectQuery("fn_xe_file_target_read_file").
226
WillReturnError(errors.New("boom"))
227
228
c := New()
229
c.db = db
230
+ handler := newTestDeadlockHandler(c)
231
225
- resp := c.collectDeadlockInfo(context.Background())
232
+ resp := handler.collectData(context.Background())
233
require.Equal(t, 500, resp.Status)
234
assert.Contains(t, strings.ToLower(resp.Message), "deadlock query failed")
235
require.NoError(t, mock.ExpectationsWereMet())
@@ -240,9 +247,9 @@ func TestBuildDeadlockRows(t *testing.T) {
247
require.Len(t, rows, 2)
248
249
row := rows[0]
243
- assert.Equal(t, deadlockID, row[deadlockIdxDeadlockID])
244
- assert.Equal(t, deadlockID+":"+row[deadlockIdxProcessID].(string), row[deadlockIdxRowID])
245
- assert.Equal(t, "netdata", row[deadlockIdxDatabase])
250
+ assert.Equal(t, deadlockID, row.deadlockID)
251
+ assert.Equal(t, deadlockID+":"+row.processID, row.rowID)
252
+ assert.Equal(t, "netdata", row.database)
253
}
254
255
func TestDeadlockPermissionErrorDetection(t *testing.T) {
@@ -255,13 +262,14 @@ func TestCollectDeadlockInfo_PermissionDenied(t *testing.T) {
262
require.NoError(t, err)
263
defer db.Close()
264
258
- mock.ExpectQuery("WITH xevents").
265
+ mock.ExpectQuery("fn_xe_file_target_read_file").
266
WillReturnError(mssqlDriver.Error{Number: 297, Message: "VIEW SERVER STATE permission was denied"})
267
268
c := New()
269
c.db = db
270
+ handler := newTestDeadlockHandler(c)
271
264
- resp := c.collectDeadlockInfo(context.Background())
272
+ resp := handler.collectData(context.Background())
273
require.Equal(t, 403, resp.Status)
274
assert.Contains(t, strings.ToLower(resp.Message), "view server state")
275
require.NoError(t, mock.ExpectationsWereMet())
@@ -270,8 +278,9 @@ func TestCollectDeadlockInfo_PermissionDenied(t *testing.T) {
278
func TestCollectDeadlockInfo_Disabled(t *testing.T) {
279
c := New()
280
c.Config.DeadlockInfoFunctionEnabled = boolPtr(false)
281
+ handler := newTestDeadlockHandler(c)
282
274
- resp := c.collectDeadlockInfo(context.Background())
283
+ resp := handler.collectData(context.Background())
284
require.Equal(t, 503, resp.Status)
285
assert.Contains(t, strings.ToLower(resp.Message), "disabled")
286
}
@@ -281,13 +290,14 @@ func TestCollectDeadlockInfo_Timeout(t *testing.T) {
290
require.NoError(t, err)
291
defer db.Close()
292
284
- mock.ExpectQuery("WITH xevents").
293
+ mock.ExpectQuery("fn_xe_file_target_read_file").
294
WillReturnError(context.DeadlineExceeded)
295
296
c := New()
297
c.db = db
298
+ handler := newTestDeadlockHandler(c)
299
290
- resp := c.collectDeadlockInfo(context.Background())
300
+ resp := handler.collectData(context.Background())
301
require.Equal(t, 504, resp.Status)
302
assert.Contains(t, strings.ToLower(resp.Message), "timed out")
303
require.NoError(t, mock.ExpectationsWereMet())
@@ -300,7 +310,7 @@ func TestCollectDeadlockInfo_Success(t *testing.T) {
310
311
now := time.Date(2026, time.January, 25, 12, 0, 0, 0, time.UTC)
312
303
- mock.ExpectQuery("WITH xevents").
313
+ mock.ExpectQuery("fn_xe_file_target_read_file").
314
WillReturnRows(
315
sqlmock.NewRows([]string{"deadlock_time", "deadlock_xml"}).
316
AddRow(now, sampleDeadlockGraph),
@@ -314,8 +324,9 @@ func TestCollectDeadlockInfo_Success(t *testing.T) {
324
325
c := New()
326
c.db = db
327
+ handler := newTestDeadlockHandler(c)
328
318
- resp := c.collectDeadlockInfo(context.Background())
329
+ resp := handler.collectData(context.Background())
330
require.Equal(t, 200, resp.Status)
331
require.NotEmpty(t, resp.Data)
332
require.NoError(t, mock.ExpectationsWereMet())
src/go/plugin/go.d/collector/mssql/func_error_info.go
renamed
+141
-100
@@ -22,6 +22,110 @@ const (
22
23
const errorInfoMethodID = "error-info"
24
25
+type mssqlErrorRow struct {
26
+ Time time.Time
27
+ ErrorNumber *int64
28
+ ErrorState *int64
29
+ Message string
30
+ Query string
31
+ QueryHash string
32
+}
33
+
34
+type mssqlPlanOps struct {
35
+ HashMatch int64
36
+ MergeJoin int64
37
+ NestedLoops int64
38
+ Sorts int64
39
+}
40
+
41
+// errorInfoColumn defines a column for the error-info function.
42
+type errorInfoColumn struct {
43
+ funcapi.ColumnMeta
44
+ Value func(*mssqlErrorRow) any
45
+}
46
+
47
+func errorInfoColumnSet(cols []errorInfoColumn) funcapi.ColumnSet[errorInfoColumn] {
48
+ return funcapi.Columns(cols, func(c errorInfoColumn) funcapi.ColumnMeta { return c.ColumnMeta })
49
+}
50
+
51
+var errorInfoColumns = []errorInfoColumn{
52
+ {
53
+ ColumnMeta: funcapi.ColumnMeta{
54
+ Name: "timestamp",
55
+ Tooltip: "When the error occurred",
56
+ Type: funcapi.FieldTypeTimestamp,
57
+ Sortable: true,
58
+ Visible: true,
59
+ Transform: funcapi.FieldTransformDatetime,
60
+ },
61
+ Value: func(r *mssqlErrorRow) any { return r.Time },
62
+ },
63
+ {
64
+ ColumnMeta: funcapi.ColumnMeta{
65
+ Name: "errorNumber",
66
+ Tooltip: "SQL Server error number",
67
+ Type: funcapi.FieldTypeInteger,
68
+ Sortable: true,
69
+ Visible: true,
70
+ Transform: funcapi.FieldTransformNumber,
71
+ },
72
+ Value: func(r *mssqlErrorRow) any {
73
+ if r.ErrorNumber == nil {
74
+ return nil
75
+ }
76
+ return *r.ErrorNumber
77
+ },
78
+ },
79
+ {
80
+ ColumnMeta: funcapi.ColumnMeta{
81
+ Name: "errorState",
82
+ Tooltip: "Diagnostic code indicating where the error was raised (1-255)",
83
+ Type: funcapi.FieldTypeInteger,
84
+ Sortable: true,
85
+ Visible: true,
86
+ Transform: funcapi.FieldTransformNumber,
87
+ },
88
+ Value: func(r *mssqlErrorRow) any {
89
+ if r.ErrorState == nil {
90
+ return nil
91
+ }
92
+ return *r.ErrorState
93
+ },
94
+ },
95
+ {
96
+ ColumnMeta: funcapi.ColumnMeta{
97
+ Name: "errorMessage",
98
+ Tooltip: "The error message text",
99
+ Type: funcapi.FieldTypeString,
100
+ Sortable: false,
101
+ FullWidth: true,
102
+ Visible: true,
103
+ },
104
+ Value: func(r *mssqlErrorRow) any { return r.Message },
105
+ },
106
+ {
107
+ ColumnMeta: funcapi.ColumnMeta{
108
+ Name: "query",
109
+ Tooltip: "The SQL statement that caused the error",
110
+ Type: funcapi.FieldTypeString,
111
+ Sortable: false,
112
+ FullWidth: true,
113
+ Visible: true,
114
+ },
115
+ Value: func(r *mssqlErrorRow) any { return r.Query },
116
+ },
117
+ {
118
+ ColumnMeta: funcapi.ColumnMeta{
119
+ Name: "queryHash",
120
+ Tooltip: "Hash of the query for grouping similar statements",
121
+ Type: funcapi.FieldTypeString,
122
+ Sortable: true,
123
+ Visible: false,
124
+ },
125
+ Value: func(r *mssqlErrorRow) any { return r.QueryHash },
126
+ },
127
+}
128
+
129
func errorInfoMethodConfig() funcapi.MethodConfig {
130
return funcapi.MethodConfig{
131
ID: errorInfoMethodID,
@@ -60,36 +164,13 @@ func (f *funcErrorInfo) Handle(ctx context.Context, method string, params funcap
164
}
165
f.router.collector.db = db
166
}
63
- return f.router.collector.collectErrorInfo(ctx)
167
+ return f.collectData(ctx)
168
}
169
170
func (f *funcErrorInfo) Cleanup(ctx context.Context) {}
171
68
-type mssqlErrorRow struct {
69
- Time time.Time
70
- ErrorNumber *int64
71
- ErrorState *int64
72
- Message string
73
- Query string
74
- QueryHash string
75
-}
76
-
77
-type mssqlPlanOps struct {
78
- HashMatch int64
79
- MergeJoin int64
80
- NestedLoops int64
81
- Sorts int64
82
-}
83
-
84
-func (c *Collector) errorInfoParams(context.Context) ([]funcapi.ParamConfig, error) {
85
- if !c.Config.GetErrorInfoFunctionEnabled() {
86
- return nil, fmt.Errorf("error-info function disabled in configuration")
87
- }
88
- return []funcapi.ParamConfig{}, nil
89
-}
90
-
91
-func (c *Collector) collectErrorInfo(ctx context.Context) *funcapi.FunctionResponse {
92
- if !c.Config.GetErrorInfoFunctionEnabled() {
172
+func (f *funcErrorInfo) collectData(ctx context.Context) *funcapi.FunctionResponse {
173
+ if !f.router.collector.Config.GetErrorInfoFunctionEnabled() {
174
return &funcapi.FunctionResponse{
175
Status: 503,
176
Message: "error-info not enabled: function disabled in configuration. " +
@@ -97,98 +178,42 @@ func (c *Collector) collectErrorInfo(ctx context.Context) *funcapi.FunctionRespo
178
}
179
}
180
100
- sessionName := c.Config.GetErrorInfoSessionName()
101
- status, rows, err := c.fetchMSSQLErrorRows(ctx, sessionName, c.TopQueriesLimit)
181
+ sessionName := f.router.collector.Config.GetErrorInfoSessionName()
182
+ status, rows, err := f.router.collector.fetchMSSQLErrorRows(ctx, sessionName, f.router.collector.TopQueriesLimit)
183
if err != nil {
184
if isDeadlockPermissionError(err) {
185
return &funcapi.FunctionResponse{Status: 403, Message: errorInfoPermissionMessage()}
186
}
187
if status == mssqlErrorAttrNotEnabled {
107
- return &funcapi.FunctionResponse{Status: 503, Message: "error-info not enabled: Extended Events session not found or ring_buffer target missing"}
188
+ targetName := "event_file"
189
+ if f.router.collector.Config.GetErrorInfoUseRingBuffer() {
190
+ targetName = "ring_buffer"
191
+ }
192
+ return &funcapi.FunctionResponse{Status: 503, Message: fmt.Sprintf("error-info not enabled: Extended Events session not found or %s target missing", targetName)}
193
}
194
return &funcapi.FunctionResponse{Status: 500, Message: fmt.Sprintf("error-info query failed: %v", err)}
195
}
196
197
data := make([][]any, 0, len(rows))
113
- for _, row := range rows {
114
- var errNo any
115
- var errState any
116
- if row.ErrorNumber != nil {
117
- errNo = *row.ErrorNumber
198
+ for i := range rows {
199
+ row := make([]any, len(errorInfoColumns))
200
+ for j, col := range errorInfoColumns {
201
+ row[j] = col.Value(&rows[i])
202
}
119
- if row.ErrorState != nil {
120
- errState = *row.ErrorState
121
- }
122
- data = append(data, []any{
123
- row.Time,
124
- errNo,
125
- errState,
126
- row.Message,
127
- row.Query,
128
- row.QueryHash,
129
- })
203
+ data = append(data, row)
204
}
205
206
+ cs := errorInfoColumnSet(errorInfoColumns)
207
+
208
return &funcapi.FunctionResponse{
209
Status: 200,
210
Help: "Recent SQL errors from Extended Events error_reported",
135
- Columns: buildMSSQLErrorInfoColumns(),
211
+ Columns: cs.BuildColumns(),
212
Data: data,
213
DefaultSortColumn: "timestamp",
214
}
215
}
216
141
-func buildMSSQLErrorInfoColumns() map[string]any {
142
- columns := map[string]any{
143
- "timestamp": funcapi.Column{
144
- Index: 0,
145
- Name: "Timestamp",
146
- Type: funcapi.FieldTypeTimestamp,
147
- Sortable: true,
148
- ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformDatetime},
149
- }.BuildColumn(),
150
- "errorNumber": funcapi.Column{
151
- Index: 1,
152
- Name: "Error Number",
153
- Type: funcapi.FieldTypeInteger,
154
- Sortable: true,
155
- ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformNumber},
156
- }.BuildColumn(),
157
- "errorState": funcapi.Column{
158
- Index: 2,
159
- Name: "Error State",
160
- Type: funcapi.FieldTypeInteger,
161
- Sortable: true,
162
- ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformNumber},
163
- }.BuildColumn(),
164
- "errorMessage": funcapi.Column{
165
- Index: 3,
166
- Name: "Error Message",
167
- Type: funcapi.FieldTypeString,
168
- Sortable: false,
169
- FullWidth: true,
170
- ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformNone},
171
- }.BuildColumn(),
172
- "query": funcapi.Column{
173
- Index: 4,
174
- Name: "Query",
175
- Type: funcapi.FieldTypeString,
176
- Sortable: false,
177
- FullWidth: true,
178
- ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformNone},
179
- }.BuildColumn(),
180
- "queryHash": funcapi.Column{
181
- Index: 5,
182
- Name: "Query Hash",
183
- Type: funcapi.FieldTypeString,
184
- Sortable: true,
185
- Visible: false,
186
- ValueOptions: funcapi.ValueOptions{Transform: funcapi.FieldTransformNone},
187
- }.BuildColumn(),
188
- }
189
- return columns
190
-}
191
-
217
func errorInfoPermissionMessage() string {
218
return "error-info requires VIEW SERVER STATE permission. Grant with: GRANT VIEW SERVER STATE TO [netdata_user];"
219
}
@@ -198,7 +223,7 @@ func mssqlErrorAttributionColumns() []topQueriesColumn {
223
{
224
ColumnMeta: funcapi.ColumnMeta{
225
Name: "errorAttribution",
201
- Tooltip: "Error Attribution",
226
+ Tooltip: "Source of error data (enabled, not_enabled, no_data)",
227
Type: funcapi.FieldTypeString,
228
Visible: true,
229
Transform: funcapi.FieldTransformNone,
@@ -331,6 +356,12 @@ func nullableString(value string) any {
356
return value
357
}
358
359
+// TODO: Refactor error data access into a shared mssqlErrorData type.
360
+// Currently these methods live on Collector because they're used by both:
361
+// - funcErrorInfo (for error-info function)
362
+// - funcTopQueries (for error attribution columns)
363
+// A cleaner design would be a mssqlErrorData type on funcRouter that both handlers use.
364
+
365
func (c *Collector) collectMSSQLErrorDetails(ctx context.Context) (string, map[string]mssqlErrorRow) {
366
status, rows, err := c.fetchMSSQLErrorRows(ctx, c.Config.GetErrorInfoSessionName(), c.TopQueriesLimit)
367
if err != nil {
@@ -505,7 +536,12 @@ func (c *Collector) fetchMSSQLErrorRows(ctx context.Context, sessionName string,
536
qctx, cancel := context.WithTimeout(ctx, c.Timeout.Duration())
537
defer cancel()
538
508
- rows, err := c.db.QueryContext(qctx, queryMSSQLErrorInfo, sql.Named("sessionName", sessionName), sql.Named("limit", limit))
539
+ query := queryMSSQLErrorInfoEventFile
540
+ if c.Config.GetErrorInfoUseRingBuffer() {
541
+ query = queryMSSQLErrorInfoRingBuffer
542
+ }
543
+
544
+ rows, err := c.db.QueryContext(qctx, query, sql.Named("sessionName", sessionName), sql.Named("limit", limit))
545
if err != nil {
546
return mssqlErrorAttrNotSupported, nil, err
547
}
@@ -563,7 +599,12 @@ func (c *Collector) mssqlErrorSessionAvailable(ctx context.Context, sessionName
599
return false, nil
600
}
601
566
- err = c.db.QueryRowContext(qctx, queryMSSQLErrorSessionHasRingBuffer, sql.Named("sessionName", sessionName)).Scan(&count)
602
+ targetQuery := queryMSSQLErrorSessionHasEventFile
603
+ if c.Config.GetErrorInfoUseRingBuffer() {
604
+ targetQuery = queryMSSQLErrorSessionHasRingBuffer
605
+ }
606
+
607
+ err = c.db.QueryRowContext(qctx, targetQuery, sql.Named("sessionName", sessionName)).Scan(&count)
608
if err != nil {
609
return false, err
610
}
src/go/plugin/go.d/collector/mssql/metadata.yaml
+36
-5
@@ -619,7 +619,7 @@ modules:
619
- id: deadlock-info
620
name: Deadlock Info
621
description: |
622
- Retrieves the most recent deadlock event from SQL Server's `system_health` Extended Events ring buffer (`xml_deadlock_report`).
622
+ Retrieves the most recent deadlock event from SQL Server's `system_health` Extended Events session (`xml_deadlock_report`).
623
624
The deadlock graph XML is parsed to attribute the deadlock to the participating processes and their query text, lock mode, lock status, and wait resource.
625
@@ -645,7 +645,7 @@ modules:
645
- name: Timestamp
646
type: timestamp
647
unit: ""
648
- description: "Timestamp of the deadlock event from the ring buffer when available; otherwise the function execution time."
648
+ description: "Timestamp of the deadlock event from Extended Events when available; otherwise the function execution time."
649
- name: Process ID
650
type: string
651
unit: ""
@@ -695,7 +695,7 @@ modules:
695
Retrieves recent SQL errors from a user-managed Extended Events session that captures `sqlserver.error_reported`
696
with both the `sql_text` and `query_hash` actions.
697
698
- The session must be created by an administrator and include a `ring_buffer` target. Netdata reads the ring buffer
698
+ The session must be created by an administrator and include an `event_file` target. Netdata reads the event file
699
and returns recent error events with error number, message, and SQL text. The `query_hash` action is required for
700
reliable mapping into `top-queries` (query text fallback is best-effort).
701
@@ -733,11 +733,42 @@ modules:
733
visibility: hidden
734
description: "Query hash captured with the error event (used for mapping into top-queries)."
735
performance: |
736
- Executes on-demand queries against the configured Extended Events ring buffer:<br/>• Not part of regular metric collection<br/>• Overhead is limited to function execution time and XML parsing
736
+ Executes on-demand queries against the configured Extended Events event file:<br/>• Not part of regular metric collection<br/>• Overhead is limited to function execution time
737
security: |
738
Error messages and query text may include unmasked literal values including sensitive data (PII/secrets):<br/>• Restrict dashboard access to authorized personnel only
739
+ prerequisites:
740
+ list:
741
+ - title: Create Extended Events session for error capture
742
+ description: |
743
+ Create an Extended Events session that captures `sqlserver.error_reported` with `sql_text` and `query_hash` actions:
744
+
745
+ ```sql
746
+ -- Create the Extended Events session with event_file target
747
+ CREATE EVENT SESSION [netdata_errors] ON SERVER
748
+ ADD EVENT sqlserver.error_reported(
749
+ ACTION(sqlserver.sql_text, sqlserver.query_hash)
750
+ )
751
+ ADD TARGET package0.event_file(SET filename=N'netdata_errors');
752
+ GO
753
+
754
+ -- Start the session
755
+ ALTER EVENT SESSION [netdata_errors] ON SERVER STATE = START;
756
+ GO
757
+
758
+ -- Grant required permission
759
+ GRANT VIEW SERVER STATE TO [netdata_user];
760
+ ```
761
+
762
+ If you use a different session name, set it in the collector config:
763
+
764
+ ```yaml
765
+ jobs:
766
+ - name: local
767
+ dsn: "sqlserver://user:pass@localhost:1433"
768
+ error_info_session_name: your_session_name
769
+ ```
770
availability: |
740
- Available when:<br/>• The collector has successfully connected to SQL Server<br/>• `error_info_function_enabled` is true<br/>• The Extended Events session exists and has a ring_buffer target<br/>• The account has `VIEW SERVER STATE` permission<br/>• Returns HTTP 200 with empty data when no errors are found<br/>• Returns HTTP 403 when permission is missing<br/>• Returns HTTP 500 if the query fails<br/>• Returns HTTP 503 if the session is not enabled or the function is disabled<br/>• Returns HTTP 504 if the query times out
771
+ Available when:<br/>• The collector has successfully connected to SQL Server<br/>• `error_info_function_enabled` is true<br/>• The Extended Events session exists and has an event_file target<br/>• The account has `VIEW SERVER STATE` permission<br/>• Returns HTTP 200 with empty data when no errors are found<br/>• Returns HTTP 403 when permission is missing<br/>• Returns HTTP 500 if the query fails<br/>• Returns HTTP 503 if the session is not enabled or the function is disabled<br/>• Returns HTTP 504 if the query times out
772
require_cloud: true
773
metrics:
774
folding:
src/go/plugin/go.d/collector/mssql/queries.go
+40
-4
@@ -201,9 +201,21 @@ WHERE object_name LIKE '%SQL Errors%'
201
AND counter_name = 'Errors/sec';
202
`
203
204
-// querySystemHealthLatestDeadlock retrieves the latest xml_deadlock_report event
204
+// querySystemHealthLatestDeadlockEventFile retrieves the latest xml_deadlock_report event
205
+// from the system_health Extended Events file target.
206
+const querySystemHealthLatestDeadlockEventFile = `
207
+SELECT TOP (1)
208
+ timestamp_utc AS deadlock_time,
209
+ CONVERT(nvarchar(max), CAST(event_data AS XML).query('(event/data[@name="xml_report"]/value/deadlock)[1]')) AS deadlock_xml
210
+FROM sys.fn_xe_file_target_read_file('system_health*.xel', NULL, NULL, NULL)
211
+WHERE object_name = 'xml_deadlock_report'
212
+ORDER BY timestamp_utc DESC;
213
+`
214
+
215
+// querySystemHealthLatestDeadlockRingBuffer retrieves the latest xml_deadlock_report event
216
// from the system_health Extended Events ring buffer.
206
-const querySystemHealthLatestDeadlock = `
217
+// Note: This can be slow with large buffers due to XML parsing overhead.
218
+const querySystemHealthLatestDeadlockRingBuffer = `
219
WITH xevents AS (
220
SELECT CAST(xet.target_data AS XML) AS target_data
221
FROM sys.dm_xe_session_targets AS xet
@@ -238,6 +250,15 @@ FROM sys.dm_xe_sessions
250
WHERE name = @sessionName;
251
`
252
253
+// queryMSSQLErrorSessionHasEventFile verifies that the session has an event_file target.
254
+const queryMSSQLErrorSessionHasEventFile = `
255
+SELECT COUNT(*)
256
+FROM sys.dm_xe_session_targets AS xet
257
+JOIN sys.dm_xe_sessions AS xs ON xs.address = xet.event_session_address
258
+WHERE xs.name = @sessionName
259
+ AND xet.target_name = 'event_file';
260
+`
261
+
262
// queryMSSQLErrorSessionHasRingBuffer verifies that the session has a ring_buffer target.
263
const queryMSSQLErrorSessionHasRingBuffer = `
264
SELECT COUNT(*)
@@ -247,8 +268,23 @@ WHERE xs.name = @sessionName
268
AND xet.target_name = 'ring_buffer';
269
`
270
250
-// queryMSSQLErrorInfo reads recent error_reported events from the ring_buffer target.
251
-const queryMSSQLErrorInfo = `
271
+// queryMSSQLErrorInfoEventFile reads recent error_reported events from the event_file target.
272
+const queryMSSQLErrorInfoEventFile = `
273
+SELECT TOP (@limit)
274
+ timestamp_utc AS event_time,
275
+ CAST(event_data AS XML).value('(event/data[@name="error_number"]/value)[1]', 'int') AS error_number,
276
+ CAST(event_data AS XML).value('(event/data[@name="state"]/value)[1]', 'int') AS error_state,
277
+ CAST(event_data AS XML).value('(event/data[@name="message"]/value)[1]', 'nvarchar(max)') AS message,
278
+ CAST(event_data AS XML).value('(event/action[@name="sql_text"]/value)[1]', 'nvarchar(max)') AS sql_text,
279
+ CONVERT(VARCHAR(64), CAST(event_data AS XML).value('(event/action[@name="query_hash"]/value)[1]', 'varbinary(8)'), 1) AS query_hash
280
+FROM sys.fn_xe_file_target_read_file(@sessionName + N'*.xel', NULL, NULL, NULL)
281
+WHERE object_name = 'error_reported'
282
+ORDER BY event_time DESC;
283
+`
284
+
285
+// queryMSSQLErrorInfoRingBuffer reads recent error_reported events from the ring_buffer target.
286
+// Note: This can be slow with large buffers due to XML parsing overhead.
287
+const queryMSSQLErrorInfoRingBuffer = `
288
WITH xevents AS (
289
SELECT CAST(xet.target_data AS XML) AS target_data
290
FROM sys.dm_xe_session_targets AS xet
src/go/plugin/go.d/collector/mysql/func_deadlock_info.go
+12
-12
@@ -77,7 +77,7 @@ var deadlockColumns = []deadlockColumn{
77
{
78
ColumnMeta: funcapi.ColumnMeta{
79
Name: "row_id",
80
- Tooltip: "Row ID",
80
+ Tooltip: "Unique identifier for this row",
81
Type: funcapi.FieldTypeString,
82
Sort: funcapi.FieldSortAscending,
83
Sortable: true,
@@ -91,7 +91,7 @@ var deadlockColumns = []deadlockColumn{
91
{
92
ColumnMeta: funcapi.ColumnMeta{
93
Name: "timestamp",
94
- Tooltip: "Timestamp",
94
+ Tooltip: "When the deadlock occurred",
95
Type: funcapi.FieldTypeTimestamp,
96
Sort: funcapi.FieldSortDescending,
97
Sortable: true,
@@ -105,7 +105,7 @@ var deadlockColumns = []deadlockColumn{
105
{
106
ColumnMeta: funcapi.ColumnMeta{
107
Name: "is_victim",
108
- Tooltip: "Victim",
108
+ Tooltip: "Whether this transaction was rolled back to resolve the deadlock",
109
Type: funcapi.FieldTypeString,
110
Visualization: funcapi.FieldVisualPill,
111
Sort: funcapi.FieldSortAscending,
@@ -119,7 +119,7 @@ var deadlockColumns = []deadlockColumn{
119
{
120
ColumnMeta: funcapi.ColumnMeta{
121
Name: "query_text",
122
- Tooltip: "Query",
122
+ Tooltip: "The SQL statement being executed",
123
Type: funcapi.FieldTypeString,
124
Sort: funcapi.FieldSortAscending,
125
Sortable: false,
@@ -135,7 +135,7 @@ var deadlockColumns = []deadlockColumn{
135
{
136
ColumnMeta: funcapi.ColumnMeta{
137
Name: "database",
138
- Tooltip: "Database",
138
+ Tooltip: "Database where the deadlock occurred",
139
Type: funcapi.FieldTypeString,
140
Sort: funcapi.FieldSortAscending,
141
Sortable: true,
@@ -148,7 +148,7 @@ var deadlockColumns = []deadlockColumn{
148
{
149
ColumnMeta: funcapi.ColumnMeta{
150
Name: "lock_mode",
151
- Tooltip: "Lock Mode",
151
+ Tooltip: "Type of lock (S=Shared, X=Exclusive, IS/IX=Intent locks)",
152
Type: funcapi.FieldTypeString,
153
Sort: funcapi.FieldSortAscending,
154
Sortable: true,
@@ -161,7 +161,7 @@ var deadlockColumns = []deadlockColumn{
161
{
162
ColumnMeta: funcapi.ColumnMeta{
163
Name: "lock_status",
164
- Tooltip: "Lock Status",
164
+ Tooltip: "Whether the lock was granted or still waiting",
165
Type: funcapi.FieldTypeString,
166
Visualization: funcapi.FieldVisualPill,
167
Sort: funcapi.FieldSortAscending,
@@ -175,7 +175,7 @@ var deadlockColumns = []deadlockColumn{
175
{
176
ColumnMeta: funcapi.ColumnMeta{
177
Name: "wait_resource",
178
- Tooltip: "Wait Resource",
178
+ Tooltip: "The resource this transaction was waiting to acquire",
179
Type: funcapi.FieldTypeString,
180
Sort: funcapi.FieldSortAscending,
181
Sortable: false,
@@ -188,7 +188,7 @@ var deadlockColumns = []deadlockColumn{
188
{
189
ColumnMeta: funcapi.ColumnMeta{
190
Name: "spid",
191
- Tooltip: "Connection ID",
191
+ Tooltip: "MySQL thread/connection ID",
192
Type: funcapi.FieldTypeInteger,
193
Sort: funcapi.FieldSortAscending,
194
Sortable: true,
@@ -202,7 +202,7 @@ var deadlockColumns = []deadlockColumn{
202
{
203
ColumnMeta: funcapi.ColumnMeta{
204
Name: "process_id",
205
- Tooltip: "Process ID",
205
+ Tooltip: "Transaction identifier from InnoDB",
206
Type: funcapi.FieldTypeString,
207
Sort: funcapi.FieldSortAscending,
208
Sortable: true,
@@ -215,7 +215,7 @@ var deadlockColumns = []deadlockColumn{
215
{
216
ColumnMeta: funcapi.ColumnMeta{
217
Name: "deadlock_id",
218
- Tooltip: "Deadlock ID",
218
+ Tooltip: "Unique identifier for this deadlock event",
219
Type: funcapi.FieldTypeString,
220
Sort: funcapi.FieldSortAscending,
221
Sortable: true,
@@ -228,7 +228,7 @@ var deadlockColumns = []deadlockColumn{
228
{
229
ColumnMeta: funcapi.ColumnMeta{
230
Name: "ecid",
231
- Tooltip: "ECID",
231
+ Tooltip: "Execution Context ID (SQL Server concept, not used in MySQL)",
232
Type: funcapi.FieldTypeInteger,
233
Sort: funcapi.FieldSortAscending,
234
Sortable: true,
src/go/plugin/go.d/collector/mysql/func_error_info.go
+7
-7
@@ -37,7 +37,7 @@ var errorInfoColumns = []errorInfoColumn{
37
{
38
ColumnMeta: funcapi.ColumnMeta{
39
Name: "digest",
40
- Tooltip: "Digest",
40
+ Tooltip: "Normalized hash of the query for grouping similar statements",
41
Type: funcapi.FieldTypeString,
42
Sortable: true,
43
Visible: false,
@@ -48,7 +48,7 @@ var errorInfoColumns = []errorInfoColumn{
48
{
49
ColumnMeta: funcapi.ColumnMeta{
50
Name: "query",
51
- Tooltip: "Query",
51
+ Tooltip: "The SQL statement that caused the error",
52
Type: funcapi.FieldTypeString,
53
Sortable: true,
54
Visible: true,
@@ -60,7 +60,7 @@ var errorInfoColumns = []errorInfoColumn{
60
{
61
ColumnMeta: funcapi.ColumnMeta{
62
Name: "schema",
63
- Tooltip: "Schema",
63
+ Tooltip: "Database/schema where the error occurred",
64
Type: funcapi.FieldTypeString,
65
Sortable: true,
66
Visible: true,
@@ -70,7 +70,7 @@ var errorInfoColumns = []errorInfoColumn{
70
{
71
ColumnMeta: funcapi.ColumnMeta{
72
Name: "errorNumber",
73
- Tooltip: "Error Number",
73
+ Tooltip: "MySQL error number (MYSQL_ERRNO)",
74
Type: funcapi.FieldTypeInteger,
75
Sortable: true,
76
Visible: true,
@@ -86,7 +86,7 @@ var errorInfoColumns = []errorInfoColumn{
86
{
87
ColumnMeta: funcapi.ColumnMeta{
88
Name: "sqlState",
89
- Tooltip: "SQL State",
89
+ Tooltip: "5-character SQLSTATE error code",
90
Type: funcapi.FieldTypeString,
91
Sortable: true,
92
Visible: true,
@@ -96,7 +96,7 @@ var errorInfoColumns = []errorInfoColumn{
96
{
97
ColumnMeta: funcapi.ColumnMeta{
98
Name: "errorMessage",
99
- Tooltip: "Error Message",
99
+ Tooltip: "The error message text",
100
Type: funcapi.FieldTypeString,
101
Sortable: false,
102
Visible: true,
@@ -267,7 +267,7 @@ func mysqlErrorAttributionColumns() []topQueriesColumn {
267
{
268
ColumnMeta: funcapi.ColumnMeta{
269
Name: "errorAttribution",
270
- Tooltip: "Error Attribution",
270
+ Tooltip: "Source of error data (enabled, not_enabled, no_data)",
271
Type: funcapi.FieldTypeString,
272
Visible: true,
273
Transform: funcapi.FieldTransformNone,