From 86e08ec1da31c031848860bf3599d96ecb0b2fc9 Mon Sep 17 00:00:00 2001 From: amarinder Date: Mon, 18 Jul 2022 14:07:46 -0700 Subject: [PATCH 01/13] initial commit of adding median feature to statsd input plugin --- plugins/inputs/statsd/README.md | 2 ++ plugins/inputs/statsd/running_stats.go | 26 ++++++++++++++++++++++++++ plugins/inputs/statsd/statsd.go | 1 + 3 files changed, 29 insertions(+) diff --git a/plugins/inputs/statsd/README.md b/plugins/inputs/statsd/README.md index 2e3c37b4006f3..1048ce7b1d6b6 100644 --- a/plugins/inputs/statsd/README.md +++ b/plugins/inputs/statsd/README.md @@ -183,6 +183,8 @@ metric type: for that stat during that interval. - `statsd__mean`: The mean is the average of all values statsd saw for that stat during that interval. + - `statsd__median`: The median is the average of all values statsd saw + for that stat during that interval. - `statsd__stddev`: The stddev is the sample standard deviation of all values statsd saw for that stat during that interval. - `statsd__sum`: The sum is the sample sum of all values statsd saw diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index e33749b2c2179..3cf8a7a16ad3a 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -7,6 +7,7 @@ import ( ) const defaultPercentileLimit = 1000 +const defaultMedianLimit = 1000 // RunningStats calculates a running mean, variance, standard deviation, // lower bound, upper bound, count, and can calculate estimated percentiles. @@ -22,7 +23,10 @@ type RunningStats struct { // We will store a maximum of PercLimit values, at which point we will start // randomly replacing old values, hence it is an estimated percentile. perc []float64 + med []float64 + MedLen int PercLimit int + MedLimit int sum float64 @@ -45,7 +49,11 @@ func (rs *RunningStats) AddValue(v float64) { if rs.PercLimit == 0 { rs.PercLimit = defaultPercentileLimit } + if rs.MedLimit == 0 { + rs.MedLimit = defaultMedianLimit + } rs.perc = make([]float64, 0, rs.PercLimit) + rs.med = make([]float64, 0, rs.MedLimit) } // These are used for the running mean and variance @@ -69,12 +77,30 @@ func (rs *RunningStats) AddValue(v float64) { // Reached limit, choose random index to overwrite in the percentile array rs.perc[rand.Intn(len(rs.perc))] = v } + + rs.MedLen = len(rs.med) + // Need to sort for median + sort.Float64s(rs.med) + if rs.MedLen < rs.MedLimit { + rs.med = append(rs.med, v) + } else { + // Reached limit, choose random index to overwrite in the median array + rs.med[rand.Intn(rs.MedLen)] = v + } } func (rs *RunningStats) Mean() float64 { return rs.k + rs.ex/float64(rs.n) } +func (rs *RunningStats) Median() float64 { + if rs.MedLen%2 == 0 { + return float64((rs.med[rs.MedLen/2-1] + rs.med[rs.MedLen/2]) / 2) + } else { + return float64(rs.med[rs.MedLen/2]) + } +} + func (rs *RunningStats) Variance() float64 { return (rs.ex2 - (rs.ex*rs.ex)/float64(rs.n)) / float64(rs.n) } diff --git a/plugins/inputs/statsd/statsd.go b/plugins/inputs/statsd/statsd.go index 97dca4656062c..da3a0b0159a32 100644 --- a/plugins/inputs/statsd/statsd.go +++ b/plugins/inputs/statsd/statsd.go @@ -253,6 +253,7 @@ func (s *Statsd) Gather(acc telegraf.Accumulator) error { prefix = fieldName + "_" } fields[prefix+"mean"] = stats.Mean() + fields[prefix+"median"] = stats.Median() fields[prefix+"stddev"] = stats.Stddev() fields[prefix+"sum"] = stats.Sum() fields[prefix+"upper"] = stats.Upper() From 19c29cb017c71aff6d6b1b3aa707370d495fd50c Mon Sep 17 00:00:00 2001 From: amarinder Date: Mon, 18 Jul 2022 15:02:20 -0700 Subject: [PATCH 02/13] adding median timing tests and have checks for zero len array for statsd input plugin --- plugins/inputs/statsd/running_stats.go | 4 +++- plugins/inputs/statsd/statsd_test.go | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 3cf8a7a16ad3a..3ce7f22685bbf 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -94,7 +94,9 @@ func (rs *RunningStats) Mean() float64 { } func (rs *RunningStats) Median() float64 { - if rs.MedLen%2 == 0 { + if rs.MedLen == 0 { + return 0 + } else if rs.MedLen%2 == 0 { return float64((rs.med[rs.MedLen/2-1] + rs.med[rs.MedLen/2]) / 2) } else { return float64(rs.med[rs.MedLen/2]) diff --git a/plugins/inputs/statsd/statsd_test.go b/plugins/inputs/statsd/statsd_test.go index 22d6ee4e30901..9adf3c9f58835 100644 --- a/plugins/inputs/statsd/statsd_test.go +++ b/plugins/inputs/statsd/statsd_test.go @@ -419,6 +419,7 @@ func TestParse_Timings(t *testing.T) { "count": int64(5), "lower": float64(1), "mean": float64(3), + "median": float64(1), "stddev": float64(4), "sum": float64(15), "upper": float64(11), @@ -913,6 +914,7 @@ func TestParse_DataDogTags(t *testing.T) { "count": 10, "lower": float64(3), "mean": float64(3), + "median": float64(3), "stddev": float64(0), "sum": float64(30), "upper": float64(3), @@ -1211,6 +1213,7 @@ func TestParse_TimingsMultipleFieldsWithTemplate(t *testing.T) { "success_count": int64(5), "success_lower": float64(1), "success_mean": float64(3), + "success_median": float64(1), "success_stddev": float64(4), "success_sum": float64(15), "success_upper": float64(11), @@ -1219,6 +1222,7 @@ func TestParse_TimingsMultipleFieldsWithTemplate(t *testing.T) { "error_count": int64(5), "error_lower": float64(2), "error_mean": float64(6), + "error_median": float64(2), "error_stddev": float64(8), "error_sum": float64(30), "error_upper": float64(22), @@ -1259,6 +1263,7 @@ func TestParse_TimingsMultipleFieldsWithoutTemplate(t *testing.T) { "count": int64(5), "lower": float64(1), "mean": float64(3), + "median": float64(1), "stddev": float64(4), "sum": float64(15), "upper": float64(11), @@ -1268,6 +1273,7 @@ func TestParse_TimingsMultipleFieldsWithoutTemplate(t *testing.T) { "count": int64(5), "lower": float64(2), "mean": float64(6), + "median": float64(2), "stddev": float64(8), "sum": float64(30), "upper": float64(22), From 828e9a3adb5b9eb12320e76a0950721550c71f43 Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Mon, 18 Jul 2022 15:24:01 -0700 Subject: [PATCH 03/13] fix readme to include proper description of median --- plugins/inputs/statsd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/inputs/statsd/README.md b/plugins/inputs/statsd/README.md index 1048ce7b1d6b6..4ea4bcc1676c6 100644 --- a/plugins/inputs/statsd/README.md +++ b/plugins/inputs/statsd/README.md @@ -183,7 +183,7 @@ metric type: for that stat during that interval. - `statsd__mean`: The mean is the average of all values statsd saw for that stat during that interval. - - `statsd__median`: The median is the average of all values statsd saw + - `statsd__median`: The median is the middle of all values statsd saw for that stat during that interval. - `statsd__stddev`: The stddev is the sample standard deviation of all values statsd saw for that stat during that interval. From 64f9cba6719ddddce310c00908ffde790f21adcf Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Mon, 18 Jul 2022 17:36:14 -0700 Subject: [PATCH 04/13] remove unnessesory convertion to float64 --- plugins/inputs/statsd/running_stats.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 3ce7f22685bbf..c3eafc6075e74 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -97,9 +97,9 @@ func (rs *RunningStats) Median() float64 { if rs.MedLen == 0 { return 0 } else if rs.MedLen%2 == 0 { - return float64((rs.med[rs.MedLen/2-1] + rs.med[rs.MedLen/2]) / 2) + return (rs.med[rs.MedLen/2-1] + rs.med[rs.MedLen/2]) / 2 } else { - return float64(rs.med[rs.MedLen/2]) + return rs.med[rs.MedLen/2] } } From 4f3ff978d4f9e8e516ce5a167c1253840298f80f Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Tue, 19 Jul 2022 07:55:49 -0700 Subject: [PATCH 05/13] make changes as suggested by srebhan --- plugins/inputs/statsd/running_stats.go | 34 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index c3eafc6075e74..7eefe5dbbc47d 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -23,16 +23,19 @@ type RunningStats struct { // We will store a maximum of PercLimit values, at which point we will start // randomly replacing old values, hence it is an estimated percentile. perc []float64 - med []float64 - MedLen int PercLimit int - MedLimit int sum float64 lower float64 upper float64 + // Array used to calculate estimated median values + // We will store a maximum of MedLimit values, at which point we will start + // slicing old values + med []float64 + MedLimit int + // cache if we have sorted the list so that we never re-sort a sorted list, // which can have very bad performance. sorted bool @@ -78,14 +81,12 @@ func (rs *RunningStats) AddValue(v float64) { rs.perc[rand.Intn(len(rs.perc))] = v } - rs.MedLen = len(rs.med) - // Need to sort for median - sort.Float64s(rs.med) - if rs.MedLen < rs.MedLimit { + if len(rs.med) < rs.MedLimit { rs.med = append(rs.med, v) } else { - // Reached limit, choose random index to overwrite in the median array - rs.med[rand.Intn(rs.MedLen)] = v + // Reached limit, slice off first element + rs.med = rs.med[1:] + rs.med[len(rs.med)-1] = v } } @@ -94,12 +95,19 @@ func (rs *RunningStats) Mean() float64 { } func (rs *RunningStats) Median() float64 { - if rs.MedLen == 0 { + count := len(rs.med) + // Need to sort for median + if !rs.sorted { + sort.Float64s(rs.med) + rs.sorted = true + } + + if count == 0 { return 0 - } else if rs.MedLen%2 == 0 { - return (rs.med[rs.MedLen/2-1] + rs.med[rs.MedLen/2]) / 2 + } else if count%2 == 0 { + return (rs.med[count/2-1] + rs.med[count/2]) / 2 } else { - return rs.med[rs.MedLen/2] + return rs.med[count/2] } } From da214b3fb9426805d3a13c832dfa138f66a6aa8c Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Tue, 19 Jul 2022 08:01:18 -0700 Subject: [PATCH 06/13] don't replace last element, add as last element in array --- plugins/inputs/statsd/running_stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 7eefe5dbbc47d..287c4b2a385b1 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -86,7 +86,7 @@ func (rs *RunningStats) AddValue(v float64) { } else { // Reached limit, slice off first element rs.med = rs.med[1:] - rs.med[len(rs.med)-1] = v + rs.med[len(rs.med)] = v } } From eb1c2c562391fb6e11f9e13cb52729db1971fbee Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Tue, 19 Jul 2022 08:19:19 -0700 Subject: [PATCH 07/13] seperate sorted bool to SortedPerc and SortedMed to better suit our needs --- plugins/inputs/statsd/running_stats.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 287c4b2a385b1..29854d7c3b0b5 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -38,12 +38,14 @@ type RunningStats struct { // cache if we have sorted the list so that we never re-sort a sorted list, // which can have very bad performance. - sorted bool + SortedPerc bool + SortedMed bool } func (rs *RunningStats) AddValue(v float64) { // Whenever a value is added, the list is no longer sorted. - rs.sorted = false + rs.SortedPerc = false + rs.SortedMed = false if rs.n == 0 { rs.k = v @@ -97,11 +99,10 @@ func (rs *RunningStats) Mean() float64 { func (rs *RunningStats) Median() float64 { count := len(rs.med) // Need to sort for median - if !rs.sorted { - sort.Float64s(rs.med) - rs.sorted = true - } - + if !rs.SortedMed { + sort.Float64s(rs.med) + rs.SortedMed = true + } if count == 0 { return 0 } else if count%2 == 0 { @@ -140,9 +141,9 @@ func (rs *RunningStats) Percentile(n float64) float64 { n = 100 } - if !rs.sorted { + if !rs.SortedPerc { sort.Float64s(rs.perc) - rs.sorted = true + rs.SortedPerc = true } i := float64(len(rs.perc)) * n / float64(100) From 0cec8b6b87c2ee0ff8a5ec7681df6adce241c827 Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Wed, 20 Jul 2022 05:40:36 -0700 Subject: [PATCH 08/13] sort for median, but keep temporal order --- plugins/inputs/statsd/running_stats.go | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 29854d7c3b0b5..827115d6b35c3 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -30,22 +30,20 @@ type RunningStats struct { lower float64 upper float64 + // cache if we have sorted the list so that we never re-sort a sorted list, + // which can have very bad performance. + SortedPerc bool + // Array used to calculate estimated median values // We will store a maximum of MedLimit values, at which point we will start // slicing old values med []float64 MedLimit int - - // cache if we have sorted the list so that we never re-sort a sorted list, - // which can have very bad performance. - SortedPerc bool - SortedMed bool } func (rs *RunningStats) AddValue(v float64) { // Whenever a value is added, the list is no longer sorted. rs.SortedPerc = false - rs.SortedMed = false if rs.n == 0 { rs.k = v @@ -97,18 +95,16 @@ func (rs *RunningStats) Mean() float64 { } func (rs *RunningStats) Median() float64 { - count := len(rs.med) - // Need to sort for median - if !rs.SortedMed { - sort.Float64s(rs.med) - rs.SortedMed = true - } + // Need to sort for median, but keep temporal order + sorted := rs.med + sort.Float64s(sorted) + count := len(sorted) if count == 0 { return 0 } else if count%2 == 0 { - return (rs.med[count/2-1] + rs.med[count/2]) / 2 + return (sorted[count/2-1] + sorted[count/2]) / 2 } else { - return rs.med[count/2] + return sorted[count/2] } } From c3f3ce94554d6d0f8a770bea96b0bfe8fa35afc4 Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Wed, 20 Jul 2022 05:52:45 -0700 Subject: [PATCH 09/13] append to a slice instead --- plugins/inputs/statsd/running_stats.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 827115d6b35c3..2c20ed36f6118 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -96,15 +96,16 @@ func (rs *RunningStats) Mean() float64 { func (rs *RunningStats) Median() float64 { // Need to sort for median, but keep temporal order - sorted := rs.med - sort.Float64s(sorted) - count := len(sorted) + var values []float64 + values = append(values, rs.med...) + sort.Float64s(values) + count := len(values) if count == 0 { return 0 } else if count%2 == 0 { - return (sorted[count/2-1] + sorted[count/2]) / 2 + return (values[count/2-1] + values[count/2]) / 2 } else { - return sorted[count/2] + return values[count/2] } } From 23fe6a62a8555c70f672f3e35e4bbadb03a1c1e2 Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Wed, 20 Jul 2022 07:04:21 -0700 Subject: [PATCH 10/13] don't always replace last and first item when limit is reached --- plugins/inputs/statsd/running_stats.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 2c20ed36f6118..2ace0fc5500e3 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -37,8 +37,9 @@ type RunningStats struct { // Array used to calculate estimated median values // We will store a maximum of MedLimit values, at which point we will start // slicing old values - med []float64 - MedLimit int + med []float64 + MedLimit int + MedInsertIndex int } func (rs *RunningStats) AddValue(v float64) { @@ -54,6 +55,7 @@ func (rs *RunningStats) AddValue(v float64) { } if rs.MedLimit == 0 { rs.MedLimit = defaultMedianLimit + rs.MedInsertIndex = defaultMedianLimit - 1 } rs.perc = make([]float64, 0, rs.PercLimit) rs.med = make([]float64, 0, rs.MedLimit) @@ -84,9 +86,9 @@ func (rs *RunningStats) AddValue(v float64) { if len(rs.med) < rs.MedLimit { rs.med = append(rs.med, v) } else { - // Reached limit, slice off first element - rs.med = rs.med[1:] - rs.med[len(rs.med)] = v + // Reached limit, insert at rear + rs.med[rs.MedInsertIndex] = v + rs.MedInsertIndex = (rs.MedInsertIndex + 1) % rs.MedLimit } } From e2a07087a5ede0fbf1d80d628752b7dcfc380519 Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Wed, 20 Jul 2022 07:23:24 -0700 Subject: [PATCH 11/13] switch order of operations when limit is reached --- plugins/inputs/statsd/running_stats.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 2ace0fc5500e3..99a30ff005a5d 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -86,9 +86,9 @@ func (rs *RunningStats) AddValue(v float64) { if len(rs.med) < rs.MedLimit { rs.med = append(rs.med, v) } else { - // Reached limit, insert at rear - rs.med[rs.MedInsertIndex] = v + // Reached limit, start over rs.MedInsertIndex = (rs.MedInsertIndex + 1) % rs.MedLimit + rs.med[rs.MedInsertIndex] = v } } From aee98901ebfcc8e74cfafc8fbb925dbb40d693a4 Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Wed, 20 Jul 2022 07:27:50 -0700 Subject: [PATCH 12/13] make logic less confusing by inserting at index 0 when limit is reached --- plugins/inputs/statsd/running_stats.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 99a30ff005a5d..0963632ffbf8c 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -55,7 +55,7 @@ func (rs *RunningStats) AddValue(v float64) { } if rs.MedLimit == 0 { rs.MedLimit = defaultMedianLimit - rs.MedInsertIndex = defaultMedianLimit - 1 + rs.MedInsertIndex = 0 } rs.perc = make([]float64, 0, rs.PercLimit) rs.med = make([]float64, 0, rs.MedLimit) @@ -87,8 +87,8 @@ func (rs *RunningStats) AddValue(v float64) { rs.med = append(rs.med, v) } else { // Reached limit, start over - rs.MedInsertIndex = (rs.MedInsertIndex + 1) % rs.MedLimit rs.med[rs.MedInsertIndex] = v + rs.MedInsertIndex = (rs.MedInsertIndex + 1) % rs.MedLimit } } From 1269f21adc4dd6e93f73726b28eadf448aca8d55 Mon Sep 17 00:00:00 2001 From: Amarinder Cheema Date: Thu, 21 Jul 2022 08:01:54 -0700 Subject: [PATCH 13/13] add tests for median and check if MedLimit and MedInsertIndex are respected and always update MedInsertIndex --- plugins/inputs/statsd/running_stats.go | 2 +- plugins/inputs/statsd/running_stats_test.go | 30 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/plugins/inputs/statsd/running_stats.go b/plugins/inputs/statsd/running_stats.go index 0963632ffbf8c..08f24fe8861fd 100644 --- a/plugins/inputs/statsd/running_stats.go +++ b/plugins/inputs/statsd/running_stats.go @@ -88,8 +88,8 @@ func (rs *RunningStats) AddValue(v float64) { } else { // Reached limit, start over rs.med[rs.MedInsertIndex] = v - rs.MedInsertIndex = (rs.MedInsertIndex + 1) % rs.MedLimit } + rs.MedInsertIndex = (rs.MedInsertIndex + 1) % rs.MedLimit } func (rs *RunningStats) Mean() float64 { diff --git a/plugins/inputs/statsd/running_stats_test.go b/plugins/inputs/statsd/running_stats_test.go index 2cf987a69bbf1..267f9e156a09e 100644 --- a/plugins/inputs/statsd/running_stats_test.go +++ b/plugins/inputs/statsd/running_stats_test.go @@ -17,6 +17,9 @@ func TestRunningStats_Single(t *testing.T) { if rs.Mean() != 10.1 { t.Errorf("Expected %v, got %v", 10.1, rs.Mean()) } + if rs.Median() != 10.1 { + t.Errorf("Expected %v, got %v", 10.1, rs.Median()) + } if rs.Upper() != 10.1 { t.Errorf("Expected %v, got %v", 10.1, rs.Upper()) } @@ -61,6 +64,9 @@ func TestRunningStats_Duplicate(t *testing.T) { if rs.Mean() != 10.1 { t.Errorf("Expected %v, got %v", 10.1, rs.Mean()) } + if rs.Median() != 10.1 { + t.Errorf("Expected %v, got %v", 10.1, rs.Median()) + } if rs.Upper() != 10.1 { t.Errorf("Expected %v, got %v", 10.1, rs.Upper()) } @@ -105,6 +111,9 @@ func TestRunningStats(t *testing.T) { if rs.Mean() != 15.9375 { t.Errorf("Expected %v, got %v", 15.9375, rs.Mean()) } + if rs.Median() != 10.5 { + t.Errorf("Expected %v, got %v", 10.5, rs.Median()) + } if rs.Upper() != 45 { t.Errorf("Expected %v, got %v", 45, rs.Upper()) } @@ -164,3 +173,24 @@ func TestRunningStats_PercentileLimit(t *testing.T) { func fuzzyEqual(a, b, epsilon float64) bool { return math.Abs(a-b) <= epsilon } + +// Test that the median limit is respected and MedInsertIndex is properly incrementing index. +func TestRunningStats_MedianLimitIndex(t *testing.T) { + rs := RunningStats{} + rs.MedLimit = 10 + values := []float64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + + for _, v := range values { + rs.AddValue(v) + } + + if rs.Count() != 11 { + t.Errorf("Expected %v, got %v", 11, rs.Count()) + } + if len(rs.med) != 10 { + t.Errorf("Expected %v, got %v", 10, len(rs.med)) + } + if rs.MedInsertIndex != 1 { + t.Errorf("Expected %v, got %v", 0, rs.MedInsertIndex) + } +}