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
package log
import (
"fmt"
"io"
"os"
"strings"
"github.com/urfave/cli/v2"
"golang.org/x/exp/slog"
"golang.org/x/term"
"github.com/ethereum/go-ethereum/log"
opservice "github.com/ethereum-optimism/optimism/op-service"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
)
const (
LevelFlagName = "log.level"
FormatFlagName = "log.format"
ColorFlagName = "log.color"
PidFlagName = "log.pid"
)
func CLIFlags(envPrefix string) []cli.Flag {
return CLIFlagsWithCategory(envPrefix, "")
}
// CLIFlagsWithCategory creates flag definitions for the logging utils.
// Warning: flags are not safe to reuse due to an upstream urfave default-value mutation bug in GenericFlag.
// Use cliapp.ProtectFlags(flags) to create a copy before passing it into an App if the app runs more than once.
func CLIFlagsWithCategory(envPrefix string, category string) []cli.Flag {
return []cli.Flag{
&cli.GenericFlag{
Name: LevelFlagName,
Usage: "The lowest log level that will be output",
Value: NewLevelFlagValue(log.LevelInfo),
EnvVars: opservice.PrefixEnvVar(envPrefix, "LOG_LEVEL"),
Category: category,
},
&cli.GenericFlag{
Name: FormatFlagName,
Usage: "Format the log output. Supported formats: 'text', 'terminal', 'logfmt', 'json', 'json-pretty',",
Value: NewFormatFlagValue(FormatText),
EnvVars: opservice.PrefixEnvVar(envPrefix, "LOG_FORMAT"),
Category: category,
},
&cli.BoolFlag{
Name: ColorFlagName,
Usage: "Color the log output if in terminal mode",
EnvVars: opservice.PrefixEnvVar(envPrefix, "LOG_COLOR"),
Category: category,
},
&cli.BoolFlag{
Name: PidFlagName,
Usage: "Show pid in the log",
EnvVars: opservice.PrefixEnvVar(envPrefix, "LOG_PID"),
Category: category,
},
}
}
// LevelFlagValue is a value type for cli.GenericFlag to parse and validate log-level values.
// Log level: trace, debug, info, warn, error, crit. Capitals are accepted too.
type LevelFlagValue slog.Level
func NewLevelFlagValue(lvl slog.Level) *LevelFlagValue {
return (*LevelFlagValue)(&lvl)
}
func (fv *LevelFlagValue) Set(value string) error {
value = strings.ToLower(value) // ignore case
lvl, err := LevelFromString(value)
if err != nil {
return err
}
*fv = LevelFlagValue(lvl)
return nil
}
func (fv LevelFlagValue) String() string {
return slog.Level(fv).String()
}
func (fv LevelFlagValue) Level() slog.Level {
return slog.Level(fv).Level()
}
func (fv *LevelFlagValue) Clone() any {
cpy := *fv
return &cpy
}
// LevelFromString returns the appropriate Level from a string name.
// Useful for parsing command line args and configuration files.
// It also converts strings to lowercase.
// If the string is unknown, LevelDebug is returned as a default, together with
// a non-nil error.
func LevelFromString(lvlString string) (slog.Level, error) {
lvlString = strings.ToLower(lvlString) // ignore case
switch lvlString {
case "trace", "trce":
return log.LevelTrace, nil
case "debug", "dbug":
return log.LevelDebug, nil
case "info":
return log.LevelInfo, nil
case "warn":
return log.LevelWarn, nil
case "error", "eror":
return log.LevelError, nil
case "crit":
return log.LevelCrit, nil
default:
return log.LevelDebug, fmt.Errorf("unknown level: %v", lvlString)
}
}
var _ cliapp.CloneableGeneric = (*LevelFlagValue)(nil)
// FormatType defines a type of log format.
// Supported formats: 'text', 'terminal', 'logfmt', 'json'
type FormatType string
const (
FormatText FormatType = "text"
FormatTerminal FormatType = "terminal"
FormatLogFmt FormatType = "logfmt"
FormatJSON FormatType = "json"
)
// FormatHandler returns the correct slog handler factory for the provided format.
func FormatHandler(ft FormatType, color bool) func(io.Writer) slog.Handler {
termColorHandler := func(w io.Writer) slog.Handler {
return log.NewTerminalHandler(w, color)
}
logfmtHandler := func(w io.Writer) slog.Handler { return log.LogfmtHandlerWithLevel(w, log.LevelTrace) }
switch ft {
case FormatJSON:
return log.JSONHandler
case FormatText:
if term.IsTerminal(int(os.Stdout.Fd())) {
return termColorHandler
} else {
return logfmtHandler
}
case FormatTerminal:
return termColorHandler
case FormatLogFmt:
return logfmtHandler
default:
panic(fmt.Errorf("failed to create slog.Handler factory for format-type=%q and color=%v", ft, color))
}
}
func (ft FormatType) String() string {
return string(ft)
}
// FormatFlagValue is a value type for cli.GenericFlag to parse and validate log-formatting-type values
type FormatFlagValue FormatType
func NewFormatFlagValue(fmtType FormatType) *FormatFlagValue {
return (*FormatFlagValue)(&fmtType)
}
func (fv *FormatFlagValue) Set(value string) error {
switch FormatType(value) {
case FormatText, FormatTerminal, FormatLogFmt, FormatJSON:
*fv = FormatFlagValue(value)
return nil
default:
return fmt.Errorf("unrecognized log-format: %q", value)
}
}
func (fv FormatFlagValue) String() string {
return FormatType(fv).String()
}
func (fv FormatFlagValue) FormatType() FormatType {
return FormatType(fv)
}
func (fv *FormatFlagValue) Clone() any {
cpy := *fv
return &cpy
}
var _ cliapp.CloneableGeneric = (*FormatFlagValue)(nil)
type CLIConfig struct {
Level slog.Level
Color bool
Format FormatType
Pid bool
}
// AppOut returns an io.Writer to write app output to, like logs.
// This falls back to os.Stdout if the ctx, ctx.App or ctx.App.Writer are nil.
func AppOut(ctx *cli.Context) io.Writer {
if ctx == nil || ctx.App == nil || ctx.App.Writer == nil {
return os.Stdout
}
return ctx.App.Writer
}
// NewLogHandler creates a new configured handler, compatible as LvlSetter for log-level changes during runtime.
func NewLogHandler(wr io.Writer, cfg CLIConfig) slog.Handler {
handler := FormatHandler(cfg.Format, cfg.Color)(wr)
return NewDynamicLogHandler(cfg.Level, handler)
}
// NewLogger creates a new configured logger.
// The log handler of the logger is a LvlSetter, i.e. the log level can be changed as needed.
func NewLogger(wr io.Writer, cfg CLIConfig) log.Logger {
h := NewLogHandler(wr, cfg)
l := log.NewLogger(h)
if cfg.Pid {
l = l.With("pid", os.Getpid())
}
return l
}
// SetGlobalLogHandler sets the log handles as the handler of the global default logger.
// The usage of this logger is strongly discouraged,
// as it does makes it difficult to distinguish different services in the same process, e.g. during tests.
// Geth and other components may use the global logger however,
// and it is thus recommended to set the global log handler to catch these logs.
func SetGlobalLogHandler(h slog.Handler) {
log.SetDefault(log.NewLogger(h))
}
// DefaultCLIConfig creates a default log configuration.
// Color defaults to true if terminal is detected.
func DefaultCLIConfig() CLIConfig {
return CLIConfig{
Level: log.LevelInfo,
Format: FormatText,
Color: term.IsTerminal(int(os.Stdout.Fd())),
}
}
func ReadCLIConfig(ctx *cli.Context) CLIConfig {
cfg := DefaultCLIConfig()
cfg.Level = ctx.Generic(LevelFlagName).(*LevelFlagValue).Level()
cfg.Format = ctx.Generic(FormatFlagName).(*FormatFlagValue).FormatType()
if ctx.IsSet(ColorFlagName) {
cfg.Color = ctx.Bool(ColorFlagName)
}
cfg.Pid = ctx.Bool(PidFlagName)
return cfg
}