Elia Battiston | c8d0d46 | 2022-02-22 16:30:51 +0100 | [diff] [blame^] | 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 zapcore |
| 22 | |
| 23 | import ( |
| 24 | "time" |
| 25 | |
| 26 | "go.uber.org/atomic" |
| 27 | ) |
| 28 | |
| 29 | const ( |
| 30 | _numLevels = _maxLevel - _minLevel + 1 |
| 31 | _countersPerLevel = 4096 |
| 32 | ) |
| 33 | |
| 34 | type counter struct { |
| 35 | resetAt atomic.Int64 |
| 36 | counter atomic.Uint64 |
| 37 | } |
| 38 | |
| 39 | type counters [_numLevels][_countersPerLevel]counter |
| 40 | |
| 41 | func newCounters() *counters { |
| 42 | return &counters{} |
| 43 | } |
| 44 | |
| 45 | func (cs *counters) get(lvl Level, key string) *counter { |
| 46 | i := lvl - _minLevel |
| 47 | j := fnv32a(key) % _countersPerLevel |
| 48 | return &cs[i][j] |
| 49 | } |
| 50 | |
| 51 | // fnv32a, adapted from "hash/fnv", but without a []byte(string) alloc |
| 52 | func fnv32a(s string) uint32 { |
| 53 | const ( |
| 54 | offset32 = 2166136261 |
| 55 | prime32 = 16777619 |
| 56 | ) |
| 57 | hash := uint32(offset32) |
| 58 | for i := 0; i < len(s); i++ { |
| 59 | hash ^= uint32(s[i]) |
| 60 | hash *= prime32 |
| 61 | } |
| 62 | return hash |
| 63 | } |
| 64 | |
| 65 | func (c *counter) IncCheckReset(t time.Time, tick time.Duration) uint64 { |
| 66 | tn := t.UnixNano() |
| 67 | resetAfter := c.resetAt.Load() |
| 68 | if resetAfter > tn { |
| 69 | return c.counter.Inc() |
| 70 | } |
| 71 | |
| 72 | c.counter.Store(1) |
| 73 | |
| 74 | newResetAfter := tn + tick.Nanoseconds() |
| 75 | if !c.resetAt.CAS(resetAfter, newResetAfter) { |
| 76 | // We raced with another goroutine trying to reset, and it also reset |
| 77 | // the counter to 1, so we need to reincrement the counter. |
| 78 | return c.counter.Inc() |
| 79 | } |
| 80 | |
| 81 | return 1 |
| 82 | } |
| 83 | |
| 84 | // SamplingDecision is a decision represented as a bit field made by sampler. |
| 85 | // More decisions may be added in the future. |
| 86 | type SamplingDecision uint32 |
| 87 | |
| 88 | const ( |
| 89 | // LogDropped indicates that the Sampler dropped a log entry. |
| 90 | LogDropped SamplingDecision = 1 << iota |
| 91 | // LogSampled indicates that the Sampler sampled a log entry. |
| 92 | LogSampled |
| 93 | ) |
| 94 | |
| 95 | // optionFunc wraps a func so it satisfies the SamplerOption interface. |
| 96 | type optionFunc func(*sampler) |
| 97 | |
| 98 | func (f optionFunc) apply(s *sampler) { |
| 99 | f(s) |
| 100 | } |
| 101 | |
| 102 | // SamplerOption configures a Sampler. |
| 103 | type SamplerOption interface { |
| 104 | apply(*sampler) |
| 105 | } |
| 106 | |
| 107 | // nopSamplingHook is the default hook used by sampler. |
| 108 | func nopSamplingHook(Entry, SamplingDecision) {} |
| 109 | |
| 110 | // SamplerHook registers a function which will be called when Sampler makes a |
| 111 | // decision. |
| 112 | // |
| 113 | // This hook may be used to get visibility into the performance of the sampler. |
| 114 | // For example, use it to track metrics of dropped versus sampled logs. |
| 115 | // |
| 116 | // var dropped atomic.Int64 |
| 117 | // zapcore.SamplerHook(func(ent zapcore.Entry, dec zapcore.SamplingDecision) { |
| 118 | // if dec&zapcore.LogDropped > 0 { |
| 119 | // dropped.Inc() |
| 120 | // } |
| 121 | // }) |
| 122 | func SamplerHook(hook func(entry Entry, dec SamplingDecision)) SamplerOption { |
| 123 | return optionFunc(func(s *sampler) { |
| 124 | s.hook = hook |
| 125 | }) |
| 126 | } |
| 127 | |
| 128 | // NewSamplerWithOptions creates a Core that samples incoming entries, which |
| 129 | // caps the CPU and I/O load of logging while attempting to preserve a |
| 130 | // representative subset of your logs. |
| 131 | // |
| 132 | // Zap samples by logging the first N entries with a given level and message |
| 133 | // each tick. If more Entries with the same level and message are seen during |
| 134 | // the same interval, every Mth message is logged and the rest are dropped. |
| 135 | // |
| 136 | // Sampler can be configured to report sampling decisions with the SamplerHook |
| 137 | // option. |
| 138 | // |
| 139 | // Keep in mind that zap's sampling implementation is optimized for speed over |
| 140 | // absolute precision; under load, each tick may be slightly over- or |
| 141 | // under-sampled. |
| 142 | func NewSamplerWithOptions(core Core, tick time.Duration, first, thereafter int, opts ...SamplerOption) Core { |
| 143 | s := &sampler{ |
| 144 | Core: core, |
| 145 | tick: tick, |
| 146 | counts: newCounters(), |
| 147 | first: uint64(first), |
| 148 | thereafter: uint64(thereafter), |
| 149 | hook: nopSamplingHook, |
| 150 | } |
| 151 | for _, opt := range opts { |
| 152 | opt.apply(s) |
| 153 | } |
| 154 | |
| 155 | return s |
| 156 | } |
| 157 | |
| 158 | type sampler struct { |
| 159 | Core |
| 160 | |
| 161 | counts *counters |
| 162 | tick time.Duration |
| 163 | first, thereafter uint64 |
| 164 | hook func(Entry, SamplingDecision) |
| 165 | } |
| 166 | |
| 167 | // NewSampler creates a Core that samples incoming entries, which |
| 168 | // caps the CPU and I/O load of logging while attempting to preserve a |
| 169 | // representative subset of your logs. |
| 170 | // |
| 171 | // Zap samples by logging the first N entries with a given level and message |
| 172 | // each tick. If more Entries with the same level and message are seen during |
| 173 | // the same interval, every Mth message is logged and the rest are dropped. |
| 174 | // |
| 175 | // Keep in mind that zap's sampling implementation is optimized for speed over |
| 176 | // absolute precision; under load, each tick may be slightly over- or |
| 177 | // under-sampled. |
| 178 | // |
| 179 | // Deprecated: use NewSamplerWithOptions. |
| 180 | func NewSampler(core Core, tick time.Duration, first, thereafter int) Core { |
| 181 | return NewSamplerWithOptions(core, tick, first, thereafter) |
| 182 | } |
| 183 | |
| 184 | func (s *sampler) With(fields []Field) Core { |
| 185 | return &sampler{ |
| 186 | Core: s.Core.With(fields), |
| 187 | tick: s.tick, |
| 188 | counts: s.counts, |
| 189 | first: s.first, |
| 190 | thereafter: s.thereafter, |
| 191 | hook: s.hook, |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | func (s *sampler) Check(ent Entry, ce *CheckedEntry) *CheckedEntry { |
| 196 | if !s.Enabled(ent.Level) { |
| 197 | return ce |
| 198 | } |
| 199 | |
| 200 | counter := s.counts.get(ent.Level, ent.Message) |
| 201 | n := counter.IncCheckReset(ent.Time, s.tick) |
| 202 | if n > s.first && (n-s.first)%s.thereafter != 0 { |
| 203 | s.hook(ent, LogDropped) |
| 204 | return ce |
| 205 | } |
| 206 | s.hook(ent, LogSampled) |
| 207 | return s.Core.Check(ent, ce) |
| 208 | } |