Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager
This commit is contained in:
40
backend/internal/logger/config.go
Normal file
40
backend/internal/logger/config.go
Normal file
@ -0,0 +1,40 @@
|
||||
package logger
|
||||
|
||||
import "github.com/getsentry/sentry-go"
|
||||
|
||||
// Level type
|
||||
type Level int
|
||||
|
||||
// Log level definitions
|
||||
const (
|
||||
// DebugLevel usually only enabled when debugging. Very verbose logging.
|
||||
DebugLevel Level = 10
|
||||
// InfoLevel general operational entries about what's going on inside the application.
|
||||
InfoLevel Level = 20
|
||||
// WarnLevel non-critical entries that deserve eyes.
|
||||
WarnLevel Level = 30
|
||||
// ErrorLevel used for errors that should definitely be noted.
|
||||
ErrorLevel Level = 40
|
||||
)
|
||||
|
||||
// Config options for the logger.
|
||||
type Config struct {
|
||||
LogThreshold Level
|
||||
Formatter string
|
||||
SentryConfig sentry.ClientOptions
|
||||
}
|
||||
|
||||
// Interface for a logger
|
||||
type Interface interface {
|
||||
GetLogLevel() Level
|
||||
Debug(format string, args ...interface{})
|
||||
Info(format string, args ...interface{})
|
||||
Warn(format string, args ...interface{})
|
||||
Error(errorClass string, err error, args ...interface{})
|
||||
Errorf(errorClass, format string, err error, args ...interface{})
|
||||
}
|
||||
|
||||
// ConfigurableLogger is an interface for a logger that can be configured
|
||||
type ConfigurableLogger interface {
|
||||
Configure(c *Config) error
|
||||
}
|
242
backend/internal/logger/logger.go
Normal file
242
backend/internal/logger/logger.go
Normal file
@ -0,0 +1,242 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
var colorReset, colorGray, colorYellow, colorBlue, colorRed, colorMagenta, colorBlack, colorWhite *color.Color
|
||||
|
||||
// Log message structure.
|
||||
type Log struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Pid int `json:"pid"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
StackTrace []string `json:"stack_trace,omitempty"`
|
||||
}
|
||||
|
||||
// Logger instance
|
||||
type Logger struct {
|
||||
Config
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
// global logging configuration.
|
||||
var logger = NewLogger()
|
||||
|
||||
// NewLogger creates a new logger instance
|
||||
func NewLogger() *Logger {
|
||||
color.NoColor = false
|
||||
colorReset = color.New(color.Reset)
|
||||
colorGray = color.New(color.FgWhite)
|
||||
colorYellow = color.New(color.Bold, color.FgYellow)
|
||||
colorBlue = color.New(color.Bold, color.FgBlue)
|
||||
colorRed = color.New(color.Bold, color.FgRed)
|
||||
colorMagenta = color.New(color.Bold, color.FgMagenta)
|
||||
colorBlack = color.New(color.Bold, color.FgBlack)
|
||||
colorWhite = color.New(color.Bold, color.FgWhite)
|
||||
|
||||
return &Logger{
|
||||
Config: NewConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfig returns the default config
|
||||
func NewConfig() Config {
|
||||
return Config{
|
||||
LogThreshold: InfoLevel,
|
||||
Formatter: "json",
|
||||
}
|
||||
}
|
||||
|
||||
// Configure logger and will return error if missing required fields.
|
||||
func Configure(c *Config) error {
|
||||
return logger.Configure(c)
|
||||
}
|
||||
|
||||
// GetLogLevel currently configured
|
||||
func GetLogLevel() Level {
|
||||
return logger.GetLogLevel()
|
||||
}
|
||||
|
||||
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Debug(format string, args ...interface{}) {
|
||||
logger.Debug(format, args...)
|
||||
}
|
||||
|
||||
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Info(format string, args ...interface{}) {
|
||||
logger.Info(format, args...)
|
||||
}
|
||||
|
||||
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func Warn(format string, args ...interface{}) {
|
||||
logger.Warn(format, args...)
|
||||
}
|
||||
|
||||
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
|
||||
// Attempts to log to bugsang.
|
||||
func Error(errorClass string, err error) {
|
||||
logger.Error(errorClass, err)
|
||||
}
|
||||
|
||||
// Configure logger and will return error if missing required fields.
|
||||
func (l *Logger) Configure(c *Config) error {
|
||||
// ensure updates to the config are atomic
|
||||
l.mux.Lock()
|
||||
defer l.mux.Unlock()
|
||||
|
||||
if c == nil {
|
||||
return fmt.Errorf("a non nil Config is mandatory")
|
||||
}
|
||||
|
||||
if err := c.LogThreshold.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.LogThreshold = c.LogThreshold
|
||||
l.Formatter = c.Formatter
|
||||
l.SentryConfig = c.SentryConfig
|
||||
|
||||
if c.SentryConfig.Dsn != "" {
|
||||
if sentryErr := sentry.Init(c.SentryConfig); sentryErr != nil {
|
||||
fmt.Printf("Sentry initialization failed: %v\n", sentryErr)
|
||||
}
|
||||
}
|
||||
|
||||
stdlog.SetFlags(0) // this removes timestamp prefixes from logs
|
||||
return nil
|
||||
}
|
||||
|
||||
// validate the log level is in the accepted list.
|
||||
func (l Level) validate() error {
|
||||
switch l {
|
||||
case DebugLevel, InfoLevel, WarnLevel, ErrorLevel:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid \"Level\" %d", l)
|
||||
}
|
||||
}
|
||||
|
||||
var logLevels = map[Level]string{
|
||||
DebugLevel: "DEBUG",
|
||||
InfoLevel: "INFO",
|
||||
WarnLevel: "WARN",
|
||||
ErrorLevel: "ERROR",
|
||||
}
|
||||
|
||||
func (l *Logger) logLevel(logLevel Level, format string, args ...interface{}) {
|
||||
if logLevel < l.LogThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
errorClass := ""
|
||||
if logLevel == ErrorLevel {
|
||||
// First arg is the errorClass
|
||||
errorClass = args[0].(string)
|
||||
if len(args) > 1 {
|
||||
args = args[1:]
|
||||
} else {
|
||||
args = []interface{}{}
|
||||
}
|
||||
}
|
||||
|
||||
stringMessage := fmt.Sprintf(format, args...)
|
||||
|
||||
if l.Formatter == "json" {
|
||||
// JSON Log Format
|
||||
jsonLog, _ := json.Marshal(
|
||||
Log{
|
||||
Timestamp: time.Now().Format(time.RFC3339Nano),
|
||||
Level: logLevels[logLevel],
|
||||
Message: stringMessage,
|
||||
Pid: os.Getpid(),
|
||||
},
|
||||
)
|
||||
|
||||
stdlog.Println(string(jsonLog))
|
||||
} else {
|
||||
// Nice Log Format
|
||||
var colorLevel *color.Color
|
||||
switch logLevel {
|
||||
case DebugLevel:
|
||||
colorLevel = colorMagenta
|
||||
case InfoLevel:
|
||||
colorLevel = colorBlue
|
||||
case WarnLevel:
|
||||
colorLevel = colorYellow
|
||||
case ErrorLevel:
|
||||
colorLevel = colorRed
|
||||
stringMessage = fmt.Sprintf("%s: %s", errorClass, stringMessage)
|
||||
}
|
||||
|
||||
t := time.Now()
|
||||
stdlog.Println(
|
||||
colorBlack.Sprint("["),
|
||||
colorWhite.Sprint(t.Format("2006-01-02 15:04:05")),
|
||||
colorBlack.Sprint("] "),
|
||||
colorLevel.Sprintf("%-8v", logLevels[logLevel]),
|
||||
colorGray.Sprint(stringMessage),
|
||||
colorReset.Sprint(""),
|
||||
)
|
||||
|
||||
if logLevel == ErrorLevel && l.LogThreshold == DebugLevel {
|
||||
// Print a stack trace too
|
||||
debug.PrintStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogLevel currently configured
|
||||
func (l *Logger) GetLogLevel() Level {
|
||||
return l.LogThreshold
|
||||
}
|
||||
|
||||
// Debug logs if the log level is set to DebugLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||
l.logLevel(DebugLevel, format, args...)
|
||||
}
|
||||
|
||||
// Info logs if the log level is set to InfoLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Info(format string, args ...interface{}) {
|
||||
l.logLevel(InfoLevel, format, args...)
|
||||
}
|
||||
|
||||
// Warn logs if the log level is set to WarnLevel or below. Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Warn(format string, args ...interface{}) {
|
||||
l.logLevel(WarnLevel, format, args...)
|
||||
}
|
||||
|
||||
// Error logs error given if the log level is set to ErrorLevel or below. Arguments are not logged.
|
||||
// Attempts to log to bugsang.
|
||||
func (l *Logger) Error(errorClass string, err error) {
|
||||
l.logLevel(ErrorLevel, err.Error(), errorClass)
|
||||
l.notifySentry(errorClass, err)
|
||||
}
|
||||
|
||||
func (l *Logger) notifySentry(errorClass string, err error) {
|
||||
if l.SentryConfig.Dsn != "" && l.SentryConfig.Dsn != "-" {
|
||||
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetLevel(sentry.LevelError)
|
||||
scope.SetTag("service", "backend")
|
||||
scope.SetTag("error_class", errorClass)
|
||||
})
|
||||
|
||||
sentry.CaptureException(err)
|
||||
// Since sentry emits events in the background we need to make sure
|
||||
// they are sent before we shut down
|
||||
sentry.Flush(time.Second * 5)
|
||||
}
|
||||
}
|
168
backend/internal/logger/logger_test.go
Normal file
168
backend/internal/logger/logger_test.go
Normal file
@ -0,0 +1,168 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetLogLevel(t *testing.T) {
|
||||
assert.Equal(t, InfoLevel, GetLogLevel())
|
||||
}
|
||||
|
||||
func TestThreshold(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
}))
|
||||
|
||||
Debug("this should not display")
|
||||
assert.Empty(t, buf.String())
|
||||
|
||||
Info("this should display")
|
||||
assert.NotEmpty(t, buf.String())
|
||||
|
||||
Error("ErrorClass", errors.New("this should display"))
|
||||
assert.NotEmpty(t, buf.String())
|
||||
}
|
||||
|
||||
func TestDebug(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: DebugLevel,
|
||||
}))
|
||||
|
||||
Debug("This is a %s message", "test")
|
||||
assert.Contains(t, buf.String(), "DEBUG")
|
||||
assert.Contains(t, buf.String(), "This is a test message")
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
}))
|
||||
|
||||
Info("This is a %s message", "test")
|
||||
assert.Contains(t, buf.String(), "INFO")
|
||||
assert.Contains(t, buf.String(), "This is a test message")
|
||||
}
|
||||
|
||||
func TestWarn(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
}))
|
||||
|
||||
Warn("This is a %s message", "test")
|
||||
assert.Contains(t, buf.String(), "WARN")
|
||||
assert.Contains(t, buf.String(), "This is a test message")
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
log.SetOutput(buf)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
assert.NoError(t, Configure(&Config{
|
||||
LogThreshold: ErrorLevel,
|
||||
}))
|
||||
|
||||
Error("TestErrorClass", fmt.Errorf("this is a %s error", "test"))
|
||||
assert.Contains(t, buf.String(), "ERROR")
|
||||
assert.Contains(t, buf.String(), "this is a test error")
|
||||
}
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
type args struct {
|
||||
c *Config
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "configure",
|
||||
args: args{
|
||||
&Config{
|
||||
LogThreshold: InfoLevel,
|
||||
SentryConfig: sentry.ClientOptions{},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid log level",
|
||||
args: args{
|
||||
&Config{
|
||||
SentryConfig: sentry.ClientOptions{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Configure(tt.args.c); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Configure() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogLevelBelowThreshold(b *testing.B) {
|
||||
l := NewLogger()
|
||||
|
||||
log.SetOutput(ioutil.Discard)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.logLevel(DebugLevel, "benchmark %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogLevelAboveThreshold(b *testing.B) {
|
||||
l := NewLogger()
|
||||
|
||||
log.SetOutput(ioutil.Discard)
|
||||
defer func() {
|
||||
log.SetOutput(os.Stderr)
|
||||
}()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.logLevel(InfoLevel, "benchmark %d", i)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user