From e7d4b2dd8b2510f6862f95823b8744bea96441c3 Mon Sep 17 00:00:00 2001 From: Petr Vobornik Date: Fri, 27 Oct 2023 16:18:58 +0000 Subject: [PATCH] feat: configurable log prefix For adjusting prefix of output, e.g. to add prefix string or log formatting flags. Format: - prefix: string until first occurance of % - flag: combination of flags Flags: - %d - date - %t - time - %m - microseconds - %l - long file name - %s - short file name - %z - use UTC - %p - move the "prefix" from the beginning of the line to before the message - %S - datetime - same as "%d %t" Basically the meaning is the same as in std log library: https://pkg.go.dev/log#pkg-constants Signed-off-by: Petr Vobornik --- .devcontainer/host-metering.conf | 1 + config/config.go | 10 +++++++ config/config_test.go | 14 ++++++--- contrib/man/host-metering.1 | 43 +++++++++++++++++++++++++++ contrib/man/host-metering.conf.5 | 43 +++++++++++++++++++++++++++ logger/logger.go | 51 +++++++++++++++++++++++++++----- logger/logger_test.go | 42 ++++++++++++++++++++++++-- main.go | 3 +- 8 files changed, 192 insertions(+), 15 deletions(-) diff --git a/.devcontainer/host-metering.conf b/.devcontainer/host-metering.conf index c8b61cf..85ebada 100644 --- a/.devcontainer/host-metering.conf +++ b/.devcontainer/host-metering.conf @@ -18,3 +18,4 @@ write_retry_max_int_sec=2 metrics_wal_path=./mocks/cpumetrics metrics_max_age_sec=30 log_level=DEBUG +log_prefix=%S diff --git a/config/config.go b/config/config.go index 8c29ec4..3784655 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ const ( DefaultMetricsWALPath = "/var/run/host-metering/metrics" DefaultLogLevel = "INFO" DefaultLogPath = "" //Default to stderr, will be logged in journal. + DefaultLogPrefix = "" ) type Config struct { @@ -42,6 +43,7 @@ type Config struct { MetricsWALPath string LogLevel string // one of "ERROR", "WARN", "INFO", "DEBUG", "TRACE" LogPath string + LogPrefix string } func NewConfig() *Config { @@ -60,6 +62,7 @@ func NewConfig() *Config { MetricsWALPath: DefaultMetricsWALPath, LogLevel: DefaultLogLevel, LogPath: DefaultLogPath, + LogPrefix: DefaultLogPrefix, } } @@ -81,6 +84,7 @@ func (c *Config) String() string { fmt.Sprintf("| MetricsWALPath: %s", c.MetricsWALPath), fmt.Sprintf("| LogLevel: %s", c.LogLevel), fmt.Sprintf("| LogPath: %s", c.LogPath), + fmt.Sprintf("| LogPrefix: %s", c.LogPrefix), }, "\n") } @@ -138,6 +142,9 @@ func (c *Config) UpdateFromEnvVars() error { if v := os.Getenv("HOST_METERING_LOG_PATH"); v != "" { c.LogPath = v } + if v := os.Getenv("HOST_METERING_LOG_PREFIX"); v != "" { + c.LogPrefix = v + } return multiError.ErrorOrNil() } @@ -252,6 +259,9 @@ func (c *Config) UpdateFromConfigFile(path string) error { if v, ok := config[section]["log_path"]; ok { c.LogPath = v } + if v, ok := config[section]["log_prefix"]; ok { + c.LogPrefix = v + } return multiError.ErrorOrNil() } diff --git a/config/config_test.go b/config/config_test.go index 0ef2055..c4665e9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -25,7 +25,8 @@ func TestDefaultConfig(t *testing.T) { "| MetricsMaxAgeSec: 5400\n" + "| MetricsWALPath: /var/run/host-metering/metrics\n" + "| LogLevel: INFO\n" + - "| LogPath: \n" + "| LogPath: \n" + + "| LogPrefix: \n" // Create the default configuration. c := NewConfig() @@ -71,7 +72,8 @@ func TestConfigFile(t *testing.T) { "| MetricsMaxAgeSec: 700\n" + "| MetricsWALPath: /tmp/metrics\n" + "| LogLevel: ERROR\n" + - "| LogPath: /tmp/log\n" + "| LogPath: /tmp/log\n" + + "| LogPrefix: %d%t\n" // Update the configuration from a valid config file. fileContent := "[host-metering]\n" + @@ -90,7 +92,8 @@ func TestConfigFile(t *testing.T) { "metrics_max_age_sec = 700\n" + "metrics_wal_path = /tmp/metrics\n" + "log_level = ERROR\n" + - "log_path = /tmp/log\n" + "log_path = /tmp/log\n" + + "log_prefix = %d%t\n" c := NewConfig() @@ -148,7 +151,8 @@ func TestEnvVariables(t *testing.T) { "| MetricsMaxAgeSec: 700\n" + "| MetricsWALPath: /tmp/metrics\n" + "| LogLevel: ERROR\n" + - "| LogPath: /tmp/log\n" + "| LogPath: /tmp/log\n" + + "| LogPrefix: %d\n" // Set valid environment variables. t.Setenv("HOST_METERING_WRITE_URL", "http://test/url") @@ -165,6 +169,7 @@ func TestEnvVariables(t *testing.T) { t.Setenv("HOST_METERING_METRICS_WAL_PATH", "/tmp/metrics") t.Setenv("HOST_METERING_LOG_LEVEL", "ERROR") t.Setenv("HOST_METERING_LOG_PATH", "/tmp/log") + t.Setenv("HOST_METERING_LOG_PREFIX", "%d") // Environment variables are set. Change the defaults. c := NewConfig() @@ -218,6 +223,7 @@ func clearEnvironment() { _ = os.Unsetenv("HOST_METERING_METRICS_WAL_PATH") _ = os.Unsetenv("HOST_METERING_LOG_LEVEL") _ = os.Unsetenv("HOST_METERING_LOG_PATH") + _ = os.Unsetenv("HOST_METERING_LOG_PREFIX") } func checkError(t *testing.T, err error, message string) { diff --git a/contrib/man/host-metering.1 b/contrib/man/host-metering.1 index df455c6..6afb1fa 100644 --- a/contrib/man/host-metering.1 +++ b/contrib/man/host-metering.1 @@ -63,6 +63,49 @@ Log level. Possible values are: DEBUG, INFO, WARN, ERROR, TRACE. \fBHOST_METERING_LOG_PATH\fR Path to log file. Default is empty - stderr. +\fBHOST_METERING_LOG_PREFIX\fR +Prefix of log messages. Default is empty. Format: "[PREFIX][FLAG]*" + +.RS 4 + +\fBPREFIX:\fR string until first occurance of % + +\fBFLAGS:\fR +.RS 4 +.TP +.B %d +Date + +.TP +.B %t +Time + +.TP +.B %m +Microseconds + +.TP +.B %l +Long file name + +.TP +.B %s +Short file name + +.TP +.B %z +Use UTC + +.TP +.B %p +Move the "PREFIX" from the beginning of the line to before the message + +.TP +.B %S +Datetime (same as "%d %t") +.RE +.RE + .SH "FILES" .PP \fI/etc/host-metering.conf\fR diff --git a/contrib/man/host-metering.conf.5 b/contrib/man/host-metering.conf.5 index 30706bf..294e988 100644 --- a/contrib/man/host-metering.conf.5 +++ b/contrib/man/host-metering.conf.5 @@ -98,6 +98,49 @@ log_path (string) Path to log file. Default is empty - stderr. .RE +.PP +log_prefix (string) +.RS 4 +Prefix of log messages. Default is empty. Format: "[PREFIX][FLAG]*" + +\fBPREFIX:\fR string until first occurance of % + +\fBFLAGS:\fR +.RS 4 +.TP +.B %d +Date + +.TP +.B %t +Time + +.TP +.B %m +Microseconds + +.TP +.B %l +Long file name + +.TP +.B %s +Short file name + +.TP +.B %z +Use UTC + +.TP +.B %p +Move the "PREFIX" from the beginning of the line to before the message + +.TP +.B %S +Datetime (same as "%d %t") +.RE +.RE + .SH "EXAMPLES" .PP 1\&. The following example shows how to switch the logging to DEBUG level\&. diff --git a/logger/logger.go b/logger/logger.go index fd8e493..3368560 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -6,10 +6,13 @@ import ( "strings" "time" + std_log "log" + go_log "git.sr.ht/~spc/go-log" ) const defaultLogFormat = 0 +const defaultLogPrefix = "" const ( DebugLevel = "DEBUG" @@ -82,21 +85,16 @@ type Logger interface { } func InitDefaultLogger() Logger { - return go_log.New(os.Stderr, "", defaultLogFormat, go_log.LevelDebug) + return go_log.New(os.Stderr, defaultLogPrefix, defaultLogFormat, go_log.LevelDebug) } -func InitLogger(file string, level string, logStructure ...int) error { +func InitLogger(file string, level string, prefix string, flag int) error { logLevel, err := go_log.ParseLevel(level) if err != nil { return err } - actualLogStructure := defaultLogFormat - if len(logStructure) > 0 { - actualLogStructure = logStructure[0] - } - logFile := os.Stderr if file != "" { logFile, err = os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755) @@ -105,7 +103,7 @@ func InitLogger(file string, level string, logStructure ...int) error { return err } - log = go_log.New(logFile, "", actualLogStructure, logLevel) + log = go_log.New(logFile, prefix, flag, logLevel) return nil } @@ -214,6 +212,43 @@ func Traceln(v ...interface{}) { getLogger().Traceln(v...) } +func ParseLogPrefix(format string) (prefix string, flag int) { + if !strings.Contains(format, "%") { + return format, defaultLogFormat + } + + prefix = format[:strings.Index(format, "%")] + flag = 0 + + if strings.Contains(format, "%d") { + flag |= std_log.Ldate + } + if strings.Contains(format, "%t") { + flag |= std_log.Ltime + } + if strings.Contains(format, "%m") { + flag |= std_log.Lmicroseconds + } + if strings.Contains(format, "%l") { + flag |= std_log.Llongfile + } + if strings.Contains(format, "%s") { + flag |= std_log.Lshortfile + } + if strings.Contains(format, "%z") { + flag |= std_log.LUTC + } + if strings.Contains(format, "%p") { + flag |= std_log.Lmsgprefix + } + if strings.Contains(format, "%S") { + flag |= std_log.LstdFlags + } + + return prefix, flag + +} + type LogEntry struct { Time time.Time Level string diff --git a/logger/logger_test.go b/logger/logger_test.go index b64539b..971255a 100644 --- a/logger/logger_test.go +++ b/logger/logger_test.go @@ -4,6 +4,8 @@ import ( "os" "strings" "testing" + + std_log "log" ) // Test that logger global functions won't crash even if the logger is not initialized. @@ -46,7 +48,7 @@ func TestLoggerGlobalFunctions(t *testing.T) { // Test initialization of logger with only log level func TestInitLogger(t *testing.T) { - InitLogger("", DebugLevel) + InitLogger("", DebugLevel, defaultLogPrefix, defaultLogFormat) if log == nil { t.Fatalf("logger is not initialized") } @@ -58,7 +60,7 @@ func TestInitLogger(t *testing.T) { func TestInitLoggerFile(t *testing.T) { dir := t.TempDir() path := dir + "/test.log" - InitLogger(path, DebugLevel) + InitLogger(path, DebugLevel, defaultLogPrefix, defaultLogFormat) if log == nil { t.Fatalf("logger is not initialized") } @@ -203,6 +205,42 @@ func TestOverridenLogger(t *testing.T) { } } +type LogPrefixTestCase struct { + prefix string + expectedPrefix string + expectedFlag int +} + +func TestParseLogPrefix(t *testing.T) { + testCases := []LogPrefixTestCase{ + {"", "", 0}, + {"test", "test", 0}, + {"test:", "test:", 0}, + {"test: ", "test: ", 0}, + {"test: %d", "test: ", std_log.Ldate}, + {"test: %d %t", "test: ", std_log.Ldate | std_log.Ltime}, + {"test: %d %t %m", "test: ", std_log.Ldate | std_log.Ltime | std_log.Lmicroseconds}, + {"test: %S %l", "test: ", std_log.LstdFlags | std_log.Llongfile}, + {"test: %S %s", "test: ", std_log.LstdFlags | std_log.Lshortfile}, + {"test: %S %z", "test: ", std_log.LstdFlags | std_log.LUTC}, + {"test%S %p", "test", std_log.LstdFlags | std_log.Lmsgprefix}, + {"test: %S", "test: ", std_log.LstdFlags}, + } + + for _, tc := range testCases { + t.Run(tc.prefix, func(t *testing.T) { + prefix, flag := ParseLogPrefix(tc.prefix) + if prefix != tc.expectedPrefix { + t.Fatalf("expected prefix: %s got: %s", tc.prefix, prefix) + } + if flag != tc.expectedFlag { + t.Fatalf("expected flag: %d got: %d", tc.expectedFlag, flag) + } + }) + } + +} + // Helper functions func clearLogger() { diff --git a/main.go b/main.go index 6898eab..3fc78d8 100644 --- a/main.go +++ b/main.go @@ -47,7 +47,8 @@ func main() { } // initialize the logger according to the given configuration - err = logger.InitLogger(cfg.LogPath, cfg.LogLevel) + logPrefix, logFlag := logger.ParseLogPrefix(cfg.LogPrefix) + err = logger.InitLogger(cfg.LogPath, cfg.LogLevel, logPrefix, logFlag) if err != nil { logger.Debugf("Error initializing logger: %s\n", err.Error())