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 }