@@ -844,9 +844,7 @@ func (h *histogram) Write(out *dto.Metric) error {
844
844
}}
845
845
}
846
846
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 () {
850
848
h .nativeExemplars .Lock ()
851
849
his .Exemplars = append (his .Exemplars , h .nativeExemplars .exemplars ... )
852
850
h .nativeExemplars .Unlock ()
@@ -1658,10 +1656,17 @@ func addAndResetCounts(hot, cold *histogramCounts) {
1658
1656
type nativeExemplars struct {
1659
1657
sync.Mutex
1660
1658
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
+
1662
1663
exemplars []* dto.Exemplar
1663
1664
}
1664
1665
1666
+ func (n * nativeExemplars ) isEnabled () bool {
1667
+ return n .ttl != - 1
1668
+ }
1669
+
1665
1670
func makeNativeExemplars (ttl time.Duration , maxCount int ) nativeExemplars {
1666
1671
if ttl == 0 {
1667
1672
ttl = 5 * time .Minute
@@ -1673,6 +1678,7 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
1673
1678
1674
1679
if maxCount < 0 {
1675
1680
maxCount = 0
1681
+ ttl = - 1
1676
1682
}
1677
1683
1678
1684
return nativeExemplars {
@@ -1682,20 +1688,18 @@ func makeNativeExemplars(ttl time.Duration, maxCount int) nativeExemplars {
1682
1688
}
1683
1689
1684
1690
func (n * nativeExemplars ) addExemplar (e * dto.Exemplar ) {
1685
- if cap ( n . exemplars ) == 0 {
1691
+ if ! n . isEnabled () {
1686
1692
return
1687
1693
}
1688
1694
1689
1695
n .Lock ()
1690
1696
defer n .Unlock ()
1691
1697
1692
- // The index where to insert the new exemplar.
1693
- var nIdx int = - 1
1694
-
1695
1698
// When the number of exemplars has not yet exceeded or
1696
1699
// is equal to cap(n.exemplars), then
1697
1700
// insert the new exemplar directly.
1698
1701
if len (n .exemplars ) < cap (n .exemplars ) {
1702
+ var nIdx int
1699
1703
for nIdx = 0 ; nIdx < len (n .exemplars ); nIdx ++ {
1700
1704
if * e .Value < * n .exemplars [nIdx ].Value {
1701
1705
break
@@ -1705,17 +1709,46 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
1705
1709
return
1706
1710
}
1707
1711
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
+
1708
1720
// When the number of exemplars exceeds the limit, remove one exemplar.
1709
1721
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.
1719
1752
)
1720
1753
1721
1754
for i , exemplar := range n .exemplars {
@@ -1726,7 +1759,7 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
1726
1759
}
1727
1760
1728
1761
// 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 {
1730
1763
nIdx = i
1731
1764
}
1732
1765
@@ -1738,11 +1771,13 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
1738
1771
}
1739
1772
diff := math .Abs (cLog - pLog )
1740
1773
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.
1741
1776
md = diff
1742
1777
if n .exemplars [i ].Timestamp .AsTime ().Before (n .exemplars [i - 1 ].Timestamp .AsTime ()) {
1743
- mdIdx = i
1778
+ rIdx = i
1744
1779
} else {
1745
- mdIdx = i - 1
1780
+ rIdx = i - 1
1746
1781
}
1747
1782
}
1748
1783
@@ -1753,8 +1788,12 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
1753
1788
if nIdx == - 1 {
1754
1789
nIdx = len (n .exemplars )
1755
1790
}
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))
1756
1794
1757
1795
if otIdx != - 1 && e .Timestamp .AsTime ().Sub (ot ) > n .ttl {
1796
+ // If the oldest exemplar has expired, then replace it with the new exemplar.
1758
1797
rIdx = otIdx
1759
1798
} else {
1760
1799
// In the previous for loop, when calculating the closest pair of exemplars,
@@ -1764,23 +1803,26 @@ func (n *nativeExemplars) addExemplar(e *dto.Exemplar) {
1764
1803
if nIdx > 0 {
1765
1804
diff := math .Abs (elog - math .Log (n .exemplars [nIdx - 1 ].GetValue ()))
1766
1805
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.
1767
1811
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
1772
1813
}
1773
1814
}
1774
1815
if nIdx < len (n .exemplars ) {
1775
1816
diff := math .Abs (math .Log (n .exemplars [nIdx ].GetValue ()) - elog )
1776
1817
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
1781
1824
}
1782
1825
}
1783
- rIdx = mdIdx
1784
1826
}
1785
1827
1786
1828
// Adjust the slice according to rIdx and nIdx.
0 commit comments