First version of the Golang logging library 45/245/2 v0.0.1
authorRoni Riska <roni.riska@nokia.com>
Tue, 4 Jun 2019 08:27:33 +0000 (11:27 +0300)
committerRoni Riska <roni.riska@nokia.com>
Tue, 4 Jun 2019 08:33:03 +0000 (11:33 +0300)
Change-Id: Id2a997eb1269a20f428fd94fce7051ecfc9ab1e6
Signed-off-by: Roni Riska <roni.riska@nokia.com>
.gitignore [new file with mode: 0644]
LICENSES.txt [new file with mode: 0644]
README.md [new file with mode: 0644]
cmd/logtester/main.go [new file with mode: 0644]
mdclog.go [new file with mode: 0644]
mdclog_test.go [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/LICENSES.txt b/LICENSES.txt
new file mode 100644 (file)
index 0000000..7863b1e
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..74da0bd
--- /dev/null
@@ -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 (file)
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 (file)
index 0000000..3cf7422
--- /dev/null
@@ -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())
+}