master
go 769 lines 21 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package mysqlfunc
4
5 import (
6 "bufio"
7 "context"
8 "database/sql"
9 "errors"
10 "fmt"
11 "regexp"
12 "sort"
13 "strconv"
14 "strings"
15 "time"
16
17 mysqlDriver "github.com/go-sql-driver/mysql"
18
19 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
20 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/strmutil"
21 )
22
23 const deadlockInfoMethodID = "deadlock-info"
24 const queryShowEngineInnoDBStatus = "SHOW ENGINE INNODB STATUS;"
25
26 const (
27 deadlockSectionWaiting = "waiting"
28 deadlockSectionHolds = "holds"
29 )
30
31 var (
32 reDeadlockHeader = regexp.MustCompile(`LATEST DETECTED DEADLOCK`)
33 reDeadlockTxn = regexp.MustCompile(`(?i)^\*\*\* \((\d+)\) TRANSACTION:?`)
34 reDeadlockWait = regexp.MustCompile(`(?i)^\*\*\* \((\d+)\) WAITING FOR THIS LOCK TO BE GRANTED:?`)
35 reDeadlockHolds = regexp.MustCompile(`(?i)^\*\*\* \((\d+)\) HOLDS THE LOCK\(S\):?`)
36 reDeadlockWaitNoTxn = regexp.MustCompile(`(?i)^\*\*\*\s*WAITING FOR THIS LOCK TO BE GRANTED:?`)
37 reDeadlockHoldsNoTxn = regexp.MustCompile(`(?i)^\*\*\*\s*HOLDS THE LOCK\(S\):?`)
38 reDeadlockVictim = regexp.MustCompile(`(?i)^\*\*\* WE ROLL BACK TRANSACTION \((\d+)\)`)
39 reDeadlockThread = regexp.MustCompile(`(?i)\b(?:mysql|mariadb)?\s*thread id\s+(\d+)`)
40 reDeadlockMode = regexp.MustCompile(`(?i)lock[_ ]mode\s+([A-Z0-9_-]+)`)
41 reDeadlockTS = regexp.MustCompile(`\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\b`)
42 reDeadlockTable = regexp.MustCompile(`(?i)\bof\s+table\s+` + "`?" + `([-\w$]+)` + "`?" + `\.` + "`?" + `([-\w$]+)` + "`?")
43 reQueryTableRef = regexp.MustCompile(`(?i)\b(?:from|update|into|join)\s+` + "`?" + `([-\w$]+)` + "`?" + `\.` + "`?" + `([-\w$]+)` + "`?")
44 reSQLStatement = regexp.MustCompile(`(?i)^(?:/\*.*\*/\s*)?(SELECT|UPDATE|INSERT|DELETE|REPLACE|WITH|ALTER|CREATE|DROP|TRUNCATE|LOCK|UNLOCK|SET|SHOW|CALL|EXEC|EXECUTE|DO|BEGIN|COMMIT|ROLLBACK|MERGE)\b`)
45 )
46
47 const (
48 deadlockInfoHelp = "Latest detected deadlock from SHOW ENGINE INNODB STATUS. WARNING: query text may include unmasked sensitive literals; restrict dashboard access."
49 deadlockParseErrorStatus = 561
50 )
51
52 // deadlockRowData holds computed values for a single deadlock row.
53 type deadlockRowData struct {
54 rowID string
55 deadlockID string
56 timestamp string
57 processID string
58 spid any
59 isVictim string
60 queryText string
61 lockMode string
62 lockStatus string
63 waitResource string
64 database any
65 }
66
67 // deadlockColumn defines a column for the deadlock-info function.
68 type deadlockColumn struct {
69 funcapi.ColumnMeta
70 Value func(*deadlockRowData) any
71 }
72
73 func deadlockColumnSet(cols []deadlockColumn) funcapi.ColumnSet[deadlockColumn] {
74 return funcapi.Columns(cols, func(c deadlockColumn) funcapi.ColumnMeta { return c.ColumnMeta })
75 }
76
77 var deadlockColumns = []deadlockColumn{
78 {
79 ColumnMeta: funcapi.ColumnMeta{
80 Name: "row_id",
81 Tooltip: "Unique identifier for this row",
82 Type: funcapi.FieldTypeString,
83 Sort: funcapi.FieldSortAscending,
84 Sortable: true,
85 Summary: funcapi.FieldSummaryCount,
86 Filter: funcapi.FieldFilterMultiselect,
87 UniqueKey: true,
88 Visible: false,
89 },
90 Value: func(r *deadlockRowData) any { return r.rowID },
91 },
92 {
93 ColumnMeta: funcapi.ColumnMeta{
94 Name: "timestamp",
95 Tooltip: "When the deadlock occurred",
96 Type: funcapi.FieldTypeTimestamp,
97 Sort: funcapi.FieldSortDescending,
98 Sortable: true,
99 Summary: funcapi.FieldSummaryMax,
100 Filter: funcapi.FieldFilterRange,
101 Visible: true,
102 Transform: funcapi.FieldTransformDatetime,
103 },
104 Value: func(r *deadlockRowData) any { return r.timestamp },
105 },
106 {
107 ColumnMeta: funcapi.ColumnMeta{
108 Name: "is_victim",
109 Tooltip: "Whether this transaction was rolled back to resolve the deadlock",
110 Type: funcapi.FieldTypeString,
111 Visualization: funcapi.FieldVisualPill,
112 Sort: funcapi.FieldSortAscending,
113 Sortable: true,
114 Summary: funcapi.FieldSummaryCount,
115 Filter: funcapi.FieldFilterMultiselect,
116 Visible: true,
117 },
118 Value: func(r *deadlockRowData) any { return r.isVictim },
119 },
120 {
121 ColumnMeta: funcapi.ColumnMeta{
122 Name: "query_text",
123 Tooltip: "The SQL statement being executed",
124 Type: funcapi.FieldTypeString,
125 Sort: funcapi.FieldSortAscending,
126 Sortable: false,
127 Sticky: true,
128 Summary: funcapi.FieldSummaryCount,
129 Filter: funcapi.FieldFilterMultiselect,
130 FullWidth: true,
131 Wrap: true,
132 Visible: true,
133 },
134 Value: func(r *deadlockRowData) any { return r.queryText },
135 },
136 {
137 ColumnMeta: funcapi.ColumnMeta{
138 Name: "database",
139 Tooltip: "Database where the deadlock occurred",
140 Type: funcapi.FieldTypeString,
141 Sort: funcapi.FieldSortAscending,
142 Sortable: true,
143 Summary: funcapi.FieldSummaryCount,
144 Filter: funcapi.FieldFilterMultiselect,
145 Visible: true,
146 },
147 Value: func(r *deadlockRowData) any { return r.database },
148 },
149 {
150 ColumnMeta: funcapi.ColumnMeta{
151 Name: "lock_mode",
152 Tooltip: "Type of lock (S=Shared, X=Exclusive, IS/IX=Intent locks)",
153 Type: funcapi.FieldTypeString,
154 Sort: funcapi.FieldSortAscending,
155 Sortable: true,
156 Summary: funcapi.FieldSummaryCount,
157 Filter: funcapi.FieldFilterMultiselect,
158 Visible: true,
159 },
160 Value: func(r *deadlockRowData) any { return r.lockMode },
161 },
162 {
163 ColumnMeta: funcapi.ColumnMeta{
164 Name: "lock_status",
165 Tooltip: "Whether the lock was granted or still waiting",
166 Type: funcapi.FieldTypeString,
167 Visualization: funcapi.FieldVisualPill,
168 Sort: funcapi.FieldSortAscending,
169 Sortable: true,
170 Summary: funcapi.FieldSummaryCount,
171 Filter: funcapi.FieldFilterMultiselect,
172 Visible: true,
173 },
174 Value: func(r *deadlockRowData) any { return r.lockStatus },
175 },
176 {
177 ColumnMeta: funcapi.ColumnMeta{
178 Name: "wait_resource",
179 Tooltip: "The resource this transaction was waiting to acquire",
180 Type: funcapi.FieldTypeString,
181 Sort: funcapi.FieldSortAscending,
182 Sortable: false,
183 Summary: funcapi.FieldSummaryCount,
184 Filter: funcapi.FieldFilterMultiselect,
185 Visible: true,
186 },
187 Value: func(r *deadlockRowData) any { return r.waitResource },
188 },
189 {
190 ColumnMeta: funcapi.ColumnMeta{
191 Name: "spid",
192 Tooltip: "MySQL thread/connection ID",
193 Type: funcapi.FieldTypeInteger,
194 Sort: funcapi.FieldSortAscending,
195 Sortable: true,
196 Summary: funcapi.FieldSummaryCount,
197 Filter: funcapi.FieldFilterRange,
198 Visible: true,
199 Transform: funcapi.FieldTransformNumber,
200 },
201 Value: func(r *deadlockRowData) any { return r.spid },
202 },
203 {
204 ColumnMeta: funcapi.ColumnMeta{
205 Name: "process_id",
206 Tooltip: "Transaction identifier from InnoDB",
207 Type: funcapi.FieldTypeString,
208 Sort: funcapi.FieldSortAscending,
209 Sortable: true,
210 Summary: funcapi.FieldSummaryCount,
211 Filter: funcapi.FieldFilterMultiselect,
212 Visible: true,
213 },
214 Value: func(r *deadlockRowData) any { return r.processID },
215 },
216 {
217 ColumnMeta: funcapi.ColumnMeta{
218 Name: "deadlock_id",
219 Tooltip: "Unique identifier for this deadlock event",
220 Type: funcapi.FieldTypeString,
221 Sort: funcapi.FieldSortAscending,
222 Sortable: true,
223 Summary: funcapi.FieldSummaryCount,
224 Filter: funcapi.FieldFilterMultiselect,
225 Visible: true,
226 },
227 Value: func(r *deadlockRowData) any { return r.deadlockID },
228 },
229 {
230 ColumnMeta: funcapi.ColumnMeta{
231 Name: "ecid",
232 Tooltip: "Execution Context ID (SQL Server concept, not used in MySQL)",
233 Type: funcapi.FieldTypeInteger,
234 Sort: funcapi.FieldSortAscending,
235 Sortable: true,
236 Summary: funcapi.FieldSummaryCount,
237 Filter: funcapi.FieldFilterRange,
238 Visible: false, // SQL Server concept, not applicable to MySQL/MariaDB
239 Transform: funcapi.FieldTransformNumber,
240 },
241 Value: func(r *deadlockRowData) any { return nil },
242 },
243 }
244
245 func deadlockInfoMethodConfig() funcapi.MethodConfig {
246 return funcapi.MethodConfig{
247 ID: deadlockInfoMethodID,
248 Name: "Deadlock Info",
249 UpdateEvery: 10,
250 Help: deadlockInfoHelp,
251 RequireCloud: true,
252 RequiredParams: []funcapi.ParamConfig{},
253 }
254 }
255
256 // funcDeadlockInfo handles the deadlock-info function.
257 type funcDeadlockInfo struct {
258 router *router
259 }
260
261 func newFuncDeadlockInfo(r *router) *funcDeadlockInfo {
262 return &funcDeadlockInfo{router: r}
263 }
264
265 // Compile-time interface check.
266 var _ funcapi.MethodHandler = (*funcDeadlockInfo)(nil)
267
268 func (f *funcDeadlockInfo) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
269 if f.router.cfg.deadlockInfoDisabled() {
270 return nil, fmt.Errorf("deadlock-info function disabled in configuration")
271 }
272 return []funcapi.ParamConfig{}, nil
273 }
274
275 func (f *funcDeadlockInfo) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
276 if _, err := f.router.deps.DB(); err != nil {
277 return funcapi.UnavailableResponse("collector is still initializing, please retry in a few seconds")
278 }
279 queryCtx, cancel := context.WithTimeout(ctx, f.router.cfg.deadlockInfoTimeout())
280 defer cancel()
281 return f.collectData(queryCtx)
282 }
283
284 func (f *funcDeadlockInfo) Cleanup(ctx context.Context) {}
285
286 func (f *funcDeadlockInfo) collectData(ctx context.Context) *funcapi.FunctionResponse {
287 if f.router.cfg.deadlockInfoDisabled() {
288 return funcapi.UnavailableResponse("deadlock-info function has been disabled in configuration")
289 }
290
291 statusText, err := f.queryInnoDBStatus(ctx)
292 if err != nil {
293 if errors.Is(err, context.DeadlineExceeded) {
294 return f.buildResponse(504, "deadlock query timed out", nil)
295 }
296 if isMySQLPermissionError(err) {
297 return f.buildResponse(
298 403,
299 "Deadlock info requires permission to run SHOW ENGINE INNODB STATUS. "+
300 "Grant with: GRANT USAGE, REPLICATION CLIENT, PROCESS ON *.* TO 'netdata'@'%';",
301 nil,
302 )
303 }
304 f.router.log.Warningf("deadlock-info: query failed: %v", err)
305 return f.buildResponse(500, fmt.Sprintf("deadlock query failed: %v", err), nil)
306 }
307
308 parseRes := parseInnoDBDeadlock(statusText, time.Now().UTC())
309 if parseRes.parseErr != nil {
310 f.router.log.Warningf("deadlock-info: parse failed: %v", parseRes.parseErr)
311 return f.buildResponse(deadlockParseErrorStatus, "deadlock section could not be parsed", nil)
312 }
313 if !parseRes.found {
314 return f.buildResponse(200, "no deadlock found in SHOW ENGINE INNODB STATUS", nil)
315 }
316
317 deadlockID := generateDeadlockID(parseRes.deadlockTime)
318 rows := buildDeadlockRows(parseRes, deadlockID)
319 if len(rows) == 0 {
320 return f.buildResponse(200, "deadlock detected but no transactions could be parsed", nil)
321 }
322
323 return f.buildResponse(200, "latest detected deadlock", rows)
324 }
325
326 func (f *funcDeadlockInfo) buildResponse(status int, message string, rowsData []deadlockRowData) *funcapi.FunctionResponse {
327 data := make([][]any, 0, len(rowsData))
328 for i := range rowsData {
329 row := make([]any, len(deadlockColumns))
330 for j, col := range deadlockColumns {
331 row[j] = col.Value(&rowsData[i])
332 }
333 data = append(data, row)
334 }
335
336 cs := deadlockColumnSet(deadlockColumns)
337
338 return &funcapi.FunctionResponse{
339 Status: status,
340 Help: deadlockInfoHelp,
341 Message: message,
342 Columns: cs.BuildColumns(),
343 Data: data,
344 DefaultSortColumn: "timestamp",
345 }
346 }
347
348 func (f *funcDeadlockInfo) queryInnoDBStatus(ctx context.Context) (string, error) {
349 qctx, cancel := context.WithTimeout(ctx, f.router.cfg.collectorTimeout())
350 defer cancel()
351
352 db, err := f.router.deps.DB()
353 if err != nil {
354 return "", err
355 }
356
357 var typ, name, status sql.NullString
358 row := db.QueryRowContext(qctx, queryShowEngineInnoDBStatus)
359 if err := row.Scan(&typ, &name, &status); err != nil {
360 return "", err
361 }
362 if !status.Valid {
363 return "", fmt.Errorf("innodb status response was empty")
364 }
365 return status.String, nil
366 }
367
368 type mysqlDeadlockTxn struct {
369 txnNum int
370 threadID string
371 queryText string
372 lockMode string
373 lockStatus string
374 waitResource string
375 }
376
377 type mysqlDeadlockParseResult struct {
378 found bool
379 deadlockTime time.Time
380 victimTxnNum int
381 transactions []*mysqlDeadlockTxn
382 parseErr error
383 }
384
385 func parseInnoDBDeadlock(status string, now time.Time) mysqlDeadlockParseResult {
386 result := mysqlDeadlockParseResult{
387 found: false,
388 deadlockTime: now.UTC(),
389 }
390
391 section, ok := extractDeadlockSection(status)
392 if !ok {
393 return result
394 }
395 result.found = true
396
397 if ts, ok := parseDeadlockTimestamp(section); ok {
398 result.deadlockTime = ts.UTC()
399 }
400
401 scanner := bufio.NewScanner(strings.NewReader(section))
402 scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
403
404 txnByNum := make(map[int]*mysqlDeadlockTxn)
405 txnOrder := make([]int, 0, 4)
406
407 currentTxnNum := 0
408 currentSection := ""
409 expectingQueryTxn := 0
410 victimTxnNum := 0
411
412 ensureTxn := func(num int) *mysqlDeadlockTxn {
413 if txn, ok := txnByNum[num]; ok {
414 return txn
415 }
416 txn := &mysqlDeadlockTxn{txnNum: num}
417 txnByNum[num] = txn
418 txnOrder = append(txnOrder, num)
419 return txn
420 }
421
422 for scanner.Scan() {
423 line := strings.TrimSpace(scanner.Text())
424 if line == "" {
425 continue
426 }
427
428 if num, ok := parseDeadlockTxnHeader(line); ok {
429 currentTxnNum = num
430 currentSection = ""
431 expectingQueryTxn = 0
432 ensureTxn(num)
433 continue
434 }
435
436 if num, sectionType, ok := parseDeadlockTxnSection(line); ok {
437 currentTxnNum = num
438 currentSection = sectionType
439 expectingQueryTxn = 0
440 ensureTxn(num)
441 continue
442 }
443
444 if currentTxnNum != 0 {
445 if isDeadlockWaitNoTxn(line) {
446 currentSection = deadlockSectionWaiting
447 expectingQueryTxn = 0
448 ensureTxn(currentTxnNum)
449 continue
450 }
451 if isDeadlockHoldsNoTxn(line) {
452 currentSection = deadlockSectionHolds
453 expectingQueryTxn = 0
454 ensureTxn(currentTxnNum)
455 continue
456 }
457 }
458
459 if num, ok := parseDeadlockVictim(line); ok {
460 victimTxnNum = num
461 continue
462 }
463
464 if currentTxnNum == 0 {
465 continue
466 }
467
468 txn := ensureTxn(currentTxnNum)
469
470 if threadID, ok := parseDeadlockThreadID(line); ok {
471 txn.threadID = threadID
472 expectingQueryTxn = currentTxnNum
473 continue
474 }
475
476 if expectingQueryTxn == currentTxnNum && txn.queryText == "" && isSQLStatementLine(line) {
477 txn.queryText = line
478 expectingQueryTxn = 0
479 continue
480 }
481
482 if expectingQueryTxn == 0 && txn.queryText == "" && isSQLStatementLine(line) {
483 txn.queryText = line
484 continue
485 }
486
487 switch currentSection {
488 case deadlockSectionWaiting:
489 // WAITING must win even if HOLDS was seen first in the output.
490 txn.lockStatus = "WAITING"
491 if txn.waitResource == "" && isLockResourceLine(line) {
492 txn.waitResource = strmutil.TruncateText(line, topQueriesMaxTextLength)
493 }
494 if mode, ok := parseDeadlockLockMode(line); ok {
495 // WAITING lock mode should override any mode captured from HOLDS.
496 txn.lockMode = mode
497 }
498 case deadlockSectionHolds:
499 if txn.lockStatus == "" {
500 txn.lockStatus = "GRANTED"
501 }
502 if txn.lockMode == "" {
503 if mode, ok := parseDeadlockLockMode(line); ok {
504 txn.lockMode = mode
505 }
506 }
507 }
508 }
509
510 if err := scanner.Err(); err != nil {
511 result.parseErr = err
512 return result
513 }
514
515 result.victimTxnNum = victimTxnNum
516 result.transactions = make([]*mysqlDeadlockTxn, 0, len(txnOrder))
517 for _, num := range txnOrder {
518 txn := txnByNum[num]
519 if txn == nil {
520 continue
521 }
522 if txn.threadID == "" {
523 txn.threadID = fmt.Sprintf("txn-%d", num)
524 }
525 if txn.lockStatus == "" {
526 if num == victimTxnNum {
527 txn.lockStatus = "WAITING"
528 } else {
529 txn.lockStatus = "GRANTED"
530 }
531 }
532 result.transactions = append(result.transactions, txn)
533 }
534
535 if len(result.transactions) == 0 {
536 result.parseErr = fmt.Errorf("deadlock section detected but no transactions could be parsed")
537 return result
538 }
539
540 sort.Slice(result.transactions, func(i, j int) bool {
541 return result.transactions[i].txnNum < result.transactions[j].txnNum
542 })
543
544 return result
545 }
546
547 func buildDeadlockRows(parseRes mysqlDeadlockParseResult, deadlockID string) []deadlockRowData {
548 rows := make([]deadlockRowData, 0, len(parseRes.transactions))
549 timestamp := parseRes.deadlockTime.UTC().Format(time.RFC3339Nano)
550
551 for _, txn := range parseRes.transactions {
552 if txn == nil {
553 continue
554 }
555
556 processID := strings.TrimSpace(txn.threadID)
557 if processID == "" {
558 processID = fmt.Sprintf("txn-%d", txn.txnNum)
559 }
560
561 var spid any
562 if id, err := strconv.Atoi(processID); err == nil {
563 spid = id
564 } else {
565 spid = nil
566 }
567
568 isVictim := "false"
569 if parseRes.victimTxnNum != 0 && txn.txnNum == parseRes.victimTxnNum {
570 isVictim = "true"
571 }
572
573 queryText := strmutil.TruncateText(strings.TrimSpace(txn.queryText), topQueriesMaxTextLength)
574 lockMode := formatLockMode(strings.TrimSpace(txn.lockMode))
575 lockStatus := strings.TrimSpace(txn.lockStatus)
576 waitResource := strmutil.TruncateText(strings.TrimSpace(txn.waitResource), topQueriesMaxTextLength)
577 database := extractDeadlockDatabase(waitResource, queryText)
578
579 var databaseValue any
580 if database != "" {
581 databaseValue = database
582 }
583
584 rows = append(rows, deadlockRowData{
585 rowID: fmt.Sprintf("%s:%s", deadlockID, processID),
586 deadlockID: deadlockID,
587 timestamp: timestamp,
588 processID: processID,
589 spid: spid,
590 isVictim: isVictim,
591 queryText: queryText,
592 lockMode: lockMode,
593 lockStatus: lockStatus,
594 waitResource: waitResource,
595 database: databaseValue,
596 })
597 }
598
599 return rows
600 }
601
602 func extractDeadlockSection(status string) (string, bool) {
603 idx := reDeadlockHeader.FindStringIndex(status)
604 if idx == nil {
605 return "", false
606 }
607 return status[idx[0]:], true
608 }
609
610 func parseDeadlockTimestamp(section string) (time.Time, bool) {
611 match := reDeadlockTS.FindString(section)
612 if match == "" {
613 return time.Time{}, false
614 }
615 ts, err := time.ParseInLocation("2006-01-02 15:04:05", match, time.Local)
616 if err != nil {
617 return time.Time{}, false
618 }
619 return ts, true
620 }
621
622 func parseDeadlockTxnHeader(line string) (int, bool) {
623 m := reDeadlockTxn.FindStringSubmatch(line)
624 if len(m) != 2 {
625 return 0, false
626 }
627 n, err := strconv.Atoi(m[1])
628 if err != nil {
629 return 0, false
630 }
631 return n, true
632 }
633
634 func parseDeadlockTxnSection(line string) (int, string, bool) {
635 if m := reDeadlockWait.FindStringSubmatch(line); len(m) == 2 {
636 n, err := strconv.Atoi(m[1])
637 if err != nil {
638 return 0, "", false
639 }
640 return n, deadlockSectionWaiting, true
641 }
642 if m := reDeadlockHolds.FindStringSubmatch(line); len(m) == 2 {
643 n, err := strconv.Atoi(m[1])
644 if err != nil {
645 return 0, "", false
646 }
647 return n, deadlockSectionHolds, true
648 }
649 return 0, "", false
650 }
651
652 func isDeadlockWaitNoTxn(line string) bool {
653 return reDeadlockWaitNoTxn.MatchString(line)
654 }
655
656 func isDeadlockHoldsNoTxn(line string) bool {
657 return reDeadlockHoldsNoTxn.MatchString(line)
658 }
659
660 func parseDeadlockVictim(line string) (int, bool) {
661 m := reDeadlockVictim.FindStringSubmatch(line)
662 if len(m) != 2 {
663 return 0, false
664 }
665 n, err := strconv.Atoi(m[1])
666 if err != nil {
667 return 0, false
668 }
669 return n, true
670 }
671
672 func parseDeadlockThreadID(line string) (string, bool) {
673 m := reDeadlockThread.FindStringSubmatch(line)
674 if len(m) != 2 {
675 return "", false
676 }
677 return m[1], true
678 }
679
680 func parseDeadlockLockMode(line string) (string, bool) {
681 m := reDeadlockMode.FindStringSubmatch(line)
682 if len(m) != 2 {
683 return "", false
684 }
685 return strings.ToUpper(m[1]), true
686 }
687
688 func isLikelyQueryLine(line string) bool {
689 return isSQLStatementLine(line)
690 }
691
692 func isSQLStatementLine(line string) bool {
693 trimmed := strings.TrimSpace(line)
694 if trimmed == "" {
695 return false
696 }
697 upper := strings.ToUpper(trimmed)
698 if strings.HasPrefix(upper, "LOCK WAIT") {
699 return false
700 }
701 return reSQLStatement.MatchString(trimmed)
702 }
703
704 func isLockResourceLine(line string) bool {
705 upper := strings.ToUpper(strings.TrimSpace(line))
706 return strings.HasPrefix(upper, "RECORD LOCKS") || strings.HasPrefix(upper, "TABLE LOCK")
707 }
708
709 func extractDeadlockDatabase(waitResource, queryText string) string {
710 if db := extractDatabaseFromLock(waitResource); db != "" {
711 return db
712 }
713 if db := extractDatabaseFromQuery(queryText); db != "" {
714 return db
715 }
716 return ""
717 }
718
719 func extractDatabaseFromLock(line string) string {
720 m := reDeadlockTable.FindStringSubmatch(line)
721 if len(m) >= 2 {
722 return m[1]
723 }
724 return ""
725 }
726
727 func extractDatabaseFromQuery(queryText string) string {
728 m := reQueryTableRef.FindStringSubmatch(queryText)
729 if len(m) >= 2 {
730 return m[1]
731 }
732 return ""
733 }
734
735 func generateDeadlockID(t time.Time) string {
736 if t.IsZero() {
737 t = time.Now().UTC()
738 }
739 t = t.UTC()
740 micros := t.Nanosecond() / 1000
741 return t.Format("20060102150405") + fmt.Sprintf("%06d", micros)
742 }
743
744 func isMySQLPermissionError(err error) bool {
745 var mysqlErr *mysqlDriver.MySQLError
746 if errors.As(err, &mysqlErr) {
747 if mysqlErr.Number == 1045 || mysqlErr.Number == 1227 {
748 return true
749 }
750 }
751 msg := strings.ToLower(err.Error())
752 return strings.Contains(msg, "access denied") ||
753 strings.Contains(msg, "permission denied") ||
754 strings.Contains(msg, "process privilege")
755 }
756
757 // formatLockMode converts InnoDB lock mode abbreviations to human-readable format.
758 func formatLockMode(mode string) string {
759 names := map[string]string{
760 "X": "Exclusive",
761 "S": "Shared",
762 "IX": "Intent Exclusive",
763 "IS": "Intent Shared",
764 }
765 if name, ok := names[mode]; ok {
766 return fmt.Sprintf("%s (%s)", name, mode)
767 }
768 return mode
769 }