Allow logging of caller info (#4731)
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
This commit extends our logging wrapper code to allow the addition of automatically generated caller information to log entries. It also adds a way to selectively suppress this caller information for individual sinks. This commit does not change any of trufflehog's behavior - it just enables future functionality in case we want it to use it later. This commit also provides an exportable type alias for sink configuration option functions so that callers have more flexibility in how they prepare groups of them. This commit also adds some new tests for the existing redactionCore and fixes an existing bug in it. (The bug hasn't yet caused problems, but it created the potential for future problems.)
This commit is contained in:
+51
-18
@@ -22,10 +22,19 @@ type logConfig struct {
|
||||
err error
|
||||
}
|
||||
|
||||
type SinkOption func(*sinkConfig)
|
||||
|
||||
// New creates a new log object with the provided configurations. If no sinks
|
||||
// are provided, a no-op sink will be used. Returns the logger and a cleanup
|
||||
// function that should be executed before the program exits.
|
||||
func New(service string, configs ...logConfig) (logr.Logger, func() error) {
|
||||
return NewWithCaller(service, false, configs...)
|
||||
}
|
||||
|
||||
// NewWithCaller creates a new logger named after the specified service with the provided sink configurations. If
|
||||
// addCaller is true, call site information will be attached to each emitted log message. (This behavior can be disabled
|
||||
// on a per-sink basis using WithSuppressCaller.)
|
||||
func NewWithCaller(service string, addCaller bool, configs ...logConfig) (logr.Logger, func() error) {
|
||||
var cores []zapcore.Core
|
||||
var cleanupFuncs []func() error
|
||||
|
||||
@@ -40,7 +49,7 @@ func New(service string, configs ...logConfig) (logr.Logger, func() error) {
|
||||
}
|
||||
}
|
||||
// create logger
|
||||
zapLogger := zap.New(zapcore.NewTee(cores...))
|
||||
zapLogger := zap.New(zapcore.NewTee(cores...), zap.WithCaller(addCaller))
|
||||
cleanupFuncs = append(cleanupFuncs, zapLogger.Sync)
|
||||
logger := zapr.NewLogger(zapLogger).WithName(service)
|
||||
|
||||
@@ -85,14 +94,15 @@ func WithSentry(opts sentry.ClientOptions, tags map[string]string) logConfig {
|
||||
}
|
||||
|
||||
type sinkConfig struct {
|
||||
encoder zapcore.Encoder
|
||||
sink zapcore.WriteSyncer
|
||||
level levelSetter
|
||||
redactor *dynamicRedactor
|
||||
encoder zapcore.Encoder
|
||||
sink zapcore.WriteSyncer
|
||||
level levelSetter
|
||||
redactor *dynamicRedactor
|
||||
suppressCaller bool
|
||||
}
|
||||
|
||||
// WithJSONSink adds a JSON encoded output to the logger.
|
||||
func WithJSONSink(sink io.Writer, opts ...func(*sinkConfig)) logConfig {
|
||||
func WithJSONSink(sink io.Writer, opts ...SinkOption) logConfig {
|
||||
return newCoreConfig(
|
||||
zapcore.NewJSONEncoder(defaultEncoderConfig()),
|
||||
zapcore.Lock(zapcore.AddSync(sink)),
|
||||
@@ -102,7 +112,7 @@ func WithJSONSink(sink io.Writer, opts ...func(*sinkConfig)) logConfig {
|
||||
}
|
||||
|
||||
// WithConsoleSink adds a console-style output to the logger.
|
||||
func WithConsoleSink(sink io.Writer, opts ...func(*sinkConfig)) logConfig {
|
||||
func WithConsoleSink(sink io.Writer, opts ...SinkOption) logConfig {
|
||||
return newCoreConfig(
|
||||
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
|
||||
zapcore.Lock(zapcore.AddSync(sink)),
|
||||
@@ -162,10 +172,21 @@ func AddSink(l logr.Logger, sink logConfig, keysAndValues ...any) (logr.Logger,
|
||||
if err != nil {
|
||||
return l, nil, errors.New("unsupported logr implementation")
|
||||
}
|
||||
zapLogger = zapLogger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
|
||||
return zapcore.NewTee(core, newSinkCore)
|
||||
}))
|
||||
return zapr.NewLogger(zapLogger), firstErrorFunc(zapLogger.Sync, sink.cleanup), nil
|
||||
|
||||
newLoggerOptions := []zap.Option{
|
||||
// Tee the new core together with the original core
|
||||
zap.WrapCore(func(core zapcore.Core) zapcore.Core { return zapcore.NewTee(core, newSinkCore) }),
|
||||
|
||||
// CMR: zapr.NewLogger, for whatever reason, assumes that the passed-in logger doesn't have its caller frame
|
||||
// adjustment already set up, so it adds a frame skip of 2. However, that assumption doesn't hold here because
|
||||
// we're adding a core to an existing logger rather than creating a new one. I can't figure out a way to disable
|
||||
// this automatic frame adjustment, so we compensate for it with the hamfisted kludge of a compensating offset.
|
||||
zap.AddCallerSkip(-2),
|
||||
}
|
||||
|
||||
zapLogger = zapLogger.WithOptions(newLoggerOptions...)
|
||||
newLogger := zapr.NewLogger(zapLogger)
|
||||
return newLogger, firstErrorFunc(zapLogger.Sync, sink.cleanup), nil
|
||||
}
|
||||
|
||||
// getZapLogger is a helper function that gets the underlying zap logger from a
|
||||
@@ -179,7 +200,7 @@ func getZapLogger(l logr.Logger) (*zap.Logger, error) {
|
||||
|
||||
// WithLevel sets the sink's level to a static level. This option prevents
|
||||
// changing the log level for this sink later on.
|
||||
func WithLevel(level int8) func(*sinkConfig) {
|
||||
func WithLevel(level int8) SinkOption {
|
||||
return WithLeveler(
|
||||
// Zap's levels get more verbose as the number gets smaller, as explained
|
||||
// by zapr here: https://github.com/go-logr/zapr#increasing-verbosity
|
||||
@@ -189,19 +210,27 @@ func WithLevel(level int8) func(*sinkConfig) {
|
||||
}
|
||||
|
||||
// WithLeveler sets the sink's level enabler to leveler.
|
||||
func WithLeveler(leveler levelSetter) func(*sinkConfig) {
|
||||
func WithLeveler(leveler levelSetter) SinkOption {
|
||||
return func(conf *sinkConfig) {
|
||||
conf.level = leveler
|
||||
}
|
||||
}
|
||||
|
||||
// WithGlobalRedaction adds values to be redacted from logs.
|
||||
func WithGlobalRedaction() func(*sinkConfig) {
|
||||
func WithGlobalRedaction() SinkOption {
|
||||
return func(conf *sinkConfig) {
|
||||
conf.redactor = globalRedactor
|
||||
}
|
||||
}
|
||||
|
||||
// WithSuppressCaller prevents the sink being configured from logging any caller information, irrespective of any other
|
||||
// logger settings.
|
||||
func WithSuppressCaller() SinkOption {
|
||||
return func(conf *sinkConfig) {
|
||||
conf.suppressCaller = true
|
||||
}
|
||||
}
|
||||
|
||||
// ToLogger converts the logr.Logger into a legacy *log.Logger.
|
||||
func ToLogger(l logr.Logger) *log.Logger {
|
||||
return slog.NewLogLogger(logr.ToSlogHandler(l), slog.LevelInfo)
|
||||
@@ -235,7 +264,7 @@ func newCoreConfig(
|
||||
defaultEncoder zapcore.Encoder,
|
||||
defaultSink zapcore.WriteSyncer,
|
||||
defaultLevel levelSetter,
|
||||
opts ...func(*sinkConfig),
|
||||
opts ...SinkOption,
|
||||
) logConfig {
|
||||
conf := sinkConfig{
|
||||
encoder: defaultEncoder,
|
||||
@@ -251,9 +280,13 @@ func newCoreConfig(
|
||||
conf.level,
|
||||
)
|
||||
|
||||
if conf.redactor == nil {
|
||||
return logConfig{core: core}
|
||||
if conf.redactor != nil {
|
||||
core = NewRedactionCore(core, conf.redactor)
|
||||
}
|
||||
|
||||
return logConfig{core: NewRedactionCore(core, conf.redactor)}
|
||||
if conf.suppressCaller {
|
||||
core = &suppressCallerCore{Core: core}
|
||||
}
|
||||
|
||||
return logConfig{core: core}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-logr/zapr"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// ignoreEverythingCore is a zap core that rejects all log entries. It is used to test that our custom cores respect
|
||||
// wrapped core check logic.
|
||||
type ignoreEverythingCore struct {
|
||||
zapcore.Core
|
||||
}
|
||||
|
||||
func (t *ignoreEverythingCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return &ignoreEverythingCore{t.Core.With(fields)}
|
||||
}
|
||||
|
||||
func (t *ignoreEverythingCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
return ce
|
||||
}
|
||||
|
||||
var _ zapcore.Core = (*ignoreEverythingCore)(nil)
|
||||
|
||||
type TestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
oldRedactor *dynamicRedactor
|
||||
}
|
||||
|
||||
func (ts *TestSuite) SetupTest() {
|
||||
ts.oldRedactor = globalRedactor
|
||||
globalRedactor = &dynamicRedactor{
|
||||
denySet: make(map[string]struct{}),
|
||||
}
|
||||
globalRedactor.replacer.Store(strings.NewReplacer())
|
||||
}
|
||||
|
||||
func (ts *TestSuite) TearDownTest() {
|
||||
globalRedactor = ts.oldRedactor
|
||||
}
|
||||
|
||||
func TestCustomCores(t *testing.T) {
|
||||
suite.Run(t, new(TestSuite))
|
||||
}
|
||||
|
||||
// This test confirms that when our custom redaction core wraps our custom caller suppression core, both redaction and
|
||||
// caller suppression occur.
|
||||
func (ts *TestSuite) TestCoreComposition_RedactionWrappingCallerSuppression() {
|
||||
// Arrange: Create a testable log sink
|
||||
var buf bytes.Buffer
|
||||
baseCore := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(defaultEncoderConfig()),
|
||||
zapcore.Lock(zapcore.AddSync(&buf)),
|
||||
globalLogLevel)
|
||||
|
||||
// Arrange: Create a core stack and set up redaction
|
||||
core := NewRedactionCore(&suppressCallerCore{baseCore}, globalRedactor)
|
||||
RedactGlobally("sensitive")
|
||||
|
||||
// Arrange: Create a logger
|
||||
logger := zapr.NewLogger(zap.New(core, zap.AddCaller()))
|
||||
|
||||
// Act
|
||||
logger.Info("sensitive message")
|
||||
|
||||
// Assert that redaction executed correctly
|
||||
msg := buf.String()
|
||||
ts.Assert().Contains(msg, "message")
|
||||
ts.Assert().NotContains(msg, "sensitive")
|
||||
ts.Assert().NotContains(msg, "caller")
|
||||
}
|
||||
|
||||
// This test confirms that when our custom caller suppression core wraps our custom redaction core, both redaction and
|
||||
// caller suppression occur.
|
||||
func (ts *TestSuite) TestCoreComposition_CallerSuppressionWrappingRedaction() {
|
||||
// Arrange: Create a testable log sink
|
||||
var buf bytes.Buffer
|
||||
baseCore := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(defaultEncoderConfig()),
|
||||
zapcore.Lock(zapcore.AddSync(&buf)),
|
||||
globalLogLevel)
|
||||
|
||||
// Arrange: Create a core stack and set up redaction
|
||||
core := &suppressCallerCore{NewRedactionCore(baseCore, globalRedactor)}
|
||||
RedactGlobally("sensitive")
|
||||
|
||||
// Arrange: Create a logger
|
||||
logger := zapr.NewLogger(zap.New(core, zap.AddCaller()))
|
||||
|
||||
// Act
|
||||
logger.Info("sensitive message")
|
||||
|
||||
// Assert that redaction executed correctly
|
||||
msg := buf.String()
|
||||
ts.Assert().Contains(msg, "message")
|
||||
ts.Assert().NotContains(msg, "sensitive")
|
||||
ts.Assert().NotContains(msg, "caller")
|
||||
}
|
||||
|
||||
// This test confirms that the redaction logic executes correctly for console sinks.
|
||||
func (ts *TestSuite) TestGlobalRedaction_Console() {
|
||||
var buf bytes.Buffer
|
||||
logger, flush := New("console-redaction-test",
|
||||
WithConsoleSink(&buf, WithGlobalRedaction()),
|
||||
)
|
||||
RedactGlobally("foo")
|
||||
RedactGlobally("bar")
|
||||
|
||||
logger.Info("this foo is :bar",
|
||||
"foo", "bar",
|
||||
"array", []string{"foo", "bar", "baz"},
|
||||
"object", map[string]string{"foo": "bar"})
|
||||
ts.Require().NoError(flush())
|
||||
|
||||
gotParts := strings.Split(buf.String(), "\t")[1:] // The first item is the timestamp
|
||||
wantParts := []string{
|
||||
"info-0",
|
||||
"console-redaction-test",
|
||||
"this ***** is :*****",
|
||||
"{\"foo\": \"*****\", \"array\": [\"foo\", \"bar\", \"baz\"], \"object\": {\"foo\":\"bar\"}}\n",
|
||||
}
|
||||
ts.Assert().Equal(wantParts, gotParts)
|
||||
}
|
||||
|
||||
// This test confirms that the redaction logic executes correctly for JSON sinks.
|
||||
func (ts *TestSuite) TestGlobalRedaction_JSON() {
|
||||
var jsonBuffer bytes.Buffer
|
||||
logger, flush := New("json-redaction-test",
|
||||
WithJSONSink(&jsonBuffer, WithGlobalRedaction()),
|
||||
)
|
||||
RedactGlobally("foo")
|
||||
RedactGlobally("bar")
|
||||
|
||||
logger.Info("this foo is :bar",
|
||||
"foo", "bar",
|
||||
"array", []string{"foo", "bar", "baz"},
|
||||
"object", map[string]string{"foo": "bar"})
|
||||
ts.Require().NoError(flush())
|
||||
|
||||
var parsedJSON map[string]any
|
||||
ts.Require().NoError(json.Unmarshal(jsonBuffer.Bytes(), &parsedJSON))
|
||||
ts.Assert().NotEmpty(parsedJSON["ts"])
|
||||
delete(parsedJSON, "ts")
|
||||
ts.Assert().Equal(
|
||||
map[string]any{
|
||||
"level": "info-0",
|
||||
"logger": "json-redaction-test",
|
||||
"msg": "this ***** is :*****",
|
||||
"foo": "*****",
|
||||
"array": []any{"foo", "bar", "baz"},
|
||||
"object": map[string]interface{}{"foo": "bar"},
|
||||
},
|
||||
parsedJSON,
|
||||
)
|
||||
}
|
||||
|
||||
// This test confirms that our custom redaction core respects the "check" logic of any cores it wraps.
|
||||
func (ts *TestSuite) TestRedactionCore_RespectsWrappedCheckLogic() {
|
||||
// Arrange: Create a testable log sink
|
||||
var buf bytes.Buffer
|
||||
baseCore := zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
|
||||
zapcore.Lock(zapcore.AddSync(&buf)),
|
||||
globalLogLevel)
|
||||
|
||||
// Arrange: Set up a core stack
|
||||
core := NewRedactionCore(&ignoreEverythingCore{baseCore}, globalRedactor)
|
||||
|
||||
// Arrange: Create a logger
|
||||
logger := zapr.NewLogger(zap.New(core))
|
||||
|
||||
// Act
|
||||
logger.Info("message")
|
||||
|
||||
// Assert that the wrapped core's check logic was respected
|
||||
msg := buf.String()
|
||||
ts.Assert().Empty(msg)
|
||||
}
|
||||
|
||||
// This test confirms that our custom caller suppression core respects the "check" logic of any cores it wraps.
|
||||
func (ts *TestSuite) TestSuppressCallerCore_RespectsWrappedCheckLogic() {
|
||||
// Arrange: Create a testable log sink
|
||||
var buf bytes.Buffer
|
||||
baseCore := zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
|
||||
zapcore.Lock(zapcore.AddSync(&buf)),
|
||||
globalLogLevel)
|
||||
|
||||
// Arrange: Set up a core stack
|
||||
core := &suppressCallerCore{&ignoreEverythingCore{baseCore}}
|
||||
|
||||
// Arrange: Create a logger
|
||||
logger := zapr.NewLogger(zap.New(core))
|
||||
|
||||
// Act
|
||||
logger.Info("message")
|
||||
|
||||
// Assert that the wrapped core's check logic was respected
|
||||
msg := buf.String()
|
||||
ts.Assert().Empty(msg)
|
||||
}
|
||||
|
||||
// This test confirms that our custom caller suppression core suppresses caller information.
|
||||
func (ts *TestSuite) TestSuppressCallerCore_SuppressesCaller() {
|
||||
// Arrange: Create a testable log sink
|
||||
var buf bytes.Buffer
|
||||
baseCore := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(defaultEncoderConfig()),
|
||||
zapcore.Lock(zapcore.AddSync(&buf)),
|
||||
globalLogLevel)
|
||||
|
||||
// Arrange: Set up a core stack
|
||||
core := &suppressCallerCore{baseCore}
|
||||
|
||||
// Arrange: Create a logger
|
||||
logger := zapr.NewLogger(zap.New(core, zap.AddCaller()))
|
||||
|
||||
// Act
|
||||
logger.Info("message")
|
||||
|
||||
// Assert that caller information was suppressed
|
||||
msg := buf.String()
|
||||
ts.Assert().NotContains(msg, "caller")
|
||||
}
|
||||
|
||||
func BenchmarkLoggerRedact(b *testing.B) {
|
||||
msg := "this is a message with 'foo' in it"
|
||||
logKvps := []any{"key", "value", "foo", "bar", "bar", "baz", "longval", "84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa"}
|
||||
redactor := &dynamicRedactor{denySet: make(map[string]struct{})}
|
||||
redactor.replacer.CompareAndSwap(nil, strings.NewReplacer())
|
||||
|
||||
b.Run("no redaction", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
b.Run("1 redaction", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
b.Run("2 redactions", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
|
||||
redactor.configureForRedaction("foo")
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
b.Run("3 redactions", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
|
||||
redactor.configureForRedaction("foo")
|
||||
redactor.configureForRedaction("bar")
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package log
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -247,72 +246,6 @@ func splitLines(s string) []string {
|
||||
return logLines
|
||||
}
|
||||
|
||||
func TestGlobalRedaction_Console(t *testing.T) {
|
||||
oldState := globalRedactor
|
||||
globalRedactor = &dynamicRedactor{
|
||||
denySet: make(map[string]struct{}),
|
||||
}
|
||||
defer func() { globalRedactor = oldState }()
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger, flush := New("console-redaction-test",
|
||||
WithConsoleSink(&buf, WithGlobalRedaction()),
|
||||
)
|
||||
RedactGlobally("foo")
|
||||
RedactGlobally("bar")
|
||||
|
||||
logger.Info("this foo is :bar",
|
||||
"foo", "bar",
|
||||
"array", []string{"foo", "bar", "baz"},
|
||||
"object", map[string]string{"foo": "bar"})
|
||||
require.NoError(t, flush())
|
||||
|
||||
gotParts := strings.Split(buf.String(), "\t")[1:] // The first item is the timestamp
|
||||
wantParts := []string{
|
||||
"info-0",
|
||||
"console-redaction-test",
|
||||
"this ***** is :*****",
|
||||
"{\"foo\": \"*****\", \"array\": [\"foo\", \"bar\", \"baz\"], \"object\": {\"foo\":\"bar\"}}\n",
|
||||
}
|
||||
assert.Equal(t, wantParts, gotParts)
|
||||
}
|
||||
|
||||
func TestGlobalRedaction_JSON(t *testing.T) {
|
||||
oldState := globalRedactor
|
||||
globalRedactor = &dynamicRedactor{
|
||||
denySet: make(map[string]struct{}),
|
||||
}
|
||||
defer func() { globalRedactor = oldState }()
|
||||
|
||||
var jsonBuffer bytes.Buffer
|
||||
logger, flush := New("json-redaction-test",
|
||||
WithJSONSink(&jsonBuffer, WithGlobalRedaction()),
|
||||
)
|
||||
RedactGlobally("foo")
|
||||
RedactGlobally("bar")
|
||||
logger.Info("this foo is :bar",
|
||||
"foo", "bar",
|
||||
"array", []string{"foo", "bar", "baz"},
|
||||
"object", map[string]string{"foo": "bar"})
|
||||
require.NoError(t, flush())
|
||||
|
||||
var parsedJSON map[string]any
|
||||
require.NoError(t, json.Unmarshal(jsonBuffer.Bytes(), &parsedJSON))
|
||||
assert.NotEmpty(t, parsedJSON["ts"])
|
||||
delete(parsedJSON, "ts")
|
||||
assert.Equal(t,
|
||||
map[string]any{
|
||||
"level": "info-0",
|
||||
"logger": "json-redaction-test",
|
||||
"msg": "this ***** is :*****",
|
||||
"foo": "*****",
|
||||
"array": []any{"foo", "bar", "baz"},
|
||||
"object": map[string]interface{}{"foo": "bar"},
|
||||
},
|
||||
parsedJSON,
|
||||
)
|
||||
}
|
||||
|
||||
func TestToLogger(t *testing.T) {
|
||||
var jsonBuffer bytes.Buffer
|
||||
l, flush := New("service-name",
|
||||
@@ -358,57 +291,3 @@ func TestToSlogger(t *testing.T) {
|
||||
parsedJSON,
|
||||
)
|
||||
}
|
||||
|
||||
func BenchmarkLoggerRedact(b *testing.B) {
|
||||
msg := "this is a message with 'foo' in it"
|
||||
logKvps := []any{"key", "value", "foo", "bar", "bar", "baz", "longval", "84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa"}
|
||||
redactor := &dynamicRedactor{denySet: make(map[string]struct{})}
|
||||
redactor.replacer.CompareAndSwap(nil, strings.NewReplacer())
|
||||
|
||||
b.Run("no redaction", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
b.Run("1 redaction", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
b.Run("2 redactions", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
|
||||
redactor.configureForRedaction("foo")
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
b.Run("3 redactions", func(b *testing.B) {
|
||||
logger, flush := New("redaction-benchmark", WithJSONSink(
|
||||
io.Discard,
|
||||
func(conf *sinkConfig) { conf.redactor = redactor },
|
||||
))
|
||||
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
|
||||
redactor.configureForRedaction("foo")
|
||||
redactor.configureForRedaction("bar")
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg, logKvps...)
|
||||
}
|
||||
require.NoError(b, flush())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,15 +17,25 @@ func NewRedactionCore(core zapcore.Core, redactor *dynamicRedactor) zapcore.Core
|
||||
return &redactionCore{core, redactor}
|
||||
}
|
||||
|
||||
// Check overrides the embedded zapcore.Core Check() method to add the
|
||||
// redactionCore to the zapcore.CheckedEntry.
|
||||
// Check determines whether the supplied Entry should be logged and, if it should, adds the core to the entry.
|
||||
func (c *redactionCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if c.Enabled(ent.Level) {
|
||||
if !c.Enabled(ent.Level) {
|
||||
return ce
|
||||
}
|
||||
|
||||
// Check to see whether the wrapped core would write, and if so, add this core to the CheckedEntry. We do not pass
|
||||
// the CheckedEntry directly to the wrapped core, because if we do, the wrapped core will probably add itself as a
|
||||
// side effect, which would result in the wrapped core executing its own writes, which would be both duplicative and
|
||||
// unredacted.
|
||||
if wrapped := c.Core.Check(ent, nil); wrapped != nil {
|
||||
return ce.AddCore(ent, c)
|
||||
}
|
||||
|
||||
return ce
|
||||
}
|
||||
|
||||
// With adds structured context to the Core. It does not do anything interesting and is only implemented at all because
|
||||
// of the way zap works.
|
||||
func (c *redactionCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return NewRedactionCore(c.Core.With(fields), c.redactor)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package log
|
||||
|
||||
import "go.uber.org/zap/zapcore"
|
||||
|
||||
// suppressCallerCore is a zap core that removes caller information from zap entries before writing. It is intended to
|
||||
// be used as a wrapper around particular zap cores that you do not want to write caller information to.
|
||||
//
|
||||
// If you want to exclude caller information from the entire logger, don't use this core - instead, do not enable caller
|
||||
// information at the logger level. (That will be much faster than using this core, because zap will refrain from
|
||||
// generating caller information in the first place, rather than generating it and then throwing it away.)
|
||||
type suppressCallerCore struct {
|
||||
zapcore.Core
|
||||
}
|
||||
|
||||
// Check determines whether the supplied Entry should be logged and, if it should, adds the core to the entry.
|
||||
func (c *suppressCallerCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||
if !c.Enabled(ent.Level) {
|
||||
return ce
|
||||
}
|
||||
|
||||
// Check to see whether the wrapped core would write, and if so, add this core to the CheckedEntry. We do not pass
|
||||
// the CheckedEntry directly to the wrapped core, because if we do, the wrapped core will probably add itself as a
|
||||
// side effect, which would result in the wrapped core executing its own writes, which would be both duplicative and
|
||||
// without caller suppression.
|
||||
if wrapped := c.Core.Check(ent, nil); wrapped != nil {
|
||||
// If we add the wrapped core, then that core will generate its own writes - which won't have caller information
|
||||
// suppressed! Instead, we only add this core, which will delegate its (caller-suppressed) writes to the wrapped
|
||||
// core.
|
||||
return ce.AddCore(ent, c)
|
||||
}
|
||||
|
||||
return ce
|
||||
}
|
||||
|
||||
// With adds structured context to the Core. It does not do anything interesting and is only implemented at all because
|
||||
// of the way zap works.
|
||||
func (c *suppressCallerCore) With(fields []zapcore.Field) zapcore.Core {
|
||||
return &suppressCallerCore{Core: c.Core.With(fields)}
|
||||
}
|
||||
|
||||
// Write removes caller information from a zap entry and then passes it to the wrapped core for writing.
|
||||
func (c *suppressCallerCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
|
||||
ent.Caller = zapcore.EntryCaller{}
|
||||
ent.Stack = ""
|
||||
return c.Core.Write(ent, fields)
|
||||
}
|
||||
Reference in New Issue
Block a user