Adapted logging which defaults to JSON encoding

This commit is contained in:
Varakh 2024-01-24 23:21:48 +01:00
parent feedfad99f
commit 0194429c80
5 changed files with 145 additions and 79 deletions

View file

@ -4,6 +4,7 @@ Changes adhere to [semantic versioning](https://semver.org).
## [1.1.0] - UNRELEASED ## [1.1.0] - UNRELEASED
* Adapted logging which defaults to JSON encoding
* ... * ...
## [1.0.3] - 2024/01/21 ## [1.0.3] - 2024/01/21

View file

@ -128,7 +128,7 @@ via web interface or API.
The following environment variables can be used to modify application behavior. The following environment variables can be used to modify application behavior.
| Variable | Purpose | Default/Description | | Variable | Purpose | Default/Description |
|:-----------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------| |:-----------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------|
| `TZ` | The time zone (**recommended** to set it properly, background tasks depend on it) | Defaults to `Europe/Berlin`, can be any time zone according to _tz database_ | | `TZ` | The time zone (**recommended** to set it properly, background tasks depend on it) | Defaults to `Europe/Berlin`, can be any time zone according to _tz database_ |
| `ADMIN_USER` | Admin user name for login | Not set by default, you need to explicitly set it to user name | | `ADMIN_USER` | Admin user name for login | Not set by default, you need to explicitly set it to user name |
| `ADMIN_PASSWORD` | Admin password for login | Not set by default, you need to explicitly set it to a secure random | | `ADMIN_PASSWORD` | Admin password for login | Not set by default, you need to explicitly set it to a secure random |
@ -152,7 +152,8 @@ The following environment variables can be used to modify application behavior.
| `CORS_ALLOW_METHODS` | CORS configuration | Defaults to `GET, POST, PUT, PATCH, DELETE, OPTIONS` | | `CORS_ALLOW_METHODS` | CORS configuration | Defaults to `GET, POST, PUT, PATCH, DELETE, OPTIONS` |
| `CORS_ALLOW_HEADERS` | CORS configuration | Defaults to `Authorization, Content-Type` | | `CORS_ALLOW_HEADERS` | CORS configuration | Defaults to `Authorization, Content-Type` |
| | | | | | | |
| `LOGGING_LEVEL` | Logging level. Possible are `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal` | Defaults to `info` | | `LOGGING_LEVEL` | Logging level. Possible are `debug`, `info`, `warn`, `error`, `dpanic`, `panic`, `fatal`. Setting to `debug` enables high verbosity output. | Defaults to `info` |
| `LOGGING_ENCODING` | Logging encoding. Possible are `console` and `json` | Defaults to `json` |
| | | | | | | |
| `WEBHOOKS_TOKEN_LENGTH` | The length of the token | Defaults to `16`, positive number | | `WEBHOOKS_TOKEN_LENGTH` | The length of the token | Defaults to `16`, positive number |
| | | | | | | |
@ -281,7 +282,14 @@ docker build --rm --no-cache -t upda:latest .
## Development & contribution ## Development & contribution
* Ensure to set `LOGGING_LEVEL=debug` for proper debug logs during development. * Ensure to set the following environment variables for proper debug logs during development
```shell
DEVELOPMENT=true
LOGGING_ENCODING=console
LOGGING_LEVEL=debug
```
* Code guidelines * Code guidelines
* Each entity has its own repository * Each entity has its own repository
* Each entity is only used in repository and service (otherwise, mapping happens) * Each entity is only used in repository and service (otherwise, mapping happens)

View file

@ -23,8 +23,8 @@ func Start() {
// secure init // secure init
util.AssertAvailablePRNG() util.AssertAvailablePRNG()
// set gin mode derived from logging level // set gin mode derived
if zap.L().Level() == zap.DebugLevel { if env.appConfig.isDevelopment {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
} else { } else {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)

View file

@ -1,14 +1,19 @@
package server package server
const ( const (
envDevelopment = "DEVELOPMENT"
envLoggingLevel = "LOGGING_LEVEL"
loggingLevelDefault = "info"
envLoggingEncoding = "LOGGING_ENCODING"
loggingEncodingDefault = "json"
envTZ = "TZ" envTZ = "TZ"
tzDefault = "Europe/Berlin" tzDefault = "Europe/Berlin"
envAdminUser = "ADMIN_USER" envAdminUser = "ADMIN_USER"
envAdminPassword = "ADMIN_PASSWORD" envAdminPassword = "ADMIN_PASSWORD"
envLoggingLevel = "LOGGING_LEVEL"
envServerPort = "SERVER_PORT" envServerPort = "SERVER_PORT"
envServerListen = "SERVER_LISTEN" envServerListen = "SERVER_LISTEN"
envServerTlsEnabled = "SERVER_TLS_ENABLED" envServerTlsEnabled = "SERVER_TLS_ENABLED"

View file

@ -1,6 +1,7 @@
package server package server
import ( import (
"errors"
"fmt" "fmt"
"github.com/adrg/xdg" "github.com/adrg/xdg"
"go.uber.org/zap" "go.uber.org/zap"
@ -8,6 +9,7 @@ import (
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
"log" "log"
"moul.io/zapgorm2" "moul.io/zapgorm2"
"os" "os"
@ -17,6 +19,8 @@ import (
type appConfig struct { type appConfig struct {
timeZone string timeZone string
isDevelopment bool
isDebug bool
} }
type serverConfig struct { type serverConfig struct {
@ -74,34 +78,77 @@ type Environment struct {
} }
func bootstrapEnvironment() *Environment { func bootstrapEnvironment() *Environment {
// logging (configured independently)
var logger *zap.Logger
var err error var err error
level := zap.NewAtomicLevelAt(zapcore.InfoLevel)
envLoggingLevel := os.Getenv(envLoggingLevel) // bootstrap logging (configured independently and required before any other action)
if envLoggingLevel != "" { loggingLevel := os.Getenv(envLoggingLevel)
if level, err = zap.ParseAtomicLevel(envLoggingLevel); err != nil { if loggingLevel == "" {
if err = os.Setenv(envLoggingLevel, loggingLevelDefault); err != nil {
log.Fatalf("Cannot set logging level: %v", err)
}
loggingLevel = os.Getenv(envLoggingLevel)
}
var level zap.AtomicLevel
if level, err = zap.ParseAtomicLevel(loggingLevel); err != nil {
log.Fatalf("Cannot parse logging level: %v", err) log.Fatalf("Cannot parse logging level: %v", err)
} }
loggingEncoding := os.Getenv(envLoggingEncoding)
if loggingEncoding == "" {
if err = os.Setenv(envLoggingEncoding, loggingEncodingDefault); err != nil {
log.Fatalf("Cannot set logging encoding: %v", err)
}
loggingEncoding = os.Getenv(envLoggingEncoding)
}
if loggingEncoding != "json" && loggingEncoding != "console" {
log.Fatalf("Cannot parse logging level: %v", errors.New("only 'json' and 'console' are allowed logging encodings"))
}
isDebug := level.Level() == zap.DebugLevel
isDevelopment := os.Getenv(envDevelopment) == "true"
var loggingEncoderConfig zapcore.EncoderConfig
if loggingEncoding == "json" {
loggingEncoderConfig = zap.NewProductionEncoderConfig()
} else {
loggingEncoderConfig = zap.NewDevelopmentEncoderConfig()
} }
logger, err = zap.NewDevelopment(zap.IncreaseLevel(level)) var zapConfig *zap.Config
if err != nil { if isDebug {
log.Fatalf("Can't initialize logger: %v", err) zapConfig = &zap.Config{
Level: level,
Development: isDevelopment,
Encoding: loggingEncoding,
EncoderConfig: loggingEncoderConfig,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
} else {
zapConfig = &zap.Config{
Level: level,
Development: isDevelopment,
Sampling: &zap.SamplingConfig{
Initial: 100,
Thereafter: 100,
},
Encoding: loggingEncoding,
EncoderConfig: loggingEncoderConfig,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
} }
// flushes buffer, if any
defer logger.Sync()
zap.ReplaceGlobals(logger) zapLogger := zap.Must(zapConfig.Build())
defer zapLogger.Sync()
zap.ReplaceGlobals(zapLogger)
// assign defaults from given environment variables and validate // assign defaults from given environment variables and validate
bootstrapFromEnvironmentAndValidate() bootstrapFromEnvironmentAndValidate()
// parse environment variables in actual configuration structs // parse environment variables in actual configuration structs
// app config // app config
appConfig := &appConfig{ ac := &appConfig{
timeZone: os.Getenv(envTZ), timeZone: os.Getenv(envTZ),
isDebug: isDebug,
isDevelopment: isDevelopment,
} }
// server config // server config
@ -137,7 +184,7 @@ func bootstrapEnvironment() *Environment {
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)}, corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
} }
authConfig := &authConfig{ authC := &authConfig{
adminUser: os.Getenv(envAdminUser), adminUser: os.Getenv(envAdminUser),
adminPassword: os.Getenv(envAdminPassword), adminPassword: os.Getenv(envAdminPassword),
} }
@ -179,24 +226,29 @@ func bootstrapEnvironment() *Environment {
zap.L().Sugar().Fatalln("Invalid webhook token length. Reason: must be a positive number") zap.L().Sugar().Fatalln("Invalid webhook token length. Reason: must be a positive number")
} }
webhookConfig := &webhookConfig{ wc := &webhookConfig{
tokenLength: webhookTokenLength, tokenLength: webhookTokenLength,
} }
prometheusConfig := &prometheusConfig{ pc := &prometheusConfig{
enabled: os.Getenv(envPrometheusEnabled) == "true", enabled: os.Getenv(envPrometheusEnabled) == "true",
path: os.Getenv(envPrometheusMetricsPath), path: os.Getenv(envPrometheusMetricsPath),
secureTokenEnabled: os.Getenv(envPrometheusSecureTokenEnabled) == "true", secureTokenEnabled: os.Getenv(envPrometheusSecureTokenEnabled) == "true",
secureToken: os.Getenv(envPrometheusSecureToken), secureToken: os.Getenv(envPrometheusSecureToken),
} }
if prometheusConfig.enabled && prometheusConfig.secureTokenEnabled { if pc.enabled && pc.secureTokenEnabled {
failIfEnvKeyNotPresent(envPrometheusSecureToken) failIfEnvKeyNotPresent(envPrometheusSecureToken)
} }
// database setup // database setup
gormLogger := zapgorm2.New(logger) gormConfig := &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}
gormLogger.SetAsDefault() if isDebug && isDevelopment {
gormZapLogger := zap.Must(zapConfig.Build())
defer gormZapLogger.Sync()
gormLogger := zapgorm2.New(gormZapLogger)
gormConfig = &gorm.Config{Logger: gormLogger}
}
var db *gorm.DB var db *gorm.DB
zap.L().Sugar().Infof("Using database type '%s'", os.Getenv(envDbType)) zap.L().Sugar().Infof("Using database type '%s'", os.Getenv(envDbType))
@ -213,7 +265,7 @@ func bootstrapEnvironment() *Environment {
dbFile := os.Getenv(envDbSqliteFile) dbFile := os.Getenv(envDbSqliteFile)
zap.L().Sugar().Infof("Using database file '%s'", dbFile) zap.L().Sugar().Infof("Using database file '%s'", dbFile)
if db, err = gorm.Open(sqlite.Open(dbFile), &gorm.Config{Logger: gormLogger}); err != nil { if db, err = gorm.Open(sqlite.Open(dbFile), gormConfig); err != nil {
zap.L().Sugar().Fatalf("Could not setup database: %v", err) zap.L().Sugar().Fatalf("Could not setup database: %v", err)
} }
@ -237,7 +289,7 @@ func bootstrapEnvironment() *Environment {
} }
dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=disable TimeZone=%v", host, dbUser, dbPass, dbName, port, dbTZ) dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=disable TimeZone=%v", host, dbUser, dbPass, dbName, port, dbTZ)
if db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: gormLogger}); err != nil { if db, err = gorm.Open(postgres.Open(dsn), gormConfig); err != nil {
zap.L().Sugar().Fatalf("Could not setup database: %v", err) zap.L().Sugar().Fatalf("Could not setup database: %v", err)
} }
} else { } else {
@ -248,13 +300,13 @@ func bootstrapEnvironment() *Environment {
zap.L().Sugar().Fatalf("Could not setup database") zap.L().Sugar().Fatalf("Could not setup database")
} }
env := &Environment{appConfig: appConfig, env := &Environment{appConfig: ac,
authConfig: authConfig, authConfig: authC,
serverConfig: sc, serverConfig: sc,
taskConfig: tc, taskConfig: tc,
lockConfig: lc, lockConfig: lc,
webhookConfig: webhookConfig, webhookConfig: wc,
prometheusConfig: prometheusConfig, prometheusConfig: pc,
db: db} db: db}
if err = env.db.AutoMigrate(&Update{}, &Webhook{}, &Event{}); err != nil { if err = env.db.AutoMigrate(&Update{}, &Webhook{}, &Event{}); err != nil {