diff --git a/pkg/utils/logger/logger.go b/pkg/utils/logger/logger.go index b5c4cab2e..5394fb8d4 100644 --- a/pkg/utils/logger/logger.go +++ b/pkg/utils/logger/logger.go @@ -9,11 +9,35 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" ) +// severityLevelEncoder encodes log levels to cloud-compatible severity names. +// Uses "WARNING" instead of zap's "WARN", and maps DPANIC/PANIC/FATAL to CRITICAL. +func severityLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + switch level { + case zapcore.WarnLevel: + enc.AppendString("WARNING") + case zapcore.DPanicLevel, zapcore.PanicLevel, zapcore.FatalLevel: + enc.AppendString("CRITICAL") + default: + enc.AppendString(level.CapitalString()) + } +} + func NewLogger() logr.Logger { opts := zap.Options{ - Development: true, + // Use production mode for JSON output (enables structured logging in cloud environments). + // Can be overridden with --zap-devel flag for local development. + Development: false, StacktraceLevel: zapcore.DPanicLevel, TimeEncoder: zapcore.RFC3339TimeEncoder, + // Configure encoder for cloud logging compatibility: + // - Use "severity" field name (recognized by GKE, AKS, EKS log explorers) + // - Use standard severity names (WARNING instead of WARN, CRITICAL for fatal errors) + EncoderConfigOptions: []zap.EncoderConfigOption{ + func(ec *zapcore.EncoderConfig) { + ec.LevelKey = "severity" + ec.EncodeLevel = severityLevelEncoder + }, + }, } return zap.New(zap.UseFlagOptions(&opts)) } diff --git a/pkg/utils/logger/logger_test.go b/pkg/utils/logger/logger_test.go new file mode 100644 index 000000000..8c98512a1 --- /dev/null +++ b/pkg/utils/logger/logger_test.go @@ -0,0 +1,70 @@ +// Copyright Mondoo, Inc. 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package logger + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zapcore" +) + +type testEncoder struct { + result string +} + +func (t *testEncoder) AppendString(s string) { + t.result = s +} + +// Implement remaining PrimitiveArrayEncoder methods as no-ops +func (t *testEncoder) AppendBool(bool) {} +func (t *testEncoder) AppendByteString([]byte) {} +func (t *testEncoder) AppendComplex128(complex128) {} +func (t *testEncoder) AppendComplex64(complex64) {} +func (t *testEncoder) AppendFloat64(float64) {} +func (t *testEncoder) AppendFloat32(float32) {} +func (t *testEncoder) AppendInt(int) {} +func (t *testEncoder) AppendInt64(int64) {} +func (t *testEncoder) AppendInt32(int32) {} +func (t *testEncoder) AppendInt16(int16) {} +func (t *testEncoder) AppendInt8(int8) {} +func (t *testEncoder) AppendUint(uint) {} +func (t *testEncoder) AppendUint64(uint64) {} +func (t *testEncoder) AppendUint32(uint32) {} +func (t *testEncoder) AppendUint16(uint16) {} +func (t *testEncoder) AppendUint8(uint8) {} +func (t *testEncoder) AppendUintptr(uintptr) {} + +func TestSeverityLevelEncoder(t *testing.T) { + tests := []struct { + level zapcore.Level + expected string + }{ + {zapcore.DebugLevel, "DEBUG"}, + {zapcore.InfoLevel, "INFO"}, + {zapcore.WarnLevel, "WARNING"}, // Cloud log explorers expect WARNING, not WARN + {zapcore.ErrorLevel, "ERROR"}, + {zapcore.DPanicLevel, "CRITICAL"}, + {zapcore.PanicLevel, "CRITICAL"}, + {zapcore.FatalLevel, "CRITICAL"}, + } + + for _, tt := range tests { + t.Run(tt.level.String(), func(t *testing.T) { + enc := &testEncoder{} + severityLevelEncoder(tt.level, enc) + assert.Equal(t, tt.expected, enc.result) + }) + } +} + +func TestNewLogger(t *testing.T) { + // Ensure NewLogger creates a valid logger without panicking + logger := NewLogger() + assert.NotNil(t, logger) + + // Test that the logger can be used + logger.Info("test message", "key", "value") +}