github.com/zchee/zap-cloudlogging@v0.0.0-20220819025602-19b026d3900e/cloudlogging.go (about)

     1  // Copyright 2022 The zap-cloudlogging Authors
     2  // SPDX-License-Identifier: BSD-3-Clause
     3  
     4  // Package zapcloudlogging provides the Cloud Logging integration for Zap.
     5  package zapcloudlogging
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"sort"
    12  	"time"
    13  
    14  	json "github.com/goccy/go-json"
    15  	"go.uber.org/zap"
    16  	"go.uber.org/zap/zapcore"
    17  	"golang.org/x/sys/unix"
    18  	logtypepb "google.golang.org/genproto/googleapis/logging/type"
    19  
    20  	"github.com/zchee/zap-cloudlogging/pkg/monitoredresource"
    21  )
    22  
    23  var levelToSeverity = map[zapcore.Level]logtypepb.LogSeverity{
    24  	zapcore.DebugLevel:  logtypepb.LogSeverity_DEBUG,
    25  	zapcore.InfoLevel:   logtypepb.LogSeverity_INFO,
    26  	zapcore.WarnLevel:   logtypepb.LogSeverity_WARNING,
    27  	zapcore.ErrorLevel:  logtypepb.LogSeverity_ERROR,
    28  	zapcore.DPanicLevel: logtypepb.LogSeverity_CRITICAL,
    29  	zapcore.PanicLevel:  logtypepb.LogSeverity_ALERT,
    30  	zapcore.FatalLevel:  logtypepb.LogSeverity_EMERGENCY,
    31  }
    32  
    33  func encoderConfig() zapcore.EncoderConfig {
    34  	return zapcore.EncoderConfig{
    35  		TimeKey:        "eventTime",
    36  		LevelKey:       "severity",
    37  		NameKey:        "logger",
    38  		CallerKey:      "caller",
    39  		MessageKey:     "message",
    40  		StacktraceKey:  "stacktrace",
    41  		LineEnding:     zapcore.DefaultLineEnding,
    42  		EncodeLevel:    levelEncoder,
    43  		EncodeTime:     rfc3339NanoTimeEncoder,
    44  		EncodeDuration: zapcore.SecondsDurationEncoder,
    45  		EncodeCaller:   zapcore.ShortCallerEncoder,
    46  		NewReflectedEncoder: func(w io.Writer) zapcore.ReflectedEncoder {
    47  			enc := json.NewEncoder(w)
    48  			enc.SetEscapeHTML(false)
    49  			return enc
    50  		},
    51  	}
    52  }
    53  
    54  func levelEncoder(lvl zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
    55  	enc.AppendString(levelToSeverity[lvl].Enum().String())
    56  }
    57  
    58  // rfc3339NanoTimeEncoder serializes a time.Time to an RFC3339Nano-formatted
    59  // string with nanoseconds precision.
    60  //
    61  // The Cloud Logging timestamp field spec is RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits.
    62  //
    63  // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#FIELDS.timestamp
    64  func rfc3339NanoTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    65  	enc.AppendString(t.Format(time.RFC3339Nano))
    66  }
    67  
    68  type nopWriteSyncer struct {
    69  	io.Writer
    70  }
    71  
    72  func (nopWriteSyncer) Sync() error { return nil }
    73  
    74  // Core represents a zapcor.Core that is Cloud Logging integration for Zap logger.
    75  type Core struct {
    76  	zapcore.LevelEnabler
    77  
    78  	enc        zapcore.Encoder
    79  	ws         zapcore.WriteSyncer
    80  	fields     []zapcore.Field
    81  	initFields map[string]interface{}
    82  }
    83  
    84  var _ zapcore.Core = (*Core)(nil)
    85  
    86  func (c *Core) clone() *Core {
    87  	newCore := &Core{
    88  		fields: make([]zapcore.Field, len(c.fields)),
    89  		enc:    c.enc.Clone(),
    90  		ws:     c.ws,
    91  	}
    92  	copy(newCore.fields, c.fields)
    93  
    94  	return newCore
    95  }
    96  
    97  func addFields(enc zapcore.ObjectEncoder, fields []zapcore.Field) {
    98  	for i := range fields {
    99  		fields[i].AddTo(enc)
   100  	}
   101  }
   102  
   103  // With adds structured context to the Core.
   104  //
   105  // With implements zapcore.Core.With.
   106  func (c *Core) With(fields []zapcore.Field) zapcore.Core {
   107  	clone := c.clone()
   108  	addFields(clone.enc, fields)
   109  
   110  	return clone
   111  }
   112  
   113  // Check determines whether the supplied Entry should be logged (using the
   114  // embedded LevelEnabler and possibly some extra logic). If the entry
   115  // should be logged, the Core adds itself to the CheckedEntry and returns
   116  // the result.
   117  //
   118  // Check implements zapcore.Core.Check.
   119  func (c *Core) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
   120  	if c.Enabled(entry.Level) {
   121  		return ce.AddCore(entry, c)
   122  	}
   123  
   124  	return ce
   125  }
   126  
   127  // Write serializes the Entry and any Fields supplied at the log site and
   128  // writes them to their destination.
   129  //
   130  // Write implemenns zapcore.Core.Write.
   131  func (c *Core) Write(ent zapcore.Entry, fields []zapcore.Field) error {
   132  	for _, field := range c.fields {
   133  		field.AddTo(c.enc)
   134  	}
   135  
   136  	buf, err := c.enc.EncodeEntry(ent, fields)
   137  	if err != nil {
   138  		return fmt.Errorf("could not encode entry: %w", err)
   139  	}
   140  
   141  	_, err = c.ws.Write(buf.Bytes())
   142  	buf.Free()
   143  	if err != nil {
   144  		return fmt.Errorf("could not write buf: %w", err)
   145  	}
   146  
   147  	if ent.Level > zapcore.ErrorLevel {
   148  		// Since we may be crashing the program, sync the output. Ignore Sync
   149  		// errors, pending a clean solution to issue #370.
   150  		c.Sync()
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  // Sync flushes buffered logs if any.
   157  //
   158  // Sync implemenns zapcore.Core.Sync.
   159  func (c *Core) Sync() error {
   160  	if err := c.ws.Sync(); err != nil {
   161  		if !knownSyncError(err) {
   162  			return fmt.Errorf("faild to sync logger: %w", err)
   163  		}
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  // knownSyncError reports whether the given error is one of the known
   170  // non-actionable errors returned by Sync on Linux and macOS.
   171  //
   172  // Linux:
   173  // - sync /dev/stdout: invalid argument
   174  //
   175  // macOS:
   176  // - sync /dev/stdout: inappropriate ioctl for device
   177  //
   178  // This code was borrowed from:
   179  // - https://github.com/open-telemetry/opentelemetry-collector/blob/v0.46.0/exporter/loggingexporter/known_sync_error.go#L24-L39.
   180  func knownSyncError(err error) bool {
   181  	switch {
   182  	case errors.Is(err, unix.EINVAL),
   183  		errors.Is(err, unix.ENOTSUP),
   184  		errors.Is(err, unix.ENOTTY),
   185  		errors.Is(err, unix.EBADF):
   186  
   187  		return true
   188  	}
   189  
   190  	return false
   191  }
   192  
   193  // Option configures a core.
   194  type Option interface {
   195  	apply(*Core)
   196  }
   197  
   198  // optionFunc wraps a func so it satisfies the Option interface.
   199  type optionFunc func(*Core)
   200  
   201  func (f optionFunc) apply(c *Core) {
   202  	f(c)
   203  }
   204  
   205  // WithInitialFields configures the zap InitialFields.
   206  func WithInitialFields(fields map[string]interface{}) Option {
   207  	return optionFunc(func(c *Core) {
   208  		c.initFields = fields
   209  	})
   210  }
   211  
   212  // WithWriteSyncer configures the zapcore.WriteSyncer.
   213  func WithWriteSyncer(ws zapcore.WriteSyncer) Option {
   214  	return optionFunc(func(c *Core) {
   215  		c.ws = ws
   216  	})
   217  }
   218  
   219  func newCore(ws zapcore.WriteSyncer, enab zapcore.LevelEnabler, opts ...Option) *Core {
   220  	core := &Core{
   221  		LevelEnabler: enab,
   222  		enc:          zapcore.NewJSONEncoder(encoderConfig()),
   223  		ws:           ws,
   224  	}
   225  	for _, opt := range opts {
   226  		opt.apply(core)
   227  	}
   228  
   229  	res := monitoredresource.Detect()
   230  	core.fields = []zapcore.Field{
   231  		zap.String(res.Type, res.LogID),
   232  		zap.Inline(res),
   233  	}
   234  
   235  	// handling initFields option
   236  	if len(core.initFields) > 0 {
   237  		fs := make([]zapcore.Field, 0, len(core.initFields))
   238  		keys := make([]string, 0, len(core.initFields))
   239  		for k := range core.initFields {
   240  			keys = append(keys, k)
   241  		}
   242  		sort.Strings(keys)
   243  
   244  		for _, k := range keys {
   245  			fs = append(fs, zap.Any(k, core.initFields[k]))
   246  		}
   247  		core.fields = append(core.fields, fs...)
   248  	}
   249  
   250  	return core
   251  }
   252  
   253  // NewCore creates a Core that writes logs to a WriteSyncer.
   254  func NewCore(ws zapcore.WriteSyncer, enab zapcore.LevelEnabler, opts ...Option) zapcore.Core {
   255  	core := newCore(ws, enab, opts...)
   256  
   257  	return zapcore.NewCore(core.enc, core.ws, core.LevelEnabler)
   258  }
   259  
   260  // WrapCore wraps or replaces the Logger's underlying zapcore.Core.
   261  func WrapCore(opts ...Option) zap.Option {
   262  	return zap.WrapCore(func(c zapcore.Core) zapcore.Core {
   263  		core := newCore(nopWriteSyncer{}, c, opts...)
   264  
   265  		return zapcore.NewCore(core.enc, core.ws, core.LevelEnabler)
   266  	})
   267  }