1// Copyright (c) 2016 Uber Technologies, Inc. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a copy 4// of this software and associated documentation files (the "Software"), to deal 5// in the Software without restriction, including without limitation the rights 6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7// copies of the Software, and to permit persons to whom the Software is 8// furnished to do so, subject to the following conditions: 9// 10// The above copyright notice and this permission notice shall be included in 11// all copies or substantial portions of the Software. 12// 13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19// THE SOFTWARE. 20 21// Package observer provides a zapcore.Core that keeps an in-memory, 22// encoding-agnostic repesentation of log entries. It's useful for 23// applications that want to unit test their log output without tying their 24// tests to a particular output encoding. 25package observer // import "go.uber.org/zap/zaptest/observer" 26 27import ( 28 "strings" 29 "sync" 30 "time" 31 32 "go.uber.org/zap/zapcore" 33) 34 35// An LoggedEntry is an encoding-agnostic representation of a log message. 36// Field availability is context dependant. 37type LoggedEntry struct { 38 zapcore.Entry 39 Context []zapcore.Field 40} 41 42// ObservedLogs is a concurrency-safe, ordered collection of observed logs. 43type ObservedLogs struct { 44 mu sync.RWMutex 45 logs []LoggedEntry 46} 47 48// Len returns the number of items in the collection. 49func (o *ObservedLogs) Len() int { 50 o.mu.RLock() 51 n := len(o.logs) 52 o.mu.RUnlock() 53 return n 54} 55 56// All returns a copy of all the observed logs. 57func (o *ObservedLogs) All() []LoggedEntry { 58 o.mu.RLock() 59 ret := make([]LoggedEntry, len(o.logs)) 60 for i := range o.logs { 61 ret[i] = o.logs[i] 62 } 63 o.mu.RUnlock() 64 return ret 65} 66 67// TakeAll returns a copy of all the observed logs, and truncates the observed 68// slice. 69func (o *ObservedLogs) TakeAll() []LoggedEntry { 70 o.mu.Lock() 71 ret := o.logs 72 o.logs = nil 73 o.mu.Unlock() 74 return ret 75} 76 77// AllUntimed returns a copy of all the observed logs, but overwrites the 78// observed timestamps with time.Time's zero value. This is useful when making 79// assertions in tests. 80func (o *ObservedLogs) AllUntimed() []LoggedEntry { 81 ret := o.All() 82 for i := range ret { 83 ret[i].Time = time.Time{} 84 } 85 return ret 86} 87 88// FilterMessage filters entries to those that have the specified message. 89func (o *ObservedLogs) FilterMessage(msg string) *ObservedLogs { 90 return o.filter(func(e LoggedEntry) bool { 91 return e.Message == msg 92 }) 93} 94 95// FilterMessageSnippet filters entries to those that have a message containing the specified snippet. 96func (o *ObservedLogs) FilterMessageSnippet(snippet string) *ObservedLogs { 97 return o.filter(func(e LoggedEntry) bool { 98 return strings.Contains(e.Message, snippet) 99 }) 100} 101 102// FilterField filters entries to those that have the specified field. 103func (o *ObservedLogs) FilterField(field zapcore.Field) *ObservedLogs { 104 return o.filter(func(e LoggedEntry) bool { 105 for _, ctxField := range e.Context { 106 if ctxField.Equals(field) { 107 return true 108 } 109 } 110 return false 111 }) 112} 113 114func (o *ObservedLogs) filter(match func(LoggedEntry) bool) *ObservedLogs { 115 o.mu.RLock() 116 defer o.mu.RUnlock() 117 118 var filtered []LoggedEntry 119 for _, entry := range o.logs { 120 if match(entry) { 121 filtered = append(filtered, entry) 122 } 123 } 124 return &ObservedLogs{logs: filtered} 125} 126 127func (o *ObservedLogs) add(log LoggedEntry) { 128 o.mu.Lock() 129 o.logs = append(o.logs, log) 130 o.mu.Unlock() 131} 132 133// New creates a new Core that buffers logs in memory (without any encoding). 134// It's particularly useful in tests. 135func New(enab zapcore.LevelEnabler) (zapcore.Core, *ObservedLogs) { 136 ol := &ObservedLogs{} 137 return &contextObserver{ 138 LevelEnabler: enab, 139 logs: ol, 140 }, ol 141} 142 143type contextObserver struct { 144 zapcore.LevelEnabler 145 logs *ObservedLogs 146 context []zapcore.Field 147} 148 149func (co *contextObserver) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { 150 if co.Enabled(ent.Level) { 151 return ce.AddCore(ent, co) 152 } 153 return ce 154} 155 156func (co *contextObserver) With(fields []zapcore.Field) zapcore.Core { 157 return &contextObserver{ 158 LevelEnabler: co.LevelEnabler, 159 logs: co.logs, 160 context: append(co.context[:len(co.context):len(co.context)], fields...), 161 } 162} 163 164func (co *contextObserver) Write(ent zapcore.Entry, fields []zapcore.Field) error { 165 all := make([]zapcore.Field, 0, len(fields)+len(co.context)) 166 all = append(all, co.context...) 167 all = append(all, fields...) 168 co.logs.add(LoggedEntry{ent, all}) 169 return nil 170} 171 172func (co *contextObserver) Sync() error { 173 return nil 174} 175