Skip to content

Commit 5307f47

Browse files
committed
feat: added log package with importantLogger support
Important logger panics if it fails to write the log more than specified in `maxErrors` param
1 parent be550b7 commit 5307f47

File tree

5 files changed

+233
-0
lines changed

5 files changed

+233
-0
lines changed

log/doc.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Package log пакет предназначен для упрощения ведения логов в программах.
2+
// Представляет надстройку над go-kit/kit/log.
3+
// Этот пакет создан для ведения важных логов, т.е. без нормально работающего логгера продолжение работы программы не имеет смысла, т.к. разрабочик не получит важную информацию в случае откраза.
4+
// Потому этот парет предоставялет возможность использовать важный логгер (см. `NewImportantLogger()`), который следит за количеством ошибок при выполнение записи в носитель лога (например, файл). И если число ошибок превышает заданное - вызывает панику.
5+
package log
6+
7+
// Logger интерфейс логгера базовый для препятствования сильному связыванию
8+
// см. https://dave.cheney.net/2015/11/05/lets-talk-about-logging
9+
// https://docs.google.com/document/d/1oTjtY49y8iSxmM9YBaz2NrZIlaXtQsq3nQMd-E0HtwM/edit
10+
// https://groups.google.com/forum/#!topic/golang-dev/F3l9Iz1JX4g
11+
/*
12+
Это позволит выводить любой тип данных, например:
13+
14+
// common formatted string output
15+
Log(fmt.Sprintf(“some custom print string: %s”, value))
16+
17+
// no format string, just values
18+
Log(someText, struct1, struct2, err)
19+
20+
// Custom key value style structs that can be interrogated with the logger that satisfied the interface to create structured logging
21+
Log(key, value, key, value)
22+
23+
// log levels (with a colog style implementation)
24+
Log(“info: starting up system”)
25+
Log(fmt.Sprintf(“err: kaboom %s”, err))
26+
*/

log/example/main.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
"github.com/r3code/go-useful-snippets/log"
7+
kitlog "github.com/go-kit/kit/log"
8+
)
9+
10+
func main() {
11+
logger := kitlog.NewLogfmtLogger(os.Stdout)
12+
for depth := 0; depth <= 5; depth++ {
13+
l := log.With(logger, "caller", kitlog.Caller(depth))
14+
l.Log("depth", depth) // line 13
15+
}
16+
l2 := log.NewDefaultStdOutLogger(false)
17+
l2.Log("testNo", "1")
18+
l3 := log.MustCreateComponentLog(l2, "cp2")
19+
l3.Log("test", "2")
20+
}

log/important_logger.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package log
2+
3+
import (
4+
gklog "github.com/go-kit/kit/log"
5+
"github.com/juju/errors"
6+
)
7+
8+
/* See https://github.com/go-kit/kit/issues/164#issuecomment-274185353
9+
10+
Пример использования:
11+
func main() {
12+
// ...
13+
var logger log.Logger
14+
logger = importantLogger{
15+
logger: gklog.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)),
16+
thresh: 10,
17+
metricErrCounter: logErrorCounter,
18+
}
19+
logger = log.NewContext(logger).With("ts",log.DefaultTimestampUTC,
20+
"caller", log.DefaultCaller)
21+
// ...
22+
app.AppLogic(logger, 5, "hello, world")
23+
}
24+
25+
*/
26+
27+
type importantLogger struct {
28+
logger gklog.Logger
29+
// metricErrCounter metrics.Counter
30+
errorCount uint8
31+
maxErrors uint8
32+
}
33+
34+
// importantLogger отслеживает число ошибок записи писателя лога, а когда число ошибок переваливает за указанное число `maxErrors`, то он вызывает панику.
35+
// Реализует интерфейс log.Logger.
36+
// Всегда возвращает только nil или вызывает panic.
37+
func (il *importantLogger) Log(keyvals ...interface{}) error {
38+
if err := il.logger.Log(keyvals...); err != nil {
39+
il.errorCount++
40+
// TODO(dsin): добавить счетичик для вывода метрики "логгер.число_ошибок_записи" - бекэнд или expvar или prometheus
41+
// il.metricErrCounter.Add(1)
42+
tooMuchLogFails := il.errorCount > il.maxErrors
43+
if tooMuchLogFails {
44+
panic(errors.Annotate(err, "Logger write failed"))
45+
}
46+
}
47+
return nil
48+
}
49+
50+
// NewImportantLogger созает экземпляр важного логгера из обычного, отказ которого вызывает панику и остановку программы.
51+
// Параметр `maxErrors` указывает предельное количество ошибок запси в лог после достижения которого будет вызвана паника и выполнение программы прервется.
52+
// Если `maxErrors` = 0, то используется значение `maxErrors` по умолчанию (=1).
53+
func NewImportantLogger(logger gklog.Logger, maxErrors uint8) Logger {
54+
if maxErrors == 0 {
55+
maxErrors = 1
56+
}
57+
return &importantLogger{
58+
logger: logger,
59+
maxErrors: maxErrors,
60+
errorCount: 0,
61+
}
62+
}

log/important_logger_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package log_test
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io"
7+
"strings"
8+
"testing"
9+
"time"
10+
11+
"github.com/r3code/go-useful-snippets/log"
12+
)
13+
14+
func fakeNow() time.Time {
15+
timeNow, _ := time.Parse(time.RFC3339, "2014-11-12T11:45:26.371Z")
16+
return timeNow
17+
}
18+
19+
func Test_Logger_Log(t *testing.T) {
20+
var buf bytes.Buffer
21+
logger := log.NewLogger(&buf, 1, false)
22+
23+
if err := logger.Log("hello", "world"); err != nil {
24+
t.Fatal(err)
25+
}
26+
want := "hello=world\n"
27+
have := buf.String()
28+
if !strings.Contains(have, want) {
29+
t.Errorf("\nwant %#v\nhave %#v", want, have)
30+
}
31+
if !strings.Contains(have, "time=") {
32+
t.Errorf("No time= field printed")
33+
}
34+
if !strings.Contains(have, "caller=") {
35+
t.Errorf("No caller= field printed")
36+
}
37+
}
38+
39+
type failingWriter struct {
40+
io.Writer
41+
}
42+
43+
func (w *failingWriter) Write(p []byte) (n int, err error) {
44+
return 0, errors.New("Write failed")
45+
}
46+
47+
func Test_Logger_Log_WriteError(t *testing.T) {
48+
var w failingWriter
49+
logger := log.NewLogger(&w, 3, false)
50+
51+
var initPainc = func() {
52+
for i := 0; i <= 4; i++ {
53+
_ = logger.Log("hello", "world")
54+
}
55+
}
56+
// отложим проверку на возникновение паники, затем вызовем функцию в которой должна быть паника. Если паника не произошла - тест провалится
57+
defer func() {
58+
if r := recover(); r == nil {
59+
t.Errorf("Logger did not panic after 3 errors as it should")
60+
}
61+
}()
62+
63+
initPainc()
64+
}

log/logger.go

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package log
2+
3+
import (
4+
"io"
5+
"os"
6+
"strings"
7+
8+
kitlog "github.com/go-kit/kit/log"
9+
)
10+
11+
// Logger переименовывает (go-kit/log).Logger интерфейс для удобства
12+
type Logger kitlog.Logger
13+
14+
var (
15+
// DefaultCaller указывает на место вызова, добавляет свойство "caller" в лог
16+
DefaultCaller = kitlog.Caller(5)
17+
// DefaultTimestampUTC определяет временую метку, используется в свойстве "ts" в логе
18+
DefaultTimestampUTC = kitlog.DefaultTimestampUTC
19+
)
20+
21+
// NewDefaultStdOutLogger создает логгер с настройками по умолчанию для вывода в STDOUT
22+
func NewDefaultStdOutLogger(disableTimestamp bool) Logger {
23+
var maxErrors uint8 = 10
24+
return NewStdOutLogger(maxErrors, disableTimestamp)
25+
}
26+
27+
// NewStdOutLogger создает логгер для вывода сообщений в STDOUT
28+
func NewStdOutLogger(maxErrors uint8, disableTimestamp bool) Logger {
29+
lg := NewLogger(os.Stdout, maxErrors, disableTimestamp)
30+
31+
return NewImportantLogger(lg, maxErrors)
32+
}
33+
34+
// NewLogger создает новый логер с указанным писателем.
35+
// Параметр `maxErrors` указывает предельное количество ошибок запси в лог после достижения которого будет вызвана паника и выполнение программы прервется
36+
func NewLogger(writer io.Writer, maxErrors uint8, disableTimestamp bool) Logger {
37+
lg := kitlog.NewLogfmtLogger(kitlog.NewSyncWriter(writer))
38+
39+
il := NewImportantLogger(lg, maxErrors)
40+
41+
il = kitlog.WithPrefix(il, "caller", DefaultCaller)
42+
if !disableTimestamp {
43+
il = kitlog.WithPrefix(il, "time", DefaultTimestampUTC)
44+
}
45+
46+
return il
47+
}
48+
49+
// With добавляет новые постоянно добавляемые поля со значениями в сообщение
50+
func With(l Logger, keyvals ...interface{}) Logger {
51+
return kitlog.With(l, keyvals...)
52+
}
53+
54+
// MustCreateComponentLog создает новый логгер для компонента или паникует, если имя не указано
55+
func MustCreateComponentLog(l Logger, componentName string) Logger {
56+
if strings.TrimSpace(componentName) == "" {
57+
panic("Can not create named logger. Empty component name passed")
58+
}
59+
60+
return With(l, "component", componentName)
61+
}

0 commit comments

Comments
 (0)