From aaffc8ebe3f1dd3d3f77b80d5de3de7465994863 Mon Sep 17 00:00:00 2001 From: Roni Riska Date: Tue, 4 Jun 2019 11:27:33 +0300 Subject: [PATCH] First version of the Golang logging library Change-Id: Id2a997eb1269a20f428fd94fce7051ecfc9ab1e6 Signed-off-by: Roni Riska --- .gitignore | 0 LICENSES.txt | 34 ++++++++++ README.md | 66 +++++++++++++++++++ cmd/logtester/main.go | 38 +++++++++++ mdclog.go | 177 ++++++++++++++++++++++++++++++++++++++++++++++++++ mdclog_test.go | 154 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 469 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSES.txt create mode 100644 README.md create mode 100644 cmd/logtester/main.go create mode 100644 mdclog.go create mode 100644 mdclog_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/LICENSES.txt b/LICENSES.txt new file mode 100644 index 0000000..7863b1e --- /dev/null +++ b/LICENSES.txt @@ -0,0 +1,34 @@ +LICENSES.txt + + +Unless otherwise specified, all software contained herein is licensed +under the Apache License, Version 2.0 (the "Software License"); +you may not use this software except in compliance with the Software +License. You may obtain a copy of the Software License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the Software License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the Software License for the specific language governing permissions +and limitations under the Software License. + + + +Unless otherwise specified, all documentation contained herein is licensed +under the Creative Commons License, Attribution 4.0 Intl. (the +"Documentation License"); you may not use this documentation except in +compliance with the Documentation License. You may obtain a copy of the +Documentation License at + +https://creativecommons.org/licenses/by/4.0/ + +Unless required by applicable law or agreed to in writing, documentation +distributed under the Documentation License is distributed on an "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the Documentation License for the specific language governing +permissions and limitations under the Documentation License. + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d5ed40 --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +Logging library with MDC support +================================ + +A Golang implementation of a structured logging library with Mapped Diagnostics Context (MDC) support. + +Overview +-------- + +### Initialization + +A new logger instance is created with InitLogger function. Process identity is given as a parameter. + +### Mapped Diagnostics Context + +The MDCs are key-value pairs, which are included to all log entries by the library. +The MDC pairs are logger instance specific. + +### Log entry format + +Each log entry written the library contains + + * Timestamp + * Logger identity + * Log entry severity + * MDC pairs of the logger instance + * Log message text + +Currently the library only supports JSON formatted output written to standard out of the process + +*Example log output* + +`{"ts":1551183682974,"crit":"INFO","id":"myprog","mdc":{"second key":"other value","mykey":"keyval"},"msg":"hello world!"}` + +Example +------- + +```go +package main + +import ( + mdcloggo "gerrit.o-ran-sc.org/r/com/golog" +) + +func main() { + logger, _ := mdcloggo.InitLogger("myname") + logger.MdcAdd("mykey", "keyval") + logger.Info("Some test logs") +} +``` + +License +------- + Copyright (c) 2019 AT&T Intellectual Property. + Copyright (c) 2018-2019 Nokia. + + 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. diff --git a/cmd/logtester/main.go b/cmd/logtester/main.go new file mode 100644 index 0000000..74da0bd --- /dev/null +++ b/cmd/logtester/main.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 AT&T Intellectual Property. + * Copyright (c) 2018-2019 Nokia. + * + * 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 main + +import ( + "fmt" + "os" + "time" + + mdcloggo "gerrit.o-ran-sc.org/r/com/golog" +) + +func main() { + logger, _ := mdcloggo.InitLogger("myname") + logger.MdcAdd("foo", "bar") + logger.MdcAdd("foo2", "bar2") + start := time.Now() + for i := 0; i < 10; i++ { + logger.Info("Some test logs") + } + elapsed := time.Since(start) + fmt.Fprintf(os.Stderr, "Elapsed %v\n", elapsed) +} diff --git a/mdclog.go b/mdclog.go new file mode 100644 index 0000000..2a37ea7 --- /dev/null +++ b/mdclog.go @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2019 AT&T Intellectual Property. + * Copyright (c) 2018-2019 Nokia. + * + * 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 golog implements a simple structured logging with MDC (Mapped Diagnostics Context) support. +package golog + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "sync" + "time" +) + +// Level is a type define for the logging level. +type Level int + +const ( + // ERR is an error level log entry. + ERR Level = 1 + // WARN is a warning level log entry. + WARN Level = 2 + // INFO is an info level log entry. + INFO Level = 3 + // DEBUG is a debug level log entry. + DEBUG Level = 4 +) + +// MdcLogger is the logger instance, created with InitLogger() function. +type MdcLogger struct { + proc string + writer io.Writer + mdc map[string]string + mutex sync.Mutex + level Level +} + +type logEntry struct { + Ts int64 `json:"ts"` + Crit string `json:"crit"` + Id string `json:"id"` + Mdc map[string]string `json:"mdc"` + Msg string `json:"msg"` +} + +func levelString(level Level) string { + switch level { + case ERR: + return "ERROR" + case WARN: + return "WARNING" + case INFO: + return "INFO" + case DEBUG: + return "DEBUG" + default: + return "" + } +} + +func getTime() int64 { + ns := time.Time.UnixNano(time.Now()) + return ns / int64(time.Millisecond) +} + +func (l *MdcLogger) formatLog(level Level, msg string) ([]byte, error) { + log := logEntry{getTime(), levelString(level), l.proc, l.mdc, msg} + buf := bytes.NewBuffer(nil) + encoder := json.NewEncoder(buf) + encoder.SetEscapeHTML(false) + err := encoder.Encode(log) + return buf.Bytes(), err +} + +func initLogger(proc string, writer io.Writer) (*MdcLogger, error) { + return &MdcLogger{proc: proc, writer: writer, mdc: make(map[string]string), level: DEBUG}, nil +} + +// InitLogger is the init routine which returns a new logger instance. +// The program identity is given as a parameter. The identity +// is added to every log writing. +// The function returns a new instance or an error. +func InitLogger(proc string) (*MdcLogger, error) { + return initLogger(proc, os.Stdout) +} + +// Log is the basic logging function to write a log message with +// the given level +func (l *MdcLogger) Log(level Level, formatMsg string, a ...interface{}) { + l.mutex.Lock() + defer l.mutex.Unlock() + if l.level < level { + return + } + log, err := l.formatLog(level, fmt.Sprintf(formatMsg, a...)) + if err == nil { + l.writer.Write(log) + } +} + +// Error is the "error" level logging function. +func (l *MdcLogger) Error(formatMsg string, a ...interface{}) { + l.Log(ERR, formatMsg, a...) +} + +// Warning is the "warning" level logging function. +func (l *MdcLogger) Warning(formatMsg string, a ...interface{}) { + l.Log(WARN, formatMsg, a...) +} + +// Info is the "info" level logging function. +func (l *MdcLogger) Info(formatMsg string, a ...interface{}) { + l.Log(INFO, formatMsg, a...) +} + +// Debug is the "debug" level logging function. +func (l *MdcLogger) Debug(formatMsg string, a ...interface{}) { + l.Log(DEBUG, formatMsg, a...) +} + +// LevelSet sets the current logging level. +// Log writings with less significant level are discarded. +func (l *MdcLogger) LevelSet(level Level) { + l.level = level +} + +// LevelGet returns the current logging level. +func (l *MdcLogger) LevelGet() Level { + return l.level +} + +// MdcAdd adds a new MDC key value pair to the logger. +func (l *MdcLogger) MdcAdd(key string, value string) { + l.mutex.Lock() + defer l.mutex.Unlock() + l.mdc[key] = value +} + +// MdcRemove removes an MDC key from the logger. +func (l *MdcLogger) MdcRemove(key string) { + l.mutex.Lock() + defer l.mutex.Unlock() + delete(l.mdc, key) +} + +// MdcGet gets the value of an MDC from the logger. +// The function returns the value string and a boolean +// which tells if the key was found or not. +func (l *MdcLogger) MdcGet(key string) (string, bool) { + l.mutex.Lock() + defer l.mutex.Unlock() + val, ok := l.mdc[key] + return val, ok +} + +// MdcClean removes all MDC keys from the logger. +func (l *MdcLogger) MdcClean() { + l.mutex.Lock() + defer l.mutex.Unlock() + l.mdc = make(map[string]string) +} diff --git a/mdclog_test.go b/mdclog_test.go new file mode 100644 index 0000000..3cf7422 --- /dev/null +++ b/mdclog_test.go @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2019 AT&T Intellectual Property. + * Copyright (c) 2018-2019 Nokia. + * + * 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 golog + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" +) + +// getTestLogger returns a logger instance where +// the output is directed to a byte buffer instead +// of stdout +func getTestLogger(t *testing.T) (*MdcLogger, *bytes.Buffer) { + logbuffer := new(bytes.Buffer) + logger, err := initLogger("foo", logbuffer) + assert.Nil(t, err) + return logger, logbuffer +} + +func TestLogInitDoesNotReturnAnError(t *testing.T) { + _, err := InitLogger("foo") + assert.Nil(t, err, "create failed") +} + +func TestDebugFunctionLogsCorrectString(t *testing.T) { + logger, logbuffer := getTestLogger(t) + logger.Debug("test debug") + logstr := logbuffer.String() + assert.Contains(t, logstr, "crit\":\"DEBUG\",\"id\":\"foo\",\"mdc\":{},\"msg\":\"test debug\"}\n") +} + +func TestInfoFunctionLogsCorrectString(t *testing.T) { + logger, logbuffer := getTestLogger(t) + logger.Info("test info") + logstr := logbuffer.String() + assert.Contains(t, logstr, "crit\":\"INFO\",\"id\":\"foo\",\"mdc\":{},\"msg\":\"test info\"}\n") +} + +func TestWarningLogsCorrectString(t *testing.T) { + logger, logbuffer := getTestLogger(t) + logger.Warning("test warn") + logstr := logbuffer.String() + assert.Contains(t, logstr, "crit\":\"WARNING\",\"id\":\"foo\",\"mdc\":{},\"msg\":\"test warn\"}\n") +} + +func TestErrorFunctionLogsCorrectString(t *testing.T) { + logger, logbuffer := getTestLogger(t) + logger.Error("test err") + logstr := logbuffer.String() + assert.Contains(t, logstr, "crit\":\"ERROR\",\"id\":\"foo\",\"mdc\":{},\"msg\":\"test err\"}\n") +} + +func TestLogFunctionLogsCorrectString(t *testing.T) { + logger, logbuffer := getTestLogger(t) + logger.Log(ERR, "test err") + logstr := logbuffer.String() + assert.Contains(t, logstr, "crit\":\"ERROR\",\"id\":\"foo\",\"mdc\":{},\"msg\":\"test err\"}\n") +} + +func TestFormatWithMdcReturnsJsonFormatedString(t *testing.T) { + logger, _ := InitLogger("foo") + logger.MdcAdd("foo", "bar") + logstr, err := logger.formatLog(INFO, "test2") + assert.Nil(t, err, "formatLog fails") + v := make(map[string]interface{}) + err = json.Unmarshal(logstr, &v) + assert.Equal(t, "INFO", v["crit"]) + assert.Equal(t, "test2", v["msg"]) + assert.Equal(t, "foo", v["id"]) + expectedmdc := map[string]interface{}{"foo": "bar"} + assert.Equal(t, expectedmdc, v["mdc"]) +} + +func TestMdcAddIsOk(t *testing.T) { + logger, _ := InitLogger("foo") + logger.MdcAdd("foo", "bar") + val, ok := logger.MdcGet("foo") + assert.True(t, ok) + assert.Equal(t, "bar", val) +} + +func TestMdcRemoveWorks(t *testing.T) { + logger, _ := InitLogger("foo") + logger.MdcAdd("foo", "bar") + val, ok := logger.MdcGet("foo") + assert.True(t, ok) + assert.Equal(t, "bar", val) + logger.MdcRemove("foo") + val, ok = logger.MdcGet("foo") + assert.False(t, ok) + assert.Empty(t, val) +} + +func TestRemoveNonExistentMdcDoesNotCrash(t *testing.T) { + logger, _ := InitLogger("foo") + logger.MdcRemove("foo") +} + +func TestMdcCleanRemovesAllMdcs(t *testing.T) { + logger, _ := InitLogger("foo") + logger.MdcAdd("foo1", "bar") + logger.MdcAdd("foo2", "bar") + logger.MdcAdd("foo3", "bar") + logger.MdcClean() + _, ok := logger.MdcGet("foo1") + assert.False(t, ok) + _, ok = logger.MdcGet("foo2") + assert.False(t, ok) + _, ok = logger.MdcGet("foo3") + assert.False(t, ok) +} + +func TestLevelStringsGetterWorks(t *testing.T) { + assert.Equal(t, "ERROR", levelString(ERR)) + assert.Equal(t, "WARNING", levelString(WARN)) + assert.Equal(t, "INFO", levelString(INFO)) + assert.Equal(t, "DEBUG", levelString(DEBUG)) +} + +func TestDefaultLoggingLevelIsDebug(t *testing.T) { + logger, _ := InitLogger("foo") + assert.Equal(t, DEBUG, logger.LevelGet()) +} + +func TestLevelGetReturnsWhatWasSet(t *testing.T) { + logger, _ := InitLogger("foo") + logger.LevelSet(ERR) + assert.Equal(t, ERR, logger.LevelGet()) +} + +func TestDebugLogIsNotWrittenIfCurrentLevelIsInfo(t *testing.T) { + logger, logbuffer := getTestLogger(t) + logger.LevelSet(INFO) + logger.Debug("fooo") + assert.Empty(t, logbuffer.String()) +} -- 2.16.6