@@ -844,9 +844,7 @@ func (h *histogram) Write(out *dto.Metric) error {
844844 }}
845845 }
846846
847- // If exemplars are not configured, the cap will be 0.
848- // So append is not needed in this case.
849- if cap (h .nativeExemplars .exemplars ) > 0 {
847+ if h .nativeExemplars .isEnabled () {
850848 h .nativeExemplars .Lock ()
851849 his .Exemplars = append (his .Exemplars , h .nativeExemplars .exemplars ... )
852850 h .nativeExemplars .Unlock ()
@@ -1658,10 +1656,17 @@ func addAndResetCounts(hot, cold *histogramCounts) {
16581656type nativeExemplars struct {
16591657 sync.Mutex
16601658
1661- ttl time.Duration
1659+ // Time-to-live for exemplars, it is set to -1 if exemplars are disabled, that is NativeHistogramMaxExemplars is below 0.
1660+ // The ttl is used on insertion to remove an exemplar that is older than ttl, if present.
1661+ ttl time.Duration
1662+
16621663 exemplars []* dto.Exemplar
16631664}
16641665
1666+ func (n * nativeExemplars ) isEnabled () bool {
1667+ return n .ttl != - 1
1668+ }
1669+
16651670func makeNativeExemplars (ttl time.Duration , maxCount int ) nativeExemplars {
16661671 if ttl == 0 {
16671672 ttl = 5 * time .Minute
@@ -1673,6 +1678,7 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
16731678
16741679 if maxCount < 0 {
16751680 maxCount = 0
1681+ ttl = - 1
16761682 }
16771683
16781684 return nativeExemplars {
@@ -1682,20 +1688,18 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
16821688}
16831689
16841690func (n * nativeExemplars ) addExemplar (e * dto.Exemplar ) {
1685- if cap ( n . exemplars ) == 0 {
1691+ if ! n . isEnabled () {
16861692 return
16871693 }
16881694
16891695 n .Lock ()
16901696 defer n .Unlock ()
16911697
1692- // The index where to insert the new exemplar.
1693- var nIdx int = - 1
1694-
16951698 // When the number of exemplars has not yet exceeded or
16961699 // is equal to cap(n.exemplars), then
16971700 // insert the new exemplar directly.
16981701 if len (n .exemplars ) < cap (n .exemplars ) {
1702+ var nIdx int
16991703 for nIdx = 0 ; nIdx < len (n .exemplars ); nIdx ++ {
17001704 if * e .Value < * n .exemplars [nIdx ].Value {
17011705 break
@@ -1705,17 +1709,46 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17051709 return
17061710 }
17071711
1712+ if len (n .exemplars ) == 1 {
1713+ // When the number of exemplars is 1, then
1714+ // replace the existing exemplar with the new exemplar.
1715+ n .exemplars [0 ] = e
1716+ return
1717+ }
1718+ // From this point on, the number of exemplars is greater than 1.
1719+
17081720 // When the number of exemplars exceeds the limit, remove one exemplar.
17091721 var (
1710- rIdx int // The index where to remove the old exemplar.
1711-
1712- ot = time .Now () // Oldest timestamp seen.
1713- otIdx = - 1 // Index of the exemplar with the oldest timestamp.
1714-
1715- md = - 1.0 // Logarithm of the delta of the closest pair of exemplars.
1716- mdIdx = - 1 // Index of the older exemplar within the closest pair.
1717- cLog float64 // Logarithm of the current exemplar.
1718- pLog float64 // Logarithm of the previous exemplar.
1722+ ot = time.Time {} // Oldest timestamp seen. Initial value doesn't matter as we replace it due to otIdx == -1 in the loop.
1723+ otIdx = - 1 // Index of the exemplar with the oldest timestamp.
1724+
1725+ md = - 1.0 // Logarithm of the delta of the closest pair of exemplars.
1726+
1727+ // The insertion point of the new exemplar in the exemplars slice after insertion.
1728+ // This is calculated purely based on the order of the exemplars by value.
1729+ // nIdx == len(n.exemplars) means the new exemplar is to be inserted after the end.
1730+ nIdx = - 1
1731+
1732+ // rIdx is ultimately the index for the exemplar that we are replacing with the new exemplar.
1733+ // The aim is to keep a good spread of exemplars by value and not let them bunch up too much.
1734+ // It is calculated in 3 steps:
1735+ // 1. First we set rIdx to the index of the older exemplar within the closest pair by value.
1736+ // That is the following will be true (on log scale):
1737+ // either the exemplar pair on index (rIdx-1, rIdx) or (rIdx, rIdx+1) will have
1738+ // the closest values to each other from all pairs.
1739+ // For example, suppose the values are distributed like this:
1740+ // |-----------x-------------x----------------x----x-----|
1741+ // ^--rIdx as this is older.
1742+ // Or like this:
1743+ // |-----------x-------------x----------------x----x-----|
1744+ // ^--rIdx as this is older.
1745+ // 2. If there is an exemplar that expired, then we simple reset rIdx to that index.
1746+ // 3. We check if by inserting the new exemplar we would create a closer pair at
1747+ // (nIdx-1, nIdx) or (nIdx, nIdx+1) and set rIdx to nIdx-1 or nIdx accordingly to
1748+ // keep the spread of exemplars by value; otherwise we keep rIdx as it is.
1749+ rIdx = - 1
1750+ cLog float64 // Logarithm of the current exemplar.
1751+ pLog float64 // Logarithm of the previous exemplar.
17191752 )
17201753
17211754 for i , exemplar := range n .exemplars {
@@ -1726,7 +1759,7 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17261759 }
17271760
17281761 // Find the index at which to insert new the exemplar.
1729- if * e .Value <= * exemplar .Value && nIdx == - 1 {
1762+ if nIdx == - 1 && * e .Value <= * exemplar .Value {
17301763 nIdx = i
17311764 }
17321765
@@ -1738,11 +1771,13 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17381771 }
17391772 diff := math .Abs (cLog - pLog )
17401773 if md == - 1 || diff < md {
1774+ // The closest exemplar pair is at index: i-1, i.
1775+ // Choose the exemplar with the older timestamp for replacement.
17411776 md = diff
17421777 if n .exemplars [i ].Timestamp .AsTime ().Before (n .exemplars [i - 1 ].Timestamp .AsTime ()) {
1743- mdIdx = i
1778+ rIdx = i
17441779 } else {
1745- mdIdx = i - 1
1780+ rIdx = i - 1
17461781 }
17471782 }
17481783
@@ -1753,8 +1788,12 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17531788 if nIdx == - 1 {
17541789 nIdx = len (n .exemplars )
17551790 }
1791+ // Here, we have the following relationships:
1792+ // n.exemplars[nIdx-1].Value < e.Value (if nIdx > 0)
1793+ // e.Value <= n.exemplars[nIdx].Value (if nIdx < len(n.exemplars))
17561794
17571795 if otIdx != - 1 && e .Timestamp .AsTime ().Sub (ot ) > n .ttl {
1796+ // If the oldest exemplar has expired, then replace it with the new exemplar.
17581797 rIdx = otIdx
17591798 } else {
17601799 // In the previous for loop, when calculating the closest pair of exemplars,
@@ -1764,23 +1803,26 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
17641803 if nIdx > 0 {
17651804 diff := math .Abs (elog - math .Log (n .exemplars [nIdx - 1 ].GetValue ()))
17661805 if diff < md {
1806+ // The value we are about to insert is closer to the previous exemplar at the insertion point than what we calculated before in rIdx.
1807+ // v--rIdx
1808+ // |-----------x-n-----------x----------------x----x-----|
1809+ // nIdx-1--^ ^--new exemplar value
1810+ // Do not make the spread worse, replace nIdx-1 and not rIdx.
17671811 md = diff
1768- mdIdx = nIdx
1769- if n .exemplars [nIdx - 1 ].Timestamp .AsTime ().Before (e .Timestamp .AsTime ()) {
1770- mdIdx = nIdx - 1
1771- }
1812+ rIdx = nIdx - 1
17721813 }
17731814 }
17741815 if nIdx < len (n .exemplars ) {
17751816 diff := math .Abs (math .Log (n .exemplars [nIdx ].GetValue ()) - elog )
17761817 if diff < md {
1777- mdIdx = nIdx
1778- if n .exemplars [nIdx ].Timestamp .AsTime ().Before (e .Timestamp .AsTime ()) {
1779- mdIdx = nIdx
1780- }
1818+ // The value we are about to insert is closer to the next exemplar at the insertion point than what we calculated before in rIdx.
1819+ // v--rIdx
1820+ // |-----------x-----------n-x----------------x----x-----|
1821+ // new exemplar value--^ ^--nIdx
1822+ // Do not make the spread worse, replace nIdx-1 and not rIdx.
1823+ rIdx = nIdx
17811824 }
17821825 }
1783- rIdx = mdIdx
17841826 }
17851827
17861828 // Adjust the slice according to rIdx and nIdx.
0 commit comments