aboutsummaryrefslogtreecommitdiffhomepage
path: root/message/message.go
blob: aa4870f90f885651b8c8efbb85027b7761181864 (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
// Package message provides interfaces and a base implementation for extended reporting on top of [log.Logger]
package message

import (
	"errors"
	"log"
	"sync/atomic"
)

// Error is an error with a user-facing message.
type Error interface {
	// Message returns a user-facing error message.
	Message() string

	error
}

// GetMessage returns whether an error implements [Error], and the message if it does.
func GetMessage(err error) (string, bool) {
	var e Error
	if !errors.As(err, &e) || e == nil {
		return "", false
	}
	return e.Message(), true
}

// Msg is used for package-wide verbose logging.
type Msg interface {
	// GetLogger returns the address of the underlying [log.Logger].
	GetLogger() *log.Logger

	// IsVerbose atomically loads and returns whether [Msg] has verbose logging enabled.
	IsVerbose() bool
	// SwapVerbose atomically stores a new verbose state and returns the previous value held by [Msg].
	SwapVerbose(verbose bool) bool
	// Verbose passes its argument to the Println method of the underlying [log.Logger] if IsVerbose returns true.
	Verbose(v ...any)
	// Verbosef passes its argument to the Printf method of the underlying [log.Logger] if IsVerbose returns true.
	Verbosef(format string, v ...any)

	// Suspend causes the embedded [Suspendable] to withhold writes to its downstream [io.Writer].
	// Suspend returns false and is a noop if called between calls to Suspend and Resume.
	Suspend() bool

	// Resume dumps the entire buffer held by the embedded [Suspendable] and stops withholding future writes.
	// Resume returns false and is a noop if a call to Suspend does not precede it.
	Resume() bool

	// BeforeExit runs implementation-specific cleanup code, and optionally prints warnings.
	// BeforeExit is called before [os.Exit].
	BeforeExit()
}

// defaultMsg is the default implementation of the [Msg] interface.
// The zero value is not safe for use. Callers should use the [New] function instead.
type defaultMsg struct {
	verbose atomic.Bool

	logger *log.Logger
	Suspendable
}

// New initialises a downstream [log.Logger] for a new [Msg].
// The [log.Logger] should no longer be configured after [New] returns.
// If downstream is nil, a new logger is initialised in its place.
func New(downstream *log.Logger) Msg {
	if downstream == nil {
		downstream = log.New(log.Writer(), "container: ", 0)
	}

	m := defaultMsg{logger: downstream}
	m.Suspendable.Downstream = downstream.Writer()
	downstream.SetOutput(&m.Suspendable)
	return &m
}

func (msg *defaultMsg) GetLogger() *log.Logger { return msg.logger }

func (msg *defaultMsg) IsVerbose() bool               { return msg.verbose.Load() }
func (msg *defaultMsg) SwapVerbose(verbose bool) bool { return msg.verbose.Swap(verbose) }
func (msg *defaultMsg) Verbose(v ...any) {
	if msg.verbose.Load() {
		msg.logger.Println(v...)
	}
}
func (msg *defaultMsg) Verbosef(format string, v ...any) {
	if msg.verbose.Load() {
		msg.logger.Printf(format, v...)
	}
}

// Resume calls [Suspendable.Resume] and prints a message if buffer was filled
// between calls to [Suspendable.Suspend] and Resume.
func (msg *defaultMsg) Resume() bool {
	resumed, dropped, _, err := msg.Suspendable.Resume()
	if err != nil {
		// probably going to result in an error as well, so this message is as good as unreachable
		msg.logger.Printf("cannot dump buffer on resume: %v", err)
	}
	if resumed && dropped > 0 {
		msg.logger.Printf("dropped %d bytes while output is suspended", dropped)
	}
	return resumed
}

// BeforeExit prints a message if called between calls to [Suspendable.Suspend] and Resume.
func (msg *defaultMsg) BeforeExit() {
	if msg.Resume() {
		msg.logger.Printf("beforeExit reached on suspended output")
	}
}