Skip to content

Commit d306349

Browse files
authored
logging enhancement (#246)
Added: - stacktrace to log entry - cmd flag to enable/disable stacktrace (by default stacktrace enabled, stacktrace log level = error) To disable stacktrace in logs, one should explicitly provide `--enable-stacktrace=false` flag or set corresponding env var (`ENABLE_STACKTRACE=false`). Fixed: - fallback to default logger, if context does not contain logger - sensitive data in logged tls v1.Secret is now omitted Fixes: #235 --------- Signed-off-by: Artem Bortnikov <artem.bortnikov@telekom.com>
1 parent c1bd6db commit d306349

File tree

8 files changed

+354
-66
lines changed

8 files changed

+354
-66
lines changed

cmd/app/commandline.go

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,17 @@ import (
2828
)
2929

3030
type Flags struct {
31-
Kubeconfig string
32-
MetricsAddress string
33-
ProbeAddress string
34-
LeaderElection bool
35-
SecureMetrics bool
36-
EnableHTTP2 bool
37-
DisableWebhooks bool
38-
LogLevel string
39-
StacktraceLevel string
40-
Dev bool
31+
Kubeconfig string
32+
MetricsAddress string
33+
ProbeAddress string
34+
LeaderElection bool
35+
SecureMetrics bool
36+
EnableHTTP2 bool
37+
DisableWebhooks bool
38+
LogLevel string
39+
StacktraceLevel string
40+
EnableStacktrace bool
41+
Dev bool
4142
}
4243

4344
func ParseCmdLine() Flags {
@@ -54,6 +55,7 @@ func ParseCmdLine() Flags {
5455
pflag.Bool("enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers.")
5556
pflag.Bool("disable-webhooks", false, "If set, the webhooks will be disabled.")
5657
pflag.String("log-level", "info", "Logger verbosity level.Applicable values are debug, info, warn, error.")
58+
pflag.Bool("enable-stacktrace", true, "If set log entries will contain stacktrace.")
5759
pflag.String("stacktrace-level", "error", "Logger level to add stacktrace. "+
5860
"Applicable values are debug, info, warn, error.")
5961
pflag.Bool("dev", false, "development mode.")
@@ -75,16 +77,17 @@ func ParseCmdLine() Flags {
7577
}
7678

7779
return Flags{
78-
Kubeconfig: viper.GetString("kubeconfig"),
79-
MetricsAddress: viper.GetString("metrics-bind-address"),
80-
ProbeAddress: viper.GetString("health-probe-bind-address"),
81-
LeaderElection: viper.GetBool("leader-elect"),
82-
SecureMetrics: viper.GetBool("metrics-secure"),
83-
EnableHTTP2: viper.GetBool("enable-http2"),
84-
DisableWebhooks: viper.GetBool("disable-webhooks"),
85-
LogLevel: viper.GetString("log-level"),
86-
StacktraceLevel: viper.GetString("stacktrace-level"),
87-
Dev: viper.GetBool("dev"),
80+
Kubeconfig: viper.GetString("kubeconfig"),
81+
MetricsAddress: viper.GetString("metrics-bind-address"),
82+
ProbeAddress: viper.GetString("health-probe-bind-address"),
83+
LeaderElection: viper.GetBool("leader-elect"),
84+
SecureMetrics: viper.GetBool("metrics-secure"),
85+
EnableHTTP2: viper.GetBool("enable-http2"),
86+
DisableWebhooks: viper.GetBool("disable-webhooks"),
87+
LogLevel: viper.GetString("log-level"),
88+
StacktraceLevel: viper.GetString("stacktrace-level"),
89+
EnableStacktrace: viper.GetBool("enable-stacktrace"),
90+
Dev: viper.GetBool("dev"),
8891
}
8992
}
9093

@@ -107,8 +110,9 @@ func exitUsage(err error) {
107110

108111
func LogParameters(flags Flags) log.Parameters {
109112
return log.Parameters{
110-
LogLevel: flags.LogLevel,
111-
StacktraceLevel: flags.StacktraceLevel,
112-
Development: flags.Dev,
113+
LogLevel: flags.LogLevel,
114+
StacktraceLevel: flags.StacktraceLevel,
115+
EnableStacktrace: flags.EnableStacktrace,
116+
Development: flags.Dev,
113117
}
114118
}

go.mod

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ require (
1212
github.com/spf13/pflag v1.0.5
1313
github.com/spf13/viper v1.19.0
1414
go.etcd.io/etcd/client/v3 v3.5.14
15-
go.uber.org/zap v1.27.0
16-
go.uber.org/zap/exp v0.2.0
1715
k8s.io/api v0.30.2
1816
k8s.io/apimachinery v0.30.2
1917
k8s.io/client-go v0.30.2
@@ -72,6 +70,7 @@ require (
7270
go.etcd.io/etcd/api/v3 v3.5.14 // indirect
7371
go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect
7472
go.uber.org/multierr v1.11.0 // indirect
73+
go.uber.org/zap v1.27.0 // indirect
7574
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
7675
golang.org/x/net v0.25.0 // indirect
7776
golang.org/x/oauth2 v0.18.0 // indirect

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
167167
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
168168
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
169169
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
170-
go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
171-
go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
172170
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
173171
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
174172
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

internal/controller/etcdcluster_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ func (r *EtcdClusterReconciler) getTLSConfig(ctx context.Context, cluster *etcda
378378
log.Error(ctx, err, "failed to get server trusted CA secret")
379379
return nil, err
380380
}
381-
log.Debug(ctx, "secret read", "server trusted CA secret") // serverCASecret,
381+
log.Debug(ctx, "secret read", "server trusted CA secret", serverCASecret)
382382

383383
caCertPool = x509.NewCertPool()
384384

@@ -398,7 +398,7 @@ func (r *EtcdClusterReconciler) getTLSConfig(ctx context.Context, cluster *etcda
398398
log.Error(ctx, err, "failed to get root client secret")
399399
return nil, err
400400
}
401-
log.Debug(ctx, "secret read", "root client secret") // rootSecret,
401+
log.Debug(ctx, "secret read", "root client secret", rootSecret)
402402

403403
cert, err = tls.X509KeyPair(rootSecret.Data["tls.crt"], rootSecret.Data["tls.key"])
404404
if err != nil {

internal/log/handler.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
Copyright 2024 The etcd-operator Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package log
18+
19+
import (
20+
"context"
21+
"io"
22+
"log/slog"
23+
"os"
24+
"slices"
25+
"strings"
26+
"time"
27+
28+
corev1 "k8s.io/api/core/v1"
29+
)
30+
31+
var sensitiveDataKeys = []string{"key", "crt", "password", "secret", "token"}
32+
33+
const sensitiveDataReplacement = "OMITTED"
34+
35+
type Handler struct {
36+
slog.Handler
37+
writer io.Writer
38+
level slog.Leveler
39+
stacktraceEnabled bool
40+
stacktraceLevel slog.Leveler
41+
}
42+
43+
func NewHandler(opts ...Option) *Handler {
44+
handler := &Handler{
45+
writer: os.Stdout,
46+
level: slog.LevelInfo,
47+
stacktraceLevel: slog.LevelError,
48+
}
49+
applyForHandler(handler, opts...)
50+
handler.Handler = slog.NewJSONHandler(handler.writer, &slog.HandlerOptions{
51+
Level: handler.level,
52+
ReplaceAttr: replaceCommonKeyValues,
53+
})
54+
55+
return handler
56+
}
57+
58+
func (h *Handler) stacktrace(level slog.Level) bool {
59+
return h.stacktraceEnabled && level >= h.stacktraceLevel.Level()
60+
}
61+
62+
func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
63+
return h.Handler.Enabled(ctx, level)
64+
}
65+
66+
func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
67+
if h.stacktrace(r.Level) {
68+
trace := getStacktrace(4)
69+
r.AddAttrs(slog.Attr{Key: "stacktrace", Value: slog.AnyValue(trace)})
70+
}
71+
return h.Handler.Handle(ctx, r)
72+
}
73+
74+
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
75+
redacted := make([]slog.Attr, 0)
76+
for _, attr := range attrs {
77+
redacted = append(redacted, redactAttr(attr))
78+
}
79+
handler := h.clone()
80+
handler.Handler = handler.Handler.WithAttrs(redacted)
81+
return handler
82+
}
83+
84+
func (h *Handler) WithGroup(name string) slog.Handler {
85+
handler := h.clone()
86+
handler.Handler = handler.Handler.WithGroup(name)
87+
return handler
88+
}
89+
90+
func (h *Handler) clone() *Handler {
91+
return &Handler{
92+
Handler: h.Handler,
93+
writer: h.writer,
94+
level: h.level,
95+
stacktraceEnabled: h.stacktraceEnabled,
96+
stacktraceLevel: h.stacktraceLevel,
97+
}
98+
}
99+
100+
func replaceCommonKeyValues(_ []string, a slog.Attr) slog.Attr {
101+
if a.Key == slog.MessageKey {
102+
a.Key = "message"
103+
return a
104+
}
105+
if a.Key == slog.TimeKey {
106+
v := a.Value
107+
t := v.Time().Format(time.RFC3339)
108+
a.Value = slog.StringValue(t)
109+
}
110+
return a
111+
}
112+
113+
func redactAttr(attr slog.Attr) slog.Attr {
114+
// more redacting function could be added
115+
redacted := redactKubernetesSecretAttr(attr)
116+
return redacted
117+
}
118+
119+
func redactKubernetesSecretAttr(attr slog.Attr) slog.Attr {
120+
val := attr.Value.Any()
121+
sec, ok := val.(*corev1.Secret)
122+
if !ok {
123+
return attr
124+
}
125+
res := sec.DeepCopy()
126+
data := res.Data
127+
for k, v := range data {
128+
if !slices.ContainsFunc(sensitiveDataKeys, func(s string) bool {
129+
return strings.Contains(k, s)
130+
}) {
131+
data[k] = v
132+
continue
133+
}
134+
data[k] = []byte(sensitiveDataReplacement)
135+
}
136+
137+
res.Data = data
138+
return slog.Attr{
139+
Key: attr.Key,
140+
Value: slog.AnyValue(res),
141+
}
142+
}

internal/log/logger.go

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,28 @@ import (
2222
"os"
2323

2424
"github.com/go-logr/logr"
25-
"go.uber.org/zap"
26-
"go.uber.org/zap/exp/zapslog"
27-
"go.uber.org/zap/zapcore"
2825
)
2926

30-
func mapLogLevel(level string) zapcore.Level {
27+
func mapLogLevel(level string) slog.Level {
3128
switch level {
3229
case "debug":
33-
return zapcore.DebugLevel
30+
return slog.LevelDebug
3431
case "info":
35-
return zapcore.InfoLevel
32+
return slog.LevelInfo
3633
case "warn":
37-
return zapcore.WarnLevel
34+
return slog.LevelWarn
3835
case "error":
39-
return zapcore.ErrorLevel
36+
return slog.LevelError
4037
default:
41-
return zapcore.InfoLevel
38+
return slog.LevelInfo
4239
}
4340
}
4441

4542
type Parameters struct {
46-
LogLevel string
47-
StacktraceLevel string
48-
Development bool
43+
LogLevel string
44+
StacktraceLevel string
45+
EnableStacktrace bool
46+
Development bool
4947
}
5048

5149
// Setup initializes the logger and returns a new context with the logger attached.
@@ -63,52 +61,47 @@ type Parameters struct {
6361
// Development: true,
6462
// })
6563
func Setup(ctx context.Context, p Parameters) context.Context {
66-
encoderConfig := zapcore.EncoderConfig{
67-
MessageKey: "message",
68-
LevelKey: "level",
69-
TimeKey: "time",
70-
CallerKey: "caller",
71-
StacktraceKey: "stacktrace",
72-
EncodeLevel: zapcore.CapitalLevelEncoder,
73-
EncodeTime: zapcore.RFC3339TimeEncoder,
74-
EncodeDuration: zapcore.StringDurationEncoder,
75-
EncodeCaller: zapcore.ShortCallerEncoder,
76-
EncodeName: zapcore.FullNameEncoder,
77-
}
78-
encoder := zapcore.NewJSONEncoder(encoderConfig)
79-
writer := os.Stderr
64+
w := os.Stderr
8065
if p.Development {
81-
encoder = zapcore.NewConsoleEncoder(encoderConfig)
82-
writer = os.Stdout
66+
w = os.Stdout
8367
}
84-
85-
core := zapcore.NewCore(encoder, writer, mapLogLevel(p.LogLevel))
86-
logger := zap.New(core, zap.AddStacktrace(mapLogLevel(p.StacktraceLevel)))
87-
l := slog.New(zapslog.NewHandler(logger.Core(), &zapslog.HandlerOptions{AddSource: p.Development}))
68+
handler := NewHandler(
69+
WithWriter(w),
70+
WithLevel(mapLogLevel(p.LogLevel)),
71+
WithStacktrace(p.EnableStacktrace, mapLogLevel(p.StacktraceLevel)))
72+
l := slog.New(handler)
8873
return logr.NewContextWithSlogLogger(ctx, l)
8974
}
9075

9176
// Info logs an informational message with optional key-value pairs.
9277
func Info(ctx context.Context, msg string, keysAndValues ...interface{}) {
93-
logr.FromContextAsSlogLogger(ctx).With(keysAndValues...).Info(msg)
78+
fromContextOrDefault(ctx).With(keysAndValues...).Info(msg)
9479
}
9580

9681
// Debug logs a debug message with optional key-value pairs.
9782
func Debug(ctx context.Context, msg string, keysAndValues ...interface{}) {
98-
logr.FromContextAsSlogLogger(ctx).With(keysAndValues...).Debug(msg)
83+
fromContextOrDefault(ctx).With(keysAndValues...).Debug(msg)
9984
}
10085

10186
// Warn logs a warning message with optional key-value pairs.
10287
func Warn(ctx context.Context, msg string, keysAndValues ...interface{}) {
103-
logr.FromContextAsSlogLogger(ctx).With(keysAndValues...).Warn(msg)
88+
fromContextOrDefault(ctx).With(keysAndValues...).Warn(msg)
10489
}
10590

10691
// Error logs an error message with optional key-value pairs.
10792
func Error(ctx context.Context, err error, msg string, keysAndValues ...interface{}) {
108-
logr.FromContextAsSlogLogger(ctx).With(keysAndValues...).Error(msg, slog.Any("error", err))
93+
fromContextOrDefault(ctx).With(keysAndValues...).Error(msg, slog.Any("error", err))
10994
}
11095

11196
// WithValues adds additional key-value pairs to the context's logger.
11297
func WithValues(ctx context.Context, keysAndValues ...interface{}) context.Context {
113-
return logr.NewContextWithSlogLogger(ctx, logr.FromContextAsSlogLogger(ctx).With(keysAndValues...))
98+
return logr.NewContextWithSlogLogger(ctx, fromContextOrDefault(ctx).With(keysAndValues...))
99+
}
100+
101+
func fromContextOrDefault(ctx context.Context) *slog.Logger {
102+
var l *slog.Logger
103+
if l = logr.FromContextAsSlogLogger(ctx); l == nil {
104+
l = slog.Default()
105+
}
106+
return l
114107
}

0 commit comments

Comments
 (0)