aboutsummaryrefslogtreecommitdiff
path: root/internal/bug/bug.go
blob: c18d35a6a2cefb57c15cfe557fe2fa7f38b88930 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package bug provides utilities for reporting internal bugs, and being
// notified when they occur.
//
// Philosophically, because gopls runs as a sidecar process that the user does
// not directly control, sometimes it keeps going on broken invariants rather
// than panicking. In those cases, bug reports provide a mechanism to alert
// developers and capture relevant metadata.
package bug

import (
	"fmt"
	"runtime"
	"runtime/debug"
	"sort"
	"sync"
)

// PanicOnBugs controls whether to panic when bugs are reported.
//
// It may be set to true during testing.
var PanicOnBugs = false

var (
	mu        sync.Mutex
	exemplars map[string]Bug
	waiters   []chan<- Bug
)

// A Bug represents an unexpected event or broken invariant. They are used for
// capturing metadata that helps us understand the event.
type Bug struct {
	File        string // file containing the call to bug.Report
	Line        int    // line containing the call to bug.Report
	Description string // description of the bug
	Data        Data   // additional metadata
	Key         string // key identifying the bug (file:line if available)
	Stack       string // call stack
}

// Data is additional metadata to record for a bug.
type Data map[string]interface{}

// Reportf reports a formatted bug message.
func Reportf(format string, args ...interface{}) {
	report(fmt.Sprintf(format, args...), nil)
}

// Errorf calls fmt.Errorf for the given arguments, and reports the resulting
// error message as a bug.
func Errorf(format string, args ...interface{}) error {
	err := fmt.Errorf(format, args...)
	report(err.Error(), nil)
	return err
}

// Report records a new bug encountered on the server.
// It uses reflection to report the position of the immediate caller.
func Report(description string, data Data) {
	report(description, data)
}

func report(description string, data Data) {
	_, file, line, ok := runtime.Caller(2) // all exported reporting functions call report directly

	key := "<missing callsite>"
	if ok {
		key = fmt.Sprintf("%s:%d", file, line)
	}

	if PanicOnBugs {
		panic(fmt.Sprintf("%s: %s", key, description))
	}

	bug := Bug{
		File:        file,
		Line:        line,
		Description: description,
		Data:        data,
		Key:         key,
		Stack:       string(debug.Stack()),
	}

	mu.Lock()
	defer mu.Unlock()

	if exemplars == nil {
		exemplars = make(map[string]Bug)
	}

	if _, ok := exemplars[key]; !ok {
		exemplars[key] = bug // capture one exemplar per key
	}

	for _, waiter := range waiters {
		waiter <- bug
	}
	waiters = nil
}

// Notify returns a channel that will be sent the next bug to occur on the
// server. This channel only ever receives one bug.
func Notify() <-chan Bug {
	mu.Lock()
	defer mu.Unlock()

	ch := make(chan Bug, 1) // 1-buffered so that bug reporting is non-blocking
	waiters = append(waiters, ch)
	return ch
}

// List returns a slice of bug exemplars -- the first bugs to occur at each
// callsite.
func List() []Bug {
	mu.Lock()
	defer mu.Unlock()

	var bugs []Bug

	for _, bug := range exemplars {
		bugs = append(bugs, bug)
	}

	sort.Slice(bugs, func(i, j int) bool {
		return bugs[i].Key < bugs[j].Key
	})

	return bugs
}