diff --git a/receiver/prometheusreceiver/internal/logger.go b/receiver/prometheusreceiver/internal/logger.go index fd0a4404506..6917331e214 100644 --- a/receiver/prometheusreceiver/internal/logger.go +++ b/receiver/prometheusreceiver/internal/logger.go @@ -16,9 +16,15 @@ package internal import ( gokitLog "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" "go.uber.org/zap" ) +const ( + levelKey = "level" + msgKey = "msg" +) + // NewZapToGokitLogAdapter create an adapter for zap.Logger to gokitLog.Logger func NewZapToGokitLogAdapter(logger *zap.Logger) gokitLog.Logger { // need to skip two levels in order to get the correct caller @@ -31,10 +37,22 @@ type zapToGokitLogAdapter struct { l *zap.SugaredLogger } +type logData struct { + level level.Value + msg string + otherFields []interface{} +} + func (w *zapToGokitLogAdapter) Log(keyvals ...interface{}) error { + // expecting key value pairs, the number of items need to be even if len(keyvals)%2 == 0 { - // expecting key value pairs, the number of items need to be even - w.l.Infow("", keyvals...) + // Extract log level and message and log them using corresponding zap function + ld := extractLogData(keyvals) + logFunc, err := levelToFunc(w.l, ld.level) + if err != nil { + return err + } + logFunc(ld.msg, ld.otherFields...) } else { // in case something goes wrong w.l.Info(keyvals...) @@ -42,4 +60,80 @@ func (w *zapToGokitLogAdapter) Log(keyvals ...interface{}) error { return nil } +func extractLogData(keyvals []interface{}) *logData { + lvl := level.InfoValue() // default + msg := "" + + other := make([]interface{}, 0, len(keyvals)) + for i := 0; i < len(keyvals); i = i + 2 { + key := keyvals[i] + val := keyvals[i+1] + + if l, ok := matchLogLevel(key, val); ok { + lvl = l + continue + } + + if m, ok := matchLogMessage(key, val); ok { + msg = m + continue + } + + other = append(other, key, val) + } + + return &logData{ + level: lvl, + msg: msg, + otherFields: other, + } +} + +// check if a given key-value pair represents go-kit log message and return it +func matchLogMessage(key interface{}, val interface{}) (string, bool) { + strKey, ok := key.(string) + if !ok || strKey != msgKey { + return "", false + } + + msg, ok := val.(string) + if !ok { + return "", false + } + + return msg, true +} + +// check if a given key-value pair represents go-kit log level and return it +func matchLogLevel(key interface{}, val interface{}) (level.Value, bool) { + strKey, ok := key.(string) + if !ok || strKey != levelKey { + return nil, false + } + + levelVal, ok := val.(level.Value) + if !ok { + return nil, false + } + + return levelVal, true +} + +// find a matching zap logging function to be used for a given level +func levelToFunc(logger *zap.SugaredLogger, lvl level.Value) (func(string, ...interface{}), error) { + switch lvl { + case level.DebugValue(): + return logger.Debugw, nil + case level.InfoValue(): + return logger.Infow, nil + case level.WarnValue(): + return logger.Warnw, nil + case level.ErrorValue(): + return logger.Errorw, nil + } + + // default + return logger.Infof, nil +} + var _ gokitLog.Logger = (*zapToGokitLogAdapter)(nil) diff --git a/receiver/prometheusreceiver/internal/logger_test.go b/receiver/prometheusreceiver/internal/logger_test.go new file mode 100644 index 00000000000..47ba3046e61 --- /dev/null +++ b/receiver/prometheusreceiver/internal/logger_test.go @@ -0,0 +1,197 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "testing" + + "github.com/go-kit/kit/log/level" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func TestLog(t *testing.T) { + tcs := []struct { + name string + input []interface{} + wantLevel zapcore.Level + wantMessage string + }{ + { + name: "Starting provider", + input: []interface{}{ + "level", + level.DebugValue(), + "msg", + "Starting provider", + "provider", + "string/0", + "subs", + "[target1]", + }, + wantLevel: zapcore.DebugLevel, + wantMessage: "Starting provider", + }, + { + name: "Scrape failed", + input: []interface{}{ + "level", + level.ErrorValue(), + "scrape_pool", + "target1", + "msg", + "Scrape failed", + "err", + "server returned HTTP status 500 Internal Server Error", + }, + wantLevel: zapcore.ErrorLevel, + wantMessage: "Scrape failed", + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + conf := zap.NewProductionConfig() + conf.Level.SetLevel(zapcore.DebugLevel) + + // capture zap log entry + var entry zapcore.Entry + h := func(e zapcore.Entry) error { + entry = e + return nil + } + + logger, err := conf.Build(zap.Hooks(h)) + require.NoError(t, err) + + adapter := NewZapToGokitLogAdapter(logger) + err = adapter.Log(tc.input...) + require.NoError(t, err) + + assert.Equal(t, tc.wantLevel, entry.Level) + assert.Equal(t, tc.wantMessage, entry.Message) + }) + } +} + +func TestExtractLogData(t *testing.T) { + tcs := []struct { + name string + input []interface{} + wantLevel level.Value + wantMessage string + wantOutput []interface{} + }{ + { + name: "nil fields", + input: nil, + wantLevel: level.InfoValue(), // Default + wantMessage: "", + wantOutput: []interface{}{}, + }, + { + name: "empty fields", + input: []interface{}{}, + wantLevel: level.InfoValue(), // Default + wantMessage: "", + wantOutput: []interface{}{}, + }, + { + name: "info level", + input: []interface{}{ + "level", + level.InfoValue(), + }, + wantLevel: level.InfoValue(), + wantMessage: "", + wantOutput: []interface{}{}, + }, + { + name: "warn level", + input: []interface{}{ + "level", + level.WarnValue(), + }, + wantLevel: level.WarnValue(), + wantMessage: "", + wantOutput: []interface{}{}, + }, + { + name: "error level", + input: []interface{}{ + "level", + level.ErrorValue(), + }, + wantLevel: level.ErrorValue(), + wantMessage: "", + wantOutput: []interface{}{}, + }, + { + name: "debug level + extra fields", + input: []interface{}{ + "timestamp", + 1596604719, + "level", + level.DebugValue(), + "msg", + "http client error", + }, + wantLevel: level.DebugValue(), + wantMessage: "http client error", + wantOutput: []interface{}{ + "timestamp", + 1596604719, + }, + }, + { + name: "missing level field", + input: []interface{}{ + "timestamp", + 1596604719, + "msg", + "http client error", + }, + wantLevel: level.InfoValue(), // Default + wantMessage: "http client error", + wantOutput: []interface{}{ + "timestamp", + 1596604719, + }, + }, + { + name: "invalid level type", + input: []interface{}{ + "level", + "warn", // String is not recognized + }, + wantLevel: level.InfoValue(), // Default + wantOutput: []interface{}{ + "level", + "warn", // Field is preserved + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + ld := extractLogData(tc.input) + assert.Equal(t, tc.wantLevel, ld.level) + assert.Equal(t, tc.wantMessage, ld.msg) + assert.Equal(t, tc.wantOutput, ld.otherFields) + }) + } +}