// Copyright (c) 2016 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package zapcore import ( "time" "go.uber.org/atomic" ) const ( _numLevels = _maxLevel - _minLevel + 1 _countersPerLevel = 4096 ) type counter struct { resetAt atomic.Int64 counter atomic.Uint64 } type counters [_numLevels][_countersPerLevel]counter func newCounters() *counters { return &counters{} } func (cs *counters) get(lvl Level, key string) *counter { i := lvl - _minLevel j := fnv32a(key) % _countersPerLevel return &cs[i][j] } // fnv32a, adapted from "hash/fnv", but without a []byte(string) alloc func fnv32a(s string) uint32 { const ( offset32 = 2166136261 prime32 = 16777619 ) hash := uint32(offset32) for i := 0; i < len(s); i++ { hash ^= uint32(s[i]) hash *= prime32 } return hash } func (c *counter) IncCheckReset(t time.Time, tick time.Duration) uint64 { tn := t.UnixNano() resetAfter := c.resetAt.Load() if resetAfter > tn { return c.counter.Inc() } c.counter.Store(1) newResetAfter := tn + tick.Nanoseconds() if !c.resetAt.CAS(resetAfter, newResetAfter) { // We raced with another goroutine trying to reset, and it also reset // the counter to 1, so we need to reincrement the counter. return c.counter.Inc() } return 1 } // SamplingDecision is a decision represented as a bit field made by sampler. // More decisions may be added in the future. type SamplingDecision uint32 const ( // LogDropped indicates that the Sampler dropped a log entry. LogDropped SamplingDecision = 1 << iota // LogSampled indicates that the Sampler sampled a log entry. LogSampled ) // optionFunc wraps a func so it satisfies the SamplerOption interface. type optionFunc func(*sampler) func (f optionFunc) apply(s *sampler) { f(s) } // SamplerOption configures a Sampler. type SamplerOption interface { apply(*sampler) } // nopSamplingHook is the default hook used by sampler. func nopSamplingHook(Entry, SamplingDecision) {} // SamplerHook registers a function which will be called when Sampler makes a // decision. // // This hook may be used to get visibility into the performance of the sampler. // For example, use it to track metrics of dropped versus sampled logs. // // var dropped atomic.Int64 // zapcore.SamplerHook(func(ent zapcore.Entry, dec zapcore.SamplingDecision) { // if dec&zapcore.LogDropped > 0 { // dropped.Inc() // } // }) func SamplerHook(hook func(entry Entry, dec SamplingDecision)) SamplerOption { return optionFunc(func(s *sampler) { s.hook = hook }) } // NewSamplerWithOptions creates a Core that samples incoming entries, which // caps the CPU and I/O load of logging while attempting to preserve a // representative subset of your logs. // // Zap samples by logging the first N entries with a given level and message // each tick. If more Entries with the same level and message are seen during // the same interval, every Mth message is logged and the rest are dropped. // // Sampler can be configured to report sampling decisions with the SamplerHook // option. // // Keep in mind that zap's sampling implementation is optimized for speed over // absolute precision; under load, each tick may be slightly over- or // under-sampled. func NewSamplerWithOptions(core Core, tick time.Duration, first, thereafter int, opts ...SamplerOption) Core { s := &sampler{ Core: core, tick: tick, counts: newCounters(), first: uint64(first), thereafter: uint64(thereafter), hook: nopSamplingHook, } for _, opt := range opts { opt.apply(s) } return s } type sampler struct { Core counts *counters tick time.Duration first, thereafter uint64 hook func(Entry, SamplingDecision) } // NewSampler creates a Core that samples incoming entries, which // caps the CPU and I/O load of logging while attempting to preserve a // representative subset of your logs. // // Zap samples by logging the first N entries with a given level and message // each tick. If more Entries with the same level and message are seen during // the same interval, every Mth message is logged and the rest are dropped. // // Keep in mind that zap's sampling implementation is optimized for speed over // absolute precision; under load, each tick may be slightly over- or // under-sampled. // // Deprecated: use NewSamplerWithOptions. func NewSampler(core Core, tick time.Duration, first, thereafter int) Core { return NewSamplerWithOptions(core, tick, first, thereafter) } func (s *sampler) With(fields []Field) Core { return &sampler{ Core: s.Core.With(fields), tick: s.tick, counts: s.counts, first: s.first, thereafter: s.thereafter, hook: s.hook, } } func (s *sampler) Check(ent Entry, ce *CheckedEntry) *CheckedEntry { if !s.Enabled(ent.Level) { return ce } if ent.Level >= _minLevel && ent.Level <= _maxLevel { counter := s.counts.get(ent.Level, ent.Message) n := counter.IncCheckReset(ent.Time, s.tick) if n > s.first && (n-s.first)%s.thereafter != 0 { s.hook(ent, LogDropped) return ce } s.hook(ent, LogSampled) } return s.Core.Check(ent, ce) }