Files
atlantis/server/logging/simple_logger.go

217 lines
6.0 KiB
Go

// Copyright 2017 HootSuite Media Inc.
//
// 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.
// Modified hereafter by contributors to runatlantis/atlantis.
//
// Package logging handles logging throughout Atlantis.
package logging
import (
"bytes"
"fmt"
"github.com/pkg/errors"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest"
)
//go:generate pegomock generate --package mocks -o mocks/mock_simple_logging.go SimpleLogging
// SimpleLogging is the interface used for logging throughout the codebase.
type SimpleLogging interface {
// These basically just fmt.Sprintf() the message and args.
Debug(format string, a ...interface{})
Info(format string, a ...interface{})
Warn(format string, a ...interface{})
Err(format string, a ...interface{})
Log(level LogLevel, format string, a ...interface{})
SetLevel(lvl LogLevel)
// With adds a variadic number of fields to the logging context. It accepts a
// mix of strongly-typed Field objects and loosely-typed key-value pairs. When
// processing pairs, the first element of the pair is used as the field key
// and the second as the field value.
With(a ...interface{}) SimpleLogging
// Creates a new logger with history preserved . log storage + search strategies
// should ideally be used instead of managing this ourselves.
// keeping as a separate method to ensure that usage of history is completely intentional
WithHistory(a ...interface{}) SimpleLogging
// Fetches the history we've stored associated with the logging context
GetHistory() string
// Flushes anything left in the buffer
Flush() error
}
type StructuredLogger struct {
z *zap.SugaredLogger
level zap.AtomicLevel
keepHistory bool
// History stores all log entries ever written using
// this logger. This is safe for short-lived loggers
// like those used during plan/apply commands.
// TODO: Deprecate this
// this is added here to maintain backwards compatibility
// This doesn't really make sense to keep given that structured logging
// gives us the ability to query our logs across multiple dimensions
// I don't believe we should mix this in with atlantis commands and expose this to the user
history bytes.Buffer
}
func NewStructuredLoggerFromLevel(lvl LogLevel) (SimpleLogging, error) {
cfg := zap.NewProductionConfig()
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
cfg.Level = zap.NewAtomicLevelAt(lvl.zLevel)
return newStructuredLogger(cfg)
}
func NewStructuredLogger() (SimpleLogging, error) {
cfg := zap.NewProductionConfig()
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
return newStructuredLogger(cfg)
}
func newStructuredLogger(cfg zap.Config) (*StructuredLogger, error) {
baseLogger, err := cfg.Build()
baseLogger = baseLogger.
// ensures that the caller doesn't just say logging/simple_logger each time
WithOptions(zap.AddCallerSkip(1)).
WithOptions(zap.AddStacktrace(zapcore.WarnLevel)).
// creates isolated context for all future kv pairs, name can be flexible as needed
With(zap.Namespace("json"))
if err != nil {
return nil, errors.Wrap(err, " initializing structured logger")
}
return &StructuredLogger{
z: baseLogger.Sugar(),
level: cfg.Level,
}, nil
}
func (l *StructuredLogger) With(a ...interface{}) SimpleLogging {
return &StructuredLogger{
z: l.z.With(a...),
level: l.level,
}
}
func (l *StructuredLogger) WithHistory(a ...interface{}) SimpleLogging {
logger := &StructuredLogger{
z: l.z.With(a...),
level: l.level,
}
// ensure that the history is kept across loggers.
logger.keepHistory = true
logger.history = l.history
return logger
}
func (l *StructuredLogger) GetHistory() string {
return l.history.String()
}
func (l *StructuredLogger) Debug(format string, a ...interface{}) {
l.z.Debugf(format, a...)
l.saveToHistory(Debug, format, a...)
}
func (l *StructuredLogger) Info(format string, a ...interface{}) {
l.z.Infof(format, a...)
l.saveToHistory(Info, format, a...)
}
func (l *StructuredLogger) Warn(format string, a ...interface{}) {
l.z.Warnf(format, a...)
l.saveToHistory(Warn, format, a...)
}
func (l *StructuredLogger) Err(format string, a ...interface{}) {
l.z.Errorf(format, a...)
l.saveToHistory(Error, format, a...)
}
func (l *StructuredLogger) Log(level LogLevel, format string, a ...interface{}) {
switch level {
case Debug:
l.z.Debugf(format, a...)
case Info:
l.z.Infof(format, a...)
case Warn:
l.z.Warnf(format, a...)
case Error:
l.z.Errorf(format, a...)
}
}
func (l *StructuredLogger) SetLevel(lvl LogLevel) {
if l != nil {
l.level.SetLevel(lvl.zLevel)
}
}
func (l *StructuredLogger) Flush() error {
return l.z.Sync()
}
func (l *StructuredLogger) saveToHistory(lvl LogLevel, format string, a ...interface{}) {
if !l.keepHistory {
return
}
msg := fmt.Sprintf(format, a...)
l.history.WriteString(fmt.Sprintf("[%s] %s\n", lvl.shortStr, msg))
}
// NewNoopLogger creates a logger instance that discards all logs and never
// writes them. Used for testing.
func NewNoopLogger(t zaptest.TestingT) SimpleLogging {
level := zap.DebugLevel
return &StructuredLogger{
z: zaptest.NewLogger(t, zaptest.Level(level)).Sugar(),
level: zap.NewAtomicLevelAt(level),
}
}
type LogLevel struct {
zLevel zapcore.Level
shortStr string
}
var (
Debug = LogLevel{
zLevel: zapcore.DebugLevel,
shortStr: "DBUG",
}
Info = LogLevel{
zLevel: zapcore.InfoLevel,
shortStr: "INFO",
}
Warn = LogLevel{
zLevel: zapcore.WarnLevel,
shortStr: "WARN",
}
Error = LogLevel{
zLevel: zapcore.ErrorLevel,
shortStr: "EROR",
}
)