| Abhay Kumar | a61c522 | 2025-11-10 07:32:50 +0000 | [diff] [blame^] | 1 | // Copyright (c) 2016-2022 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 representation 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. |
| 25 | package observer // import "go.uber.org/zap/zaptest/observer" |
| 26 | |
| 27 | import ( |
| 28 | "strings" |
| 29 | "sync" |
| 30 | "time" |
| 31 | |
| 32 | "go.uber.org/zap/internal" |
| 33 | "go.uber.org/zap/zapcore" |
| 34 | ) |
| 35 | |
| 36 | // ObservedLogs is a concurrency-safe, ordered collection of observed logs. |
| 37 | type ObservedLogs struct { |
| 38 | mu sync.RWMutex |
| 39 | logs []LoggedEntry |
| 40 | } |
| 41 | |
| 42 | // Len returns the number of items in the collection. |
| 43 | func (o *ObservedLogs) Len() int { |
| 44 | o.mu.RLock() |
| 45 | n := len(o.logs) |
| 46 | o.mu.RUnlock() |
| 47 | return n |
| 48 | } |
| 49 | |
| 50 | // All returns a copy of all the observed logs. |
| 51 | func (o *ObservedLogs) All() []LoggedEntry { |
| 52 | o.mu.RLock() |
| 53 | ret := make([]LoggedEntry, len(o.logs)) |
| 54 | copy(ret, o.logs) |
| 55 | o.mu.RUnlock() |
| 56 | return ret |
| 57 | } |
| 58 | |
| 59 | // TakeAll returns a copy of all the observed logs, and truncates the observed |
| 60 | // slice. |
| 61 | func (o *ObservedLogs) TakeAll() []LoggedEntry { |
| 62 | o.mu.Lock() |
| 63 | ret := o.logs |
| 64 | o.logs = nil |
| 65 | o.mu.Unlock() |
| 66 | return ret |
| 67 | } |
| 68 | |
| 69 | // AllUntimed returns a copy of all the observed logs, but overwrites the |
| 70 | // observed timestamps with time.Time's zero value. This is useful when making |
| 71 | // assertions in tests. |
| 72 | func (o *ObservedLogs) AllUntimed() []LoggedEntry { |
| 73 | ret := o.All() |
| 74 | for i := range ret { |
| 75 | ret[i].Time = time.Time{} |
| 76 | } |
| 77 | return ret |
| 78 | } |
| 79 | |
| 80 | // FilterLevelExact filters entries to those logged at exactly the given level. |
| 81 | func (o *ObservedLogs) FilterLevelExact(level zapcore.Level) *ObservedLogs { |
| 82 | return o.Filter(func(e LoggedEntry) bool { |
| 83 | return e.Level == level |
| 84 | }) |
| 85 | } |
| 86 | |
| 87 | // FilterMessage filters entries to those that have the specified message. |
| 88 | func (o *ObservedLogs) FilterMessage(msg string) *ObservedLogs { |
| 89 | return o.Filter(func(e LoggedEntry) bool { |
| 90 | return e.Message == msg |
| 91 | }) |
| 92 | } |
| 93 | |
| 94 | // FilterMessageSnippet filters entries to those that have a message containing the specified snippet. |
| 95 | func (o *ObservedLogs) FilterMessageSnippet(snippet string) *ObservedLogs { |
| 96 | return o.Filter(func(e LoggedEntry) bool { |
| 97 | return strings.Contains(e.Message, snippet) |
| 98 | }) |
| 99 | } |
| 100 | |
| 101 | // FilterField filters entries to those that have the specified field. |
| 102 | func (o *ObservedLogs) FilterField(field zapcore.Field) *ObservedLogs { |
| 103 | return o.Filter(func(e LoggedEntry) bool { |
| 104 | for _, ctxField := range e.Context { |
| 105 | if ctxField.Equals(field) { |
| 106 | return true |
| 107 | } |
| 108 | } |
| 109 | return false |
| 110 | }) |
| 111 | } |
| 112 | |
| 113 | // FilterFieldKey filters entries to those that have the specified key. |
| 114 | func (o *ObservedLogs) FilterFieldKey(key string) *ObservedLogs { |
| 115 | return o.Filter(func(e LoggedEntry) bool { |
| 116 | for _, ctxField := range e.Context { |
| 117 | if ctxField.Key == key { |
| 118 | return true |
| 119 | } |
| 120 | } |
| 121 | return false |
| 122 | }) |
| 123 | } |
| 124 | |
| 125 | // Filter returns a copy of this ObservedLogs containing only those entries |
| 126 | // for which the provided function returns true. |
| 127 | func (o *ObservedLogs) Filter(keep func(LoggedEntry) bool) *ObservedLogs { |
| 128 | o.mu.RLock() |
| 129 | defer o.mu.RUnlock() |
| 130 | |
| 131 | var filtered []LoggedEntry |
| 132 | for _, entry := range o.logs { |
| 133 | if keep(entry) { |
| 134 | filtered = append(filtered, entry) |
| 135 | } |
| 136 | } |
| 137 | return &ObservedLogs{logs: filtered} |
| 138 | } |
| 139 | |
| 140 | func (o *ObservedLogs) add(log LoggedEntry) { |
| 141 | o.mu.Lock() |
| 142 | o.logs = append(o.logs, log) |
| 143 | o.mu.Unlock() |
| 144 | } |
| 145 | |
| 146 | // New creates a new Core that buffers logs in memory (without any encoding). |
| 147 | // It's particularly useful in tests. |
| 148 | func New(enab zapcore.LevelEnabler) (zapcore.Core, *ObservedLogs) { |
| 149 | ol := &ObservedLogs{} |
| 150 | return &contextObserver{ |
| 151 | LevelEnabler: enab, |
| 152 | logs: ol, |
| 153 | }, ol |
| 154 | } |
| 155 | |
| 156 | type contextObserver struct { |
| 157 | zapcore.LevelEnabler |
| 158 | logs *ObservedLogs |
| 159 | context []zapcore.Field |
| 160 | } |
| 161 | |
| 162 | var ( |
| 163 | _ zapcore.Core = (*contextObserver)(nil) |
| 164 | _ internal.LeveledEnabler = (*contextObserver)(nil) |
| 165 | ) |
| 166 | |
| 167 | func (co *contextObserver) Level() zapcore.Level { |
| 168 | return zapcore.LevelOf(co.LevelEnabler) |
| 169 | } |
| 170 | |
| 171 | func (co *contextObserver) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { |
| 172 | if co.Enabled(ent.Level) { |
| 173 | return ce.AddCore(ent, co) |
| 174 | } |
| 175 | return ce |
| 176 | } |
| 177 | |
| 178 | func (co *contextObserver) With(fields []zapcore.Field) zapcore.Core { |
| 179 | return &contextObserver{ |
| 180 | LevelEnabler: co.LevelEnabler, |
| 181 | logs: co.logs, |
| 182 | context: append(co.context[:len(co.context):len(co.context)], fields...), |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | func (co *contextObserver) Write(ent zapcore.Entry, fields []zapcore.Field) error { |
| 187 | all := make([]zapcore.Field, 0, len(fields)+len(co.context)) |
| 188 | all = append(all, co.context...) |
| 189 | all = append(all, fields...) |
| 190 | co.logs.add(LoggedEntry{ent, all}) |
| 191 | return nil |
| 192 | } |
| 193 | |
| 194 | func (co *contextObserver) Sync() error { |
| 195 | return nil |
| 196 | } |