Skip to content

Zap

Zap هو مكوّن سجلات سريع، منظم، ومتعدد المستويات مبني بلغة Go.

المستودع الرسمي: uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)

الوثائق الرسمية: zap package - go.uber.org/zap - Go Packages

التثبيت

go get -u go.uber.org/zap

البدء السريع

قدم المسؤول مثالين للبدء السريع، كلاهما سجلات على مستوى الإنتاج، الأول هو Sugar الذي يدعم نمط printf لكن بأداء أقل نسبيًا.

go
logger, _ := zap.NewProduction()
defer logger.Sync() // مزامنة المخزن المؤقت إلى الملف عند انتهاء البرنامج
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

الثاني هو logger ذو الأداء الأفضل، لكنه يدعم فقط إخراج الأنواع القوية

go
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
  // Structured context as strongly typed Field values.
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

TIP

استخدام Zap بسيط جدًا، لكن التعقيد يكمن في تكوين سجلات مناسبة لمشروعك، والأمثلة الرسمية قليلة، لذا يجب قراءة تعليقات الكود المصدري بعناية.

التكوين

عمومًا يتم كتابة تكوين السجلات في ملف التكوين، ويدعم تكوين Zap أيضًا إلغاء التسلسل من ملف التكوين، لكنه يدعم فقط التكوينات الأساسية، وحتى التكوينات المتقدمة التي يقدمها المسؤول بسيطة جدًا ولا تكفي للاستخدام الفعلي، لذا سنشرح تفاصيل التكوين بالتفصيل.

أولاً لنلقِ نظرة على بنية التكوين العامة، يجب فهم معنى كل حقل

go
type Config struct {
    // أدنى مستوى للسجلات
   Level AtomicLevel `json:"level" yaml:"level"`
    // وضع التطوير، يؤثر بشكل رئيسي على تتبع المكدس
   Development bool `json:"development" yaml:"development"`
    // تتبع المستدعي
   DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // تتبع المكدس
   DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // العينات، في حالة تقييد تأثير السجلات على الأداء يتم تسجيل جزء فقط من السجلات التمثيلية
   Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // الترميز، ينقسم إلى وضعي json و console
   Encoding string `json:"encoding" yaml:"encoding"`
    // تكوين الترميز، بشكل رئيسي بعض تكوينات تنسيق الإخراج
   EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // مسار إخراج ملف السجلات
   OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // مسار إخراج ملف الأخطاء
   ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // إضافة بعض المحتوى الافتراضي للسجلات
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

فيما يلي تفاصيل تكوين الترميز

go
type EncoderConfig struct {
   // المفتاح، إذا كان المفتاح فارغًا، لن يتم إخراج الخاصية المقابلة
   MessageKey     string `json:"messageKey" yaml:"messageKey"`
   LevelKey       string `json:"levelKey" yaml:"levelKey"`
   TimeKey        string `json:"timeKey" yaml:"timeKey"`
   NameKey        string `json:"nameKey" yaml:"nameKey"`
   CallerKey      string `json:"callerKey" yaml:"callerKey"`
   FunctionKey    string `json:"functionKey" yaml:"functionKey"`
   StacktraceKey  string `json:"stacktraceKey" yaml:"stacktraceKey"`
   SkipLineEnding bool   `json:"skipLineEnding" yaml:"skipLineEnding"`
   LineEnding     string `json:"lineEnding" yaml:"lineEnding"`
   // بعض المشفرات المخصصة
   EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
   EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
   EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
   EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
   // مشفر اسم المسجل
   EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
   // مشفر الانعكاس، بشكل رئيسي للنوع interface{}، إذا لم يكن موجودًا يكون الافتراضي jsonencoder
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // سلسلة فصل إخراج وحدة التحكم
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option يتعلق ببعض مفاتيح التكوين وتطبيقها، ولديه العديد من التطبيقات.

go
type Option interface {
   apply(*Logger)
}

// تطبيق Option
type optionFunc func(*Logger)

func (f optionFunc) apply(log *Logger) {
  f(log)
}

// التطبيق
func Development() Option {
  return optionFunc(func(log *Logger) {
    log.development = true
  })
}

هذا هو الجوهر الأساسي للسجلات الأكثر استخدامًا، وحقوله الداخلية تمثل بشكل أساسي خطوات تكويننا، ويمكن أيضًا الرجوع إلى خطوات المسؤول عند إلغاء التسلسل، وهي تقريبًا متشابهة.

go
type ioCore struct {
   // مستوى السجلات
   LevelEnabler
   // ترميز السجلات
   enc Encoder
   // كتابة السجلات
   out WriteSyncer
}

zap.Encoder مسؤول عن تنسيق السجلات وترميزها

zap.WriteSyncer مسؤول عن إخراج السجلات، بشكل رئيسي الإخراج إلى الملف ووحدة التحكم

zap.LevelEnabler أدنى مستوى للسجلات، السجلات دون هذا المستوى لن تُخرج عبر syncer.

ترميز السجلات

يتعلق ترميز السجلات بشكل رئيسي ببعض تنسيقات التفاصيل للسجلات، أولاً لنلقِ نظرة على الإخراج باستخدام السجلات الأصلية مباشرة.

go
func TestQuickStart(t *testing.T) {
   rawJSON := []byte(`{
     "level": "debug",
     "encoding": "json",
     "outputPaths": ["stdout"],
     "errorOutputPaths": ["stderr"],
     "initialFields": {"foo": "bar"},
     "encoderConfig": {
       "messageKey": "message",
       "levelKey": "level",
       "levelEncoder": "lowercase"
     }
   }`)

   var cfg zap.Config
   if err := json.Unmarshal(rawJSON, &cfg); err != nil {
      panic(err)
   }
   logger := zap.Must(cfg.Build())
   defer logger.Sync()

   logger.Info("logger construction succeeded")
}
{"level":"info","message":"logger construction succeeded","foo":"bar"}

ستجد أن هذا السطر به عدة مشاكل:

  • لا يوجد وقت
  • لا يوجد موضع المستدعي، لا نعرف من أين أُخرج هذا السطر، وبالتالي عند حدوث خطأ لا يمكن تتبعه
  • لا يوجد حالة المكدس

الآن سنحل المشاكل خطوة بخطوة، بشكل رئيسي بتعديل zapcore.EncoderConfig، أولاً يجب علينا كتابة ملف التكوين بأنفسنا، دون استخدام إلغاء التسلسل المباشر من المسؤول. أولاً ننشئ ملف تكوين config.yml

yml
# تكوين سجلات Zap
zap:
  prefix: ZapLogTest
  timeFormat: 2006/01/02 - 15:04:05.00000
  level: debug
  caller: true
  stackTrace: false
  encode: console
  # أين تُخرج السجلات file | console | both
  writer: both
  logFile:
    maxSize: 20
    backups: 5
    compress: true
    output:
      - "./log/output.log"

البنية المُرتبطة

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: بنية تكوين سجلات zap
type ZapConfig struct {
  Prefix     string         `yaml:"prefix" mapstructure:""prefix`
  TimeFormat string         `yaml:"timeFormat" mapstructure:"timeFormat"`
  Level      string         `yaml:"level" mapstructure:"level"`
  Caller     bool           `yaml:"caller" mapstructure:"caller"`
  StackTrace bool           `yaml:"stackTrace" mapstructure:"stackTrace"`
  Writer     string         `yaml:"writer" mapstructure:"writer"`
  Encode     string         `yaml:"encode" mapstructure:"encode"`
  LogFile    *LogFileConfig `yaml:"logFile" mapstructure:"logFile"`
}

// LogFileConfig
// @Date: 2023-01-09 16:38:45
// @Description: بنية تكوين ملف السجلات
type LogFileConfig struct {
  MaxSize  int      `yaml:"maxSize" mapstructure:"maxSize"`
  BackUps  int      `yaml:"backups" mapstructure:"backups"`
  Compress bool     `yaml:"compress" mapstructure:"compress"`
  Output   []string `yaml:"output" mapstructure:"output"`
  Errput   []string `yaml:"errput" mapstructure:"errput"`
}

TIP

استخدم Viper لقراءة التكوين، الكود المحدد محذوف.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimerEncoder في جوهره هو دالة، يمكننا استخدام مشفرات الوقت الأخرى التي يوفرها المسؤول، أو يمكننا كتابته بأنفسنا.

go
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
   encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}

الجزء الكلي كالتالي

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // إنشاء تكوين جديد
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // تنسيق وقت مخصص
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // مستوى السجلات بأحرف كبيرة
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // فاصل زمني بالثواني
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // إخراج مستدعي مختصر
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // تسلسل كامل لاسم المسجل
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // ترميز السجلات النهائي json أو console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // الافتراضي console
   return zapcore.NewConsoleEncoder(encoderConfig)
}

إخراج السجلات

ينقسم إخراج السجلات إلى إخراج وحدة التحكم وإخراج الملف، يمكننا التكوين ديناميكيًا وفقًا لملف التكوين، وإذا أردنا تقسيم ملفات السجلات نحتاج إلى استخدام اعتماد طرف ثالث آخر.

go get -u github.com/natefinch/lumberjack

الكود النهائي كالتالي

go
 func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // إذا تم تفعيل إخراج وحدة التحكم للسجلات، أضف كاتب وحدة التحكم
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // إذا تم تفعيل تخزين ملف السجلات، أضف الكاتب وفقًا لشريحة مسار الملف
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // إضافة مُخرج السجلات
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, // مسار الملف
            MaxSize:    cfg.LogFile.MaxSize, // حجم ملف التقسيم
            MaxBackups: cfg.LogFile.BackUps, // عدد النسخ الاحتياطية
            Compress:   cfg.LogFile.Compress, // هل يتم الضغط
            LocalTime:  true, // استخدام الوقت المحلي
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

مستوى السجلات

لدى المسؤول عناصر تعداد لمستوى السجلات، استخدمها مباشرة.

go
func zapLevelEnabler(cfg *ZapConfig) zapcore.LevelEnabler {
   switch cfg.Level {
   case config.DebugLevel:
      return zap.DebugLevel
   case config.InfoLevel:
      return zap.InfoLevel
   case config.ErrorLevel:
      return zap.ErrorLevel
   case config.PanicLevel:
      return zap.PanicLevel
   case config.FatalLevel:
      return zap.FatalLevel
   }
   // المستوى الافتراضي Debug
   return zap.DebugLevel
}

البناء النهائي

go
func InitZap(config *ZapConfig) *zap.Logger {
   // بناء المشفر
   encoder := zapEncoder(config)
   // بناء مستوى السجلات
   levelEnabler := zapLevelEnabler(config)
   // الحصول على Core و Options
   subCore, options := tee(config, encoder, levelEnabler)
    // إنشاء Logger
   return zap.New(subCore, options...)
}

// دمج الكل
func tee(cfg *ZapConfig, encoder zapcore.Encoder, levelEnabler zapcore.LevelEnabler) (core zapcore.Core, options []zap.Option) {
   sink := zapWriteSyncer(cfg)
   return zapcore.NewCore(encoder, sink, levelEnabler), buildOptions(cfg, levelEnabler)
}

// بناء Option
func buildOptions(cfg *ZapConfig, levelEnabler zapcore.LevelEnabler) (options []zap.Option) {
   if cfg.Caller {
      options = append(options, zap.AddCaller())
   }

   if cfg.StackTrace {
      options = append(options, zap.AddStacktrace(levelEnabler))
   }
   return
}

التأثير النهائي

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     اكتملت تهيئة السجلات

Golang تم تحريره بواسطة www.golangdev.cn