github.com/newrelic/go-agent@v3.26.0+incompatible/internal/adaptive_sampler.go (about) 1 // Copyright 2020 New Relic Corporation. All rights reserved. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package internal 5 6 import ( 7 "math" 8 "sync" 9 "time" 10 ) 11 12 // AdaptiveSampler calculates which transactions should be sampled. An interface 13 // is used in the connect reply to facilitate testing. 14 type AdaptiveSampler interface { 15 ComputeSampled(priority float32, now time.Time) bool 16 } 17 18 // SampleEverything is used for testing. 19 type SampleEverything struct{} 20 21 // SampleNothing is used when the application is not yet connected. 22 type SampleNothing struct{} 23 24 // ComputeSampled implements AdaptiveSampler. 25 func (s SampleEverything) ComputeSampled(priority float32, now time.Time) bool { return true } 26 27 // ComputeSampled implements AdaptiveSampler. 28 func (s SampleNothing) ComputeSampled(priority float32, now time.Time) bool { return false } 29 30 type adaptiveSampler struct { 31 sync.Mutex 32 period time.Duration 33 target uint64 34 35 // Transactions with priority higher than this are sampled. 36 // This is 1 - sampleRatio. 37 priorityMin float32 38 39 currentPeriod struct { 40 numSampled uint64 41 numSeen uint64 42 end time.Time 43 } 44 } 45 46 // NewAdaptiveSampler creates an AdaptiveSampler. 47 func NewAdaptiveSampler(period time.Duration, target uint64, now time.Time) AdaptiveSampler { 48 as := &adaptiveSampler{} 49 as.period = period 50 as.target = target 51 as.currentPeriod.end = now.Add(period) 52 53 // Sample the first transactions in the first period. 54 as.priorityMin = 0.0 55 return as 56 } 57 58 // ComputeSampled calculates if the transaction should be sampled. 59 func (as *adaptiveSampler) ComputeSampled(priority float32, now time.Time) bool { 60 as.Lock() 61 defer as.Unlock() 62 63 // If the current time is after the end of the "currentPeriod". This is in 64 // a `for`/`while` loop in case there's a harvest where no sampling happened. 65 // i.e. for situations where a single call to 66 // as.currentPeriod.end = as.currentPeriod.end.Add(as.period) 67 // might not catch us up to the current period 68 for now.After(as.currentPeriod.end) { 69 as.priorityMin = 0.0 70 if as.currentPeriod.numSeen > 0 { 71 sampledRatio := float32(as.target) / float32(as.currentPeriod.numSeen) 72 as.priorityMin = 1.0 - sampledRatio 73 } 74 as.currentPeriod.numSampled = 0 75 as.currentPeriod.numSeen = 0 76 as.currentPeriod.end = as.currentPeriod.end.Add(as.period) 77 } 78 79 as.currentPeriod.numSeen++ 80 81 // exponential backoff -- if the number of sampled items is greater than our 82 // target, we need to apply the exponential backoff 83 if as.currentPeriod.numSampled > as.target { 84 if as.computeSampledBackoff(as.target, as.currentPeriod.numSeen, as.currentPeriod.numSampled) { 85 as.currentPeriod.numSampled++ 86 return true 87 } 88 return false 89 } 90 91 if priority >= as.priorityMin { 92 as.currentPeriod.numSampled++ 93 return true 94 } 95 96 return false 97 } 98 99 func (as *adaptiveSampler) computeSampledBackoff(target uint64, decidedCount uint64, sampledTrueCount uint64) bool { 100 return float64(RandUint64N(decidedCount)) < 101 math.Pow(float64(target), (float64(target)/float64(sampledTrueCount)))-math.Pow(float64(target), 0.5) 102 }