File: errmgr.go

package info (click to toggle)
golang-github-olekukonko-errors 1.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental, forky, sid
  • size: 448 kB
  • sloc: makefile: 2
file content (269 lines) | stat: -rw-r--r-- 8,328 bytes parent folder | download | duplicates (2)
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Package errmgr provides functionality for managing error templates, counts, thresholds,
// and alerts in a thread-safe manner, building on the core errors package.
package errmgr

import (
	"fmt"
	"github.com/olekukonko/errors"
	"strings"
	"sync"
	"sync/atomic"
)

// Config holds configuration for the errmgr package.
type Config struct {
	DisableMetrics bool // Disables counting and tracking if true
}

// cachedConfig holds the current configuration, updated only on Configure().
type cachedConfig struct {
	disableErrMgr bool
}

var (
	currentConfig cachedConfig
	configMu      sync.RWMutex
	registry      = errorRegistry{counts: shardedCounter{}}
	codes         = codeRegistry{m: make(map[string]int)}
)

func init() {
	currentConfig = cachedConfig{disableErrMgr: false}
}

// errorRegistry holds registered errors and their metadata.
type errorRegistry struct {
	templates  sync.Map       // map[string]string: Error templates
	funcs      sync.Map       // map[string]func(...interface{}) *errors.Error: Custom error functions
	counts     shardedCounter // Sharded counter for error occurrences
	thresholds sync.Map       // map[string]uint64: Alert thresholds
	alerts     sync.Map       // map[string]*alertChannel: Alert channels
	mu         sync.RWMutex   // Protects alerts map
}

// codeRegistry manages error codes with explicit locking.
type codeRegistry struct {
	m  map[string]int
	mu sync.RWMutex
}

// shardedCounter provides a low-contention counter for error occurrences.
type shardedCounter struct {
	counts sync.Map
}

// Categorized creates a categorized error template and returns a function to create errors.
// The returned function applies the category to each error instance.
func Categorized(category errors.ErrorCategory, name, template string) func(...interface{}) *errors.Error {
	f := Define(name, template)
	return func(args ...interface{}) *errors.Error {
		return f(args...).WithCategory(category)
	}
}

// CloseMonitor closes the alert channel for a specific error name.
// Thread-safe; subsequent alerts for this name are ignored.
func CloseMonitor(name string) {
	registry.mu.Lock()
	defer registry.mu.Unlock()

	if ch, ok := registry.alerts.Load(name); ok {
		ac := ch.(*alertChannel)
		ac.mu.Lock()
		if !ac.closed {
			close(ac.ch)
			ac.closed = true
		}
		ac.mu.Unlock()
		registry.alerts.Delete(name)
	}
}

// Coded creates a templated error with a specific HTTP status code.
// It wraps Define and applies the code to each error instance returned.
func Coded(name, template string, code int) func(...interface{}) *errors.Error {
	codes.mu.Lock()
	codes.m[name] = code
	codes.mu.Unlock()
	base := Define(name, template)
	return func(args ...interface{}) *errors.Error {
		err := base(args...)
		return err.WithCode(code)
	}
}

// Configure updates the global configuration for the errmgr package.
// Thread-safe; applies immediately to all subsequent operations.
func Configure(cfg Config) {
	configMu.Lock()
	currentConfig = cachedConfig{disableErrMgr: cfg.DisableMetrics}
	configMu.Unlock()
}

// Copy creates a new instance of a predefined static error, ensuring immutability of originals.
// Use this for static errors; templated errors should be called directly with arguments.
func Copy(err *errors.Error) *errors.Error {
	return err.Copy()
}

// Define creates a templated error that formats a message with provided arguments.
// The error is tracked in the registry if error management is enabled.
func Define(name, template string) func(...interface{}) *errors.Error {
	registry.templates.Store(name, template)
	if !currentConfig.disableErrMgr {
		registry.counts.RegisterName(name)
	}
	return func(args ...interface{}) *errors.Error {
		var buf strings.Builder
		buf.Grow(len(template) + len(name) + len(args)*10)
		fmt.Fprintf(&buf, template, args...)
		err := errors.New(buf.String()).WithName(name).WithTemplate(template)
		if !currentConfig.disableErrMgr {
			registry.counts.Inc(name)
		}
		return err
	}
}

// GetThreshold returns the current threshold for an error name, if set.
// Returns 0 and false if no threshold is defined.
func GetThreshold(name string) (uint64, bool) {
	if thresh, ok := registry.thresholds.Load(name); ok {
		return thresh.(uint64), true
	}
	return 0, false
}

// Inc increments the counter for a specific name in a shard and checks thresholds.
// Returns the new count for the shard; use Value() for the total count.
func (c *shardedCounter) Inc(name string) uint64 {
	countPtr, _ := c.counts.LoadOrStore(name, new(uint64))
	count := countPtr.(*uint64)
	newCount := atomic.AddUint64(count, 1)

	if thresh, ok := registry.thresholds.Load(name); ok {
		total := atomic.LoadUint64(count)
		if total >= thresh.(uint64) {
			if ch, ok := registry.alerts.Load(name); ok {
				ac := ch.(*alertChannel)
				ac.mu.Lock()
				if !ac.closed {
					alert := errors.New(fmt.Sprintf("%s count exceeded threshold: %d", name, total)).
						WithName(name)
					for i := uint64(0); i < total; i++ {
						_ = alert.Increment()
					}
					select {
					case ac.ch <- alert:
					default: // Drop if channel is full
					}
				}
				ac.mu.Unlock()
			}
		}
	}
	return newCount
}

// ListNames returns all registered error names in the counter.
// Thread-safe; returns an empty slice if no names are registered.
func (c *shardedCounter) ListNames() []string {
	var names []string
	c.counts.Range(func(key, _ interface{}) bool {
		names = append(names, key.(string))
		return true
	})
	return names
}

// Metrics returns a snapshot of error counts for monitoring systems.
// Returns nil if error management is disabled or no counts exist.
func Metrics() map[string]uint64 {
	if currentConfig.disableErrMgr {
		return nil
	}
	counts := make(map[string]uint64)
	registry.counts.counts.Range(func(key, value interface{}) bool {
		name := key.(string)
		count := registry.counts.Value(name)
		if count > 0 {
			counts[name] = count
		}
		return true
	})
	if len(counts) == 0 {
		return nil
	}
	return counts
}

// RegisterName ensures a counter exists for the name without incrementing it.
// Thread-safe; useful for pre-registering error names.
func (c *shardedCounter) RegisterName(name string) {
	c.counts.LoadOrStore(name, new(uint64))
}

// RemoveThreshold removes the threshold for a specific error name.
// Thread-safe; no effect if no threshold exists.
func RemoveThreshold(name string) {
	registry.thresholds.Delete(name)
}

// Reset clears all counters and removes their registrations.
// Has no effect if error management is disabled.
func Reset() {
	if currentConfig.disableErrMgr {
		return
	}
	registry.counts.counts.Range(func(key, _ interface{}) bool {
		registry.counts.Reset(key.(string))
		registry.counts.counts.Delete(key)
		return true
	})
}

// ResetCounter resets the occurrence counter for a specific error type.
// Has no effect if error management is disabled or the name isn’t registered.
func ResetCounter(name string) {
	if !currentConfig.disableErrMgr {
		registry.counts.Reset(name)
	}
}

// Reset resets the counter for a specific name across all shards.
// Thread-safe; no effect if the name isn’t registered.
func (c *shardedCounter) Reset(name string) {
	if countPtr, ok := c.counts.Load(name); ok {
		atomic.StoreUint64(countPtr.(*uint64), 0)
	}
}

// SetThreshold sets a count threshold for an error name, triggering alerts when exceeded.
// Alerts are sent to the Monitor channel if one exists for the name.
func SetThreshold(name string, threshold uint64) {
	registry.thresholds.Store(name, threshold)
}

// Tracked registers a custom error function and tracks its occurrences in the registry.
// The returned function increments the error count each time it is called.
func Tracked(name string, fn func(...interface{}) *errors.Error) func(...interface{}) *errors.Error {
	registry.funcs.Store(name, fn)
	if !currentConfig.disableErrMgr {
		registry.counts.RegisterName(name)
	}
	return func(args ...interface{}) *errors.Error {
		if !currentConfig.disableErrMgr {
			registry.counts.Inc(name)
		}
		return fn(args...)
	}
}

// Value returns the total count for a specific name across all shards.
// Thread-safe; returns 0 if the name isn’t registered.
func (c *shardedCounter) Value(name string) uint64 {
	if countPtr, ok := c.counts.Load(name); ok {
		return atomic.LoadUint64(countPtr.(*uint64))
	}
	return 0
}