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()) }) }