Правильная поза использования библиотеки журналов
что такое журнал
Под так называемым журналом (Log) понимается упорядоченная по времени совокупность определенных операций заданных системой объектов и результатов их работы. Файл журнала — это файл журнала.В файл журнала записывается информация о взаимодействии между системой и пользователями системы, и это метод сбора данных, который автоматически фиксирует тип, содержание или время взаимодействия между людьми и системой. Терминал;
Журналы — это файлы, которые используются для записи операций пользователя, состояния системы, сообщений об ошибках и т. д. и являются важной частью программной системы. Хорошая спецификация журнала имеет большое значение для анализа рабочего состояния системы и решения онлайн-проблем.
спецификация журнала
При разработке программного обеспечения для печати журналов необходимо обратить внимание на некоторые проблемы.Примеры могут быть неполными.Вы можете прочитать статьи, связанные с Baidu, самостоятельно или ознакомиться с литературой внизу статьи.
- Журналы важных функций максимально полны.
- Не распечатывайте бесполезные журналы по своему усмотрению. Слишком большое количество бесполезных журналов усложнит анализ журналов.
- Журналы должны быть классифицированы по уровням, таким как отладка, предупреждение, информация, ошибка и т. д.
- Лучше распечатать информацию о стеке ошибок при обнаружении необработанной ошибки.
Библиотека ведения журнала, обычно используемая в языке Go
Стандартная библиотека языка Go предоставляет нам библиотеку ведения журналов.log, вдобавок к этому есть множество библиотек логов, напримерlogrus,glog,logx,UberизzapПодождите, сегодня мы в основном представляемzap, будет использоваться далее в статьеzapвыполнитьkratosизlog
Zap
преимущество
- высокая производительность
- Богатые элементы конфигурации
- Несколько уровней журнала
- Крюк поддержки
- Богатый инструментарий
- Предоставляет журнал сахара
- Несколько форматов печати журнала
- ...
Простой в использовании
package main
import (
"errors"
"go.uber.org/zap"
)
var logger *zap.Logger
func init() {
logger, _ = zap.NewProduction()
}
func main() {
logger.Error(
"My name is baobao",
zap.String("from", "Hulun Buir"),
zap.Error(errors.New("no good")))
logger.Info("Worked in the Ministry of national development of China!",
zap.String("key", "eat🍚"),
zap.String("key", "sleep😴"))
defer logger.Sync()
}
Принцип анализа библиотеки журналов Kratos
наедине сУчитель ТониОбсуждая концепцию реализации библиотеки журналов,Учитель ТониСказал: Поскольку текущая библиотека журналов очень большая и простая в использовании, вKratosВ логе в основном рассматриваются следующие вопросы
- Унифицированный дизайн интерфейса журнала
- Организуйте структурированные журналы
- И нужно иметь дружественное использование уровня журнала
- Поддержка требований к стыковке нескольких источников вывода, таких как log-agent или третья библиотека журналов.
kratosБиблиотека журналов не требует определенного метода реализации. Она предоставляет только адаптеры. Пользователи могут реализовать функцию журнала самостоятельно, и им нужно только реализоватьkratos/logизLogger interfaceВы можете получить доступ к своей любимой системе журналов
kratosНа этапе проектирования библиотека журналов . Если вы не хотите видеть анализ исходного кода, вы можете не копировать домашнюю работу напрямую.
Состав лог-библиотеки
kratosизlogБиблиотека в основном состоит из следующих файлов
- level.goопределить уровень журнала
- log.goбревенчатый керн
- helper.go logизhelper
- value.goРеализовать динамические значения
Анализ исходного кода
kratosизlogВ библиотеке основной частью являетсяlog.goКод очень лаконичен и соответствуетkratosконцепт дизайна.log.goобъявлено вLogger interface, пользователям нужно только реализовать интерфейс, они могут ввести свою собственную реализацию журнала, основной код выглядит следующим образом
log.go
package log
import (
"context"
"log"
)
var (
// DefaultLogger is default logger.
DefaultLogger Logger = NewStdLogger(log.Writer())
)
// Logger 接口, 后面实现自定义日志库的时候,就是要实现这个接口。
type Logger interface {
Log(level Level, keyvals ...interface{}) error
}
type logger struct {
logs []Logger // logger 数组
prefix []interface{} // 一些默认打印的值,例如通过 With 绑定的 Valuer
hasValuer bool // 是否包含 Valuer
ctx context.Context // 上下文
}
func (c *logger) Log(level Level, keyvals ...interface{}) error {
kvs := make([]interface{}, 0, len(c.prefix)+len(keyvals))
kvs = append(kvs, c.prefix...)
// 判断是否存在 valuer
if c.hasValuer {
// 绑定 valuer
bindValues(c.ctx, kvs)
}
kvs = append(kvs, keyvals...)
// 遍历 logs,调用所有的 logger 进行日志打印。
for _, l := range c.logs {
if err := l.Log(level, kvs...); err != nil {
return err
}
}
return nil
}
// With with logger fields.
func With(l Logger, kv ...interface{}) Logger {
// 判断是否能 把传入的 logger 断言成 *logger
if c, ok := l.(*logger); ok {
// 预分配内存,make了一个空间长度为 c.prefix + keyvals长度的 interface数组
kvs := make([]interface{}, 0, len(c.prefix)+len(kv))
// 处理打印的内容
kvs = append(kvs, kv...)
kvs = append(kvs, c.prefix...)
// containsValuer()用来判断 kvs 里面是否存在 valuer
return &logger{
logs: c.logs,
prefix: kvs,
hasValuer: containsValuer(kvs),
ctx: c.ctx,
}
}
return &logger{logs: []Logger{l}, prefix: kv, hasValuer: containsValuer(kv)}
}
// WithContext 绑定 ctx,注意 ctx 必须非空
func WithContext(ctx context.Context, l Logger) Logger {
if c, ok := l.(*logger); ok {
return &logger{
logs: c.logs,
prefix: c.prefix,
hasValuer: c.hasValuer,
ctx: ctx,
}
}
return &logger{logs: []Logger{l}, ctx: ctx}
}
// MultiLogger 包装多个logger,简单说就是同时使用多个logger打印
func MultiLogger(logs ...Logger) Logger {
return &logger{logs: logs}
}
value.go
// 返回 valuer 函数.
func Value(ctx context.Context, v interface{}) interface{} {
if v, ok := v.(Valuer); ok {
return v(ctx)
}
return v
}
// ...省略一些内置的 valuer 实现
// 绑定 valuer
func bindValues(ctx context.Context, keyvals []interface{}) {
for i := 1; i < len(keyvals); i += 2 {
if v, ok := keyvals[i].(Valuer); ok {
keyvals[i] = v(ctx)
}
}
}
// 是否包含 valuer
func containsValuer(keyvals []interface{}) bool {
for i := 1; i < len(keyvals); i += 2 {
if _, ok := keyvals[i].(Valuer); ok {
return true
}
}
return false
}
helper.go
package log
import (
"context"
"fmt"
)
// Helper is a logger helper.
type Helper struct {
logger Logger
}
// 创建一个 logger helper 实例
func NewHelper(logger Logger) *Helper {
return &Helper{
logger: logger,
}
}
// 通过 WithContext() 返回包含 ctx 的一个日志的帮助类,包含一些定义好的按级别打印日志的方法
func (h *Helper) WithContext(ctx context.Context) *Helper {
return &Helper{
logger: WithContext(ctx, h.logger),
}
}
func (h *Helper) Log(level Level, keyvals ...interface{}) {
h.logger.Log(level, keyvals...)
}
func (h *Helper) Debug(a ...interface{}) {
h.logger.Log(LevelDebug, "msg", fmt.Sprint(a...))
}
func (h *Helper) Debugf(format string, a ...interface{}) {
h.logger.Log(LevelDebug, "msg", fmt.Sprintf(format, a...))
}
// ...省略一些重复的方法
Понять логику вызова с помощью модульного тестирования
func TestInfo(t *testing.T) {
logger := DefaultLogger
logger = With(logger, "ts", DefaultTimestamp, "caller", DefaultCaller)
logger.Log(LevelInfo, "key1", "value1")
}
- Одиночный тест сначала объявляетlogger, используя значение по умолчаниюDefaultLogger
- перечислитьlog.goсерединаWith()функция, переданная вloggerи два динамических значения,DefaultTimestampа такжеDefaultCaller
- Метод With вызывается, чтобы определить, можно ли передать параметрlпреобразование типа в*logger
- Если его можно преобразовать, назначьте входящий KV наlogger.prefixвключи, а потом звониvalue.goсерединаcontainsValuer()Определить, есть ли во входящем KV значение типа Valuer, и присвоить результатcontext.hasValuer, и, наконец, вернутьсяLoggerобъект
- В противном случае напрямую верните&logger{logs: []Logger{l}, prefix: kv, hasValuer: containsValuer(kv)}
- Затем, когда журнал печатается,logger structизLogметод называется
-
Log()метод сначала предварительно выделенkeyvalsпространство, а затем судитьhasValuer, еслиtrue, затем позвонитеvaluer.goсерединаbindValuer()и прошел вctxзатем получитьvaluerзначение
if v, ok := v.(Valuer); ok { return v() }
8. Последний обходlogger.logsжурнал печати
Как использовать ctx в логах
Если вам нужно использовать ctx при реализации журнала, вы можете использовать метод WithContext(), передать ctx, а затем делать то, что хотите.
Например
// 实现一个在日志中打印 traceID 的例子
func TraceID() Valuer {
return func(ctx context.Context) interface{} {
if span := trace.SpanContextFromContext(ctx); span.HasTraceID() {
return span.TraceID().String()
}
return ""
}
}
// 使用方法
logger := DefaultLogger
ctx := context.Background()
var trace = TraceID()
logger = With(logger,"traceId",trace)
WithContext(ctx,logger).Log(LevelInfo,"print trace_id")
Внедрение системы ведения журнала Кратоса с помощью Zap
Реализованный код очень прост, менее 100 строк кода, он не очень хорошо написан, он предназначен только для справки.
выполнить
// kratos/examples/log/zap.go
package logger
import (
"fmt"
"os"
"github.com/go-kratos/kratos/v2/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var _ log.Logger = (*ZapLogger)(nil)
// Zap 结构体
type ZapLogger struct {
log *zap.Logger
Sync func() error
}
// 创建一个 ZapLogger 实例
func NewZapLogger(encoder zapcore.EncoderConfig, level zap.AtomicLevel, opts ...zap.Option) *ZapLogger {
writeSyncer := getLogWriter()
// 设置 zapcore
core := zapcore.NewCore(
zapcore.NewConsoleEncoder(encoder),
zapcore.NewMultiWriteSyncer(
zapcore.AddSync(os.Stdout),
), level)
// new 一个 *zap.Logger
zapLogger := zap.New(core, opts...)
return &ZapLogger{log: zapLogger, Sync: zapLogger.Sync}
}
// Log 方法实现了 kratos/log/log.go 中的 Logger interface
func (l *ZapLogger) Log(level log.Level, keyvals ...interface{}) error {
if len(keyvals) == 0 || len(keyvals)%2 != 0{
l.log.Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals))
return nil
}
// 按照 KV 传入的时候,使用的 zap.Field
var data []zap.Field
for i := 0; i < len(keyvals); i += 2 {
data = append(data, zap.Any(fmt.Sprint(keyvals[i]), fmt.Sprint(keyvals[i+1])))
}
switch level {
case log.LevelDebug:
l.log.Debug("", data...)
case log.LevelInfo:
l.log.Info("", data...)
case log.LevelWarn:
l.log.Warn("", data...)
case log.LevelError:
l.log.Error("", data...)
}
return nil
}
// 日志自动切割,采用 lumberjack 实现的
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "./test.log",
MaxSize: 10,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}
инструкции
// kratos/examples/log/zap_test.go
package logger
import (
"testing"
"github.com/go-kratos/kratos/v2/log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func TestZapLogger(t *testing.T) {
encoder := zapcore.EncoderConfig{
TimeKey: "t",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stack",
EncodeTime: zapcore.ISO8601TimeEncoder,
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.FullCallerEncoder,
}
logger := NewZapLogger(
encoder,
zap.NewAtomicLevelAt(zapcore.DebugLevel),
zap.AddStacktrace(
zap.NewAtomicLevelAt(zapcore.ErrorLevel)),
zap.AddCallerSkip(2),
zap.Development(),
)
zlog := log.NewHelper(logger)
zlog.Infow("name","baozi","from","hulunbeier")
defer logger.Sync()
}
использованная литература
- Обсуждение библиотеки журналовissue
- Библиотека журналов Uber Zapuber/zap
- библиотека сокращения журналаlumberjack
- демонстрация журнала на основе zaplog example