Files
coder/coderd/prometheusmetrics/aggregator_test.go
T
dependabot[bot]andEthan Dickson 519812776e chore: bump github.com/stretchr/testify from 1.10.0 to 1.11.1 (#19599)
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify)
from 1.10.0 to 1.11.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/stretchr/testify/releases">github.com/stretchr/testify's
releases</a>.</em></p>
<blockquote>
<h2>v1.11.1</h2>
<p>This release fixes <a
href="https://redirect.github.com/stretchr/testify/issues/1785">#1785</a>
introduced in v1.11.0 where expected argument values implementing the
stringer interface (<code>String() string</code>) with a method which
mutates their value, when passed to mock.Mock.On
(<code>m.On(&quot;Method&quot;, &lt;expected&gt;).Return()</code>) or
actual argument values passed to mock.Mock.Called may no longer match
one another where they previously did match. The behaviour prior to
v1.11.0 where the stringer is always called is restored. Future testify
releases may not call the stringer method at all in this case.</p>
<h2>What's Changed</h2>
<ul>
<li>Backport <a
href="https://redirect.github.com/stretchr/testify/issues/1786">#1786</a>
to release/1.11: mock: revert to pre-v1.11.0 argument matching behavior
for mutating stringers by <a
href="https://github.com/brackendawson"><code>@​brackendawson</code></a>
in <a
href="https://redirect.github.com/stretchr/testify/pull/1788">stretchr/testify#1788</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/stretchr/testify/compare/v1.11.0...v1.11.1">https://github.com/stretchr/testify/compare/v1.11.0...v1.11.1</a></p>
<h2>v1.11.0</h2>
<h2>What's Changed</h2>
<h3>Functional Changes</h3>
<p>v1.11.0 Includes a number of performance improvements.</p>
<ul>
<li>Call stack perf change for CallerInfo by <a
href="https://github.com/mikeauclair"><code>@​mikeauclair</code></a> in
<a
href="https://redirect.github.com/stretchr/testify/pull/1614">stretchr/testify#1614</a></li>
<li>Lazily render mock diff output on successful match by <a
href="https://github.com/mikeauclair"><code>@​mikeauclair</code></a> in
<a
href="https://redirect.github.com/stretchr/testify/pull/1615">stretchr/testify#1615</a></li>
<li>assert: check early in Eventually, EventuallyWithT, and Never by <a
href="https://github.com/cszczepaniak"><code>@​cszczepaniak</code></a>
in <a
href="https://redirect.github.com/stretchr/testify/pull/1427">stretchr/testify#1427</a></li>
<li>assert: add IsNotType by <a
href="https://github.com/bartventer"><code>@​bartventer</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1730">stretchr/testify#1730</a></li>
<li>assert.JSONEq: shortcut if same strings by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1754">stretchr/testify#1754</a></li>
<li>assert.YAMLEq: shortcut if same strings by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1755">stretchr/testify#1755</a></li>
<li>assert: faster and simpler isEmpty using reflect.Value.IsZero by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1761">stretchr/testify#1761</a></li>
<li>suite: faster methods filtering (internal refactor) by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1758">stretchr/testify#1758</a></li>
</ul>
<h3>Fixes</h3>
<ul>
<li>assert.ErrorAs: log target type by <a
href="https://github.com/craig65535"><code>@​craig65535</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1345">stretchr/testify#1345</a></li>
<li>Fix failure message formatting for Positive and Negative asserts in
<a
href="https://redirect.github.com/stretchr/testify/pull/1062">stretchr/testify#1062</a></li>
<li>Improve ErrorIs message when error is nil but an error was expected
by <a href="https://github.com/tsioftas"><code>@​tsioftas</code></a> in
<a
href="https://redirect.github.com/stretchr/testify/pull/1681">stretchr/testify#1681</a></li>
<li>fix Subset/NotSubset when calling with mixed input types by <a
href="https://github.com/siliconbrain"><code>@​siliconbrain</code></a>
in <a
href="https://redirect.github.com/stretchr/testify/pull/1729">stretchr/testify#1729</a></li>
<li>Improve ErrorAs failure message when error is nil by <a
href="https://github.com/ccoVeille"><code>@​ccoVeille</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1734">stretchr/testify#1734</a></li>
<li>mock.AssertNumberOfCalls: improve error msg by <a
href="https://github.com/3scalation"><code>@​3scalation</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1743">stretchr/testify#1743</a></li>
</ul>
<h3>Documentation, Build &amp; CI</h3>
<ul>
<li>docs: Fix typo in README by <a
href="https://github.com/alexandear"><code>@​alexandear</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1688">stretchr/testify#1688</a></li>
<li>Replace deprecated io/ioutil with io and os by <a
href="https://github.com/alexandear"><code>@​alexandear</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1684">stretchr/testify#1684</a></li>
<li>Document consequences of calling t.FailNow() by <a
href="https://github.com/greg0ire"><code>@​greg0ire</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1710">stretchr/testify#1710</a></li>
<li>chore: update docs for Unset <a
href="https://redirect.github.com/stretchr/testify/issues/1621">#1621</a>
by <a href="https://github.com/techfg"><code>@​techfg</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1709">stretchr/testify#1709</a></li>
<li>README: apply gofmt to examples by <a
href="https://github.com/alexandear"><code>@​alexandear</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1687">stretchr/testify#1687</a></li>
<li>refactor: use %q and %T to simplify fmt.Sprintf by <a
href="https://github.com/alexandear"><code>@​alexandear</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1674">stretchr/testify#1674</a></li>
<li>Propose Christophe Colombier (ccoVeille) as approver by <a
href="https://github.com/brackendawson"><code>@​brackendawson</code></a>
in <a
href="https://redirect.github.com/stretchr/testify/pull/1716">stretchr/testify#1716</a></li>
<li>Update documentation for the Error function in assert or require
package by <a
href="https://github.com/architagr"><code>@​architagr</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1675">stretchr/testify#1675</a></li>
<li>assert: remove deprecated build constraints by <a
href="https://github.com/alexandear"><code>@​alexandear</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1671">stretchr/testify#1671</a></li>
<li>assert: apply gofumpt to internal test suite by <a
href="https://github.com/ccoVeille"><code>@​ccoVeille</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1739">stretchr/testify#1739</a></li>
<li>CI: fix shebang in .ci.*.sh scripts by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1746">stretchr/testify#1746</a></li>
<li>assert,require: enable parallel testing on (almost) all top tests by
<a href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1747">stretchr/testify#1747</a></li>
<li>suite.Passed: add one more status test report by <a
href="https://github.com/Ararsa-Derese"><code>@​Ararsa-Derese</code></a>
in <a
href="https://redirect.github.com/stretchr/testify/pull/1706">stretchr/testify#1706</a></li>
<li>Add Helper() method in internal mocks and assert.CollectT by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1423">stretchr/testify#1423</a></li>
<li>assert.Same/NotSame: improve usage of Sprintf by <a
href="https://github.com/ccoVeille"><code>@​ccoVeille</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1742">stretchr/testify#1742</a></li>
<li>mock: enable parallel testing on internal testsuite by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1756">stretchr/testify#1756</a></li>
<li>suite: cleanup use of 'testing' internals at runtime by <a
href="https://github.com/dolmen"><code>@​dolmen</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1751">stretchr/testify#1751</a></li>
<li>assert: check test failure message for Empty and NotEmpty by <a
href="https://github.com/ccoVeille"><code>@​ccoVeille</code></a> in <a
href="https://redirect.github.com/stretchr/testify/pull/1745">stretchr/testify#1745</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/stretchr/testify/commit/2a57335dc9cd6833daa820bc94d9b40c26a7917d"><code>2a57335</code></a>
Merge pull request <a
href="https://redirect.github.com/stretchr/testify/issues/1788">#1788</a>
from brackendawson/1785-backport-1.11</li>
<li><a
href="https://github.com/stretchr/testify/commit/af8c91234f184009f57ef29027b39ca89cb00100"><code>af8c912</code></a>
Backport <a
href="https://redirect.github.com/stretchr/testify/issues/1786">#1786</a>
to release/1.11</li>
<li><a
href="https://github.com/stretchr/testify/commit/b7801fbf5cd58d201296d5d0e132d1849966dbd4"><code>b7801fb</code></a>
Merge pull request <a
href="https://redirect.github.com/stretchr/testify/issues/1778">#1778</a>
from stretchr/dependabot/github_actions/actions/chec...</li>
<li><a
href="https://github.com/stretchr/testify/commit/69831f3b08c40d56a09d0be93e9d5ae034f1590b"><code>69831f3</code></a>
build(deps): bump actions/checkout from 4 to 5</li>
<li><a
href="https://github.com/stretchr/testify/commit/a53be35c3b0cfcd5189cffcfd75df60ea581104c"><code>a53be35</code></a>
Improve captureTestingT helper</li>
<li><a
href="https://github.com/stretchr/testify/commit/aafb604176db7e1f2c9810bc90d644291d057687"><code>aafb604</code></a>
mock: improve formatting of error message</li>
<li><a
href="https://github.com/stretchr/testify/commit/7218e0390acd2aea3edb18574110ec2753c0aeef"><code>7218e03</code></a>
improve error msg</li>
<li><a
href="https://github.com/stretchr/testify/commit/929a2126c2702df436312656a0304580b526c6e9"><code>929a212</code></a>
Merge pull request <a
href="https://redirect.github.com/stretchr/testify/issues/1758">#1758</a>
from stretchr/dolmen/suite-faster-method-filtering</li>
<li><a
href="https://github.com/stretchr/testify/commit/bc7459ec38128532ff32f23cfab4ea0b725210f2"><code>bc7459e</code></a>
suite: faster filtering of methods (-testify.m)</li>
<li><a
href="https://github.com/stretchr/testify/commit/7d37b5c962954410bcd7a71ff3a77c79514056d1"><code>7d37b5c</code></a>
suite: refactor methodFilter</li>
<li>Additional commits viewable in <a
href="https://github.com/stretchr/testify/compare/v1.10.0...v1.11.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/stretchr/testify&package-manager=go_modules&previous-version=1.10.0&new-version=1.11.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ethan Dickson <ethan@coder.com>
2025-09-02 03:12:37 +00:00

727 lines
24 KiB
Go

package prometheusmetrics_test
import (
"context"
"fmt"
"sort"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/agentmetrics"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/coderd/prometheusmetrics"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/testutil"
)
const (
testWorkspaceName = "yogi-workspace"
testUsername = "yogi-bear"
testAgentName = "main-agent"
testTemplateName = "main-template"
)
var testLabels = prometheusmetrics.AgentMetricLabels{
Username: testUsername,
WorkspaceName: testWorkspaceName,
AgentName: testAgentName,
TemplateName: testTemplateName,
}
func TestUpdateMetrics_MetricsDoNotExpire(t *testing.T) {
t.Parallel()
// given
registry := prometheus.NewRegistry()
metricsAggregator, err := prometheusmetrics.NewMetricsAggregator(slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}), registry, time.Hour, nil) // time.Hour, so metrics won't expire
require.NoError(t, err)
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
closeFunc := metricsAggregator.Run(ctx)
t.Cleanup(closeFunc)
given1 := []*agentproto.Stats_Metric{
{Name: "a_counter_one", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
{Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 2},
// Tests that we update labels correctly when they have extra labels
{Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 27, Labels: []*agentproto.Stats_Metric_Label{
{Name: "lizz", Value: "rizz"},
}},
{Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 3},
}
given2 := []*agentproto.Stats_Metric{
{Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 4},
// Tests that we update labels correctly when they have extra labels
{Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: -9, Labels: []*agentproto.Stats_Metric_Label{
{Name: "lizz", Value: "rizz"},
}},
{Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 5},
{Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 2, Labels: []*agentproto.Stats_Metric_Label{
{Name: "foobar", Value: "Foobaz"},
{Name: "hello", Value: "world"},
}},
{Name: "d_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 6},
{Name: "e_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 15, Labels: []*agentproto.Stats_Metric_Label{
{Name: "foobar", Value: "Foo,ba=z"},
{Name: "halo", Value: "wor\\,d=1,e=\\,2"},
{Name: "hello", Value: "wo,,r=d"},
}},
{Name: "f_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 6, Labels: []*agentproto.Stats_Metric_Label{
{Name: "empty", Value: ""},
{Name: "foobar", Value: "foobaz"},
}},
}
given3 := []*agentproto.Stats_Metric{
{Name: "e_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 17, Labels: []*agentproto.Stats_Metric_Label{
{Name: "cat", Value: "do,=g"},
{Name: "hello", Value: "wo,,rld"},
}},
{Name: "f_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 8, Labels: []*agentproto.Stats_Metric_Label{
{Name: "foobar", Value: "foobaz"},
}},
}
commonLabels := []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: testAgentName},
{Name: agentmetrics.LabelUsername, Value: testUsername},
{Name: agentmetrics.LabelWorkspaceName, Value: testWorkspaceName},
{Name: agentmetrics.LabelTemplateName, Value: testTemplateName},
}
expected := []*agentproto.Stats_Metric{
{Name: "a_counter_one", Type: agentproto.Stats_Metric_COUNTER, Value: 1, Labels: commonLabels},
{Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: -9, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: testAgentName},
{Name: "lizz", Value: "rizz"},
{Name: agentmetrics.LabelUsername, Value: testUsername},
{Name: agentmetrics.LabelWorkspaceName, Value: testWorkspaceName},
{Name: agentmetrics.LabelTemplateName, Value: testTemplateName},
}},
{Name: "b_counter_two", Type: agentproto.Stats_Metric_COUNTER, Value: 4, Labels: commonLabels},
{Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 2, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: testAgentName},
{Name: "foobar", Value: "Foobaz"},
{Name: "hello", Value: "world"},
{Name: agentmetrics.LabelUsername, Value: testUsername},
{Name: agentmetrics.LabelWorkspaceName, Value: testWorkspaceName},
{Name: agentmetrics.LabelTemplateName, Value: testTemplateName},
}},
{Name: "c_gauge_three", Type: agentproto.Stats_Metric_GAUGE, Value: 5, Labels: commonLabels},
{Name: "d_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 6, Labels: commonLabels},
{Name: "e_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 17, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: testAgentName},
{Name: "cat", Value: "do,=g"},
{Name: "hello", Value: "wo,,rld"},
{Name: agentmetrics.LabelUsername, Value: testUsername},
{Name: agentmetrics.LabelWorkspaceName, Value: testWorkspaceName},
{Name: agentmetrics.LabelTemplateName, Value: testTemplateName},
}},
{Name: "e_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 15, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: testAgentName},
{Name: "foobar", Value: "Foo,ba=z"},
{Name: "halo", Value: "wor\\,d=1,e=\\,2"},
{Name: "hello", Value: "wo,,r=d"},
{Name: agentmetrics.LabelUsername, Value: testUsername},
{Name: agentmetrics.LabelWorkspaceName, Value: testWorkspaceName},
{Name: agentmetrics.LabelTemplateName, Value: testTemplateName},
}},
{Name: "f_gauge_four", Type: agentproto.Stats_Metric_GAUGE, Value: 8, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: testAgentName},
{Name: "foobar", Value: "foobaz"},
{Name: agentmetrics.LabelUsername, Value: testUsername},
{Name: agentmetrics.LabelWorkspaceName, Value: testWorkspaceName},
{Name: agentmetrics.LabelTemplateName, Value: testTemplateName},
}},
}
// when
metricsAggregator.Update(ctx, testLabels, given1)
metricsAggregator.Update(ctx, testLabels, given2)
metricsAggregator.Update(ctx, testLabels, given3)
// then
require.Eventually(t, func() bool {
var actual []prometheus.Metric
metricsCh := make(chan prometheus.Metric)
done := make(chan struct{}, 1)
defer close(done)
go func() {
for m := range metricsCh {
actual = append(actual, m)
}
done <- struct{}{}
}()
metricsAggregator.Collect(metricsCh)
close(metricsCh)
<-done
return verifyCollectedMetrics(t, expected, actual)
}, testutil.WaitMedium, testutil.IntervalSlow)
}
func verifyCollectedMetrics(t *testing.T, expected []*agentproto.Stats_Metric, actual []prometheus.Metric) bool {
if len(expected) != len(actual) {
t.Logf("expected %d metrics, got %d", len(expected), len(actual))
return false
}
// ensure stable iteration order
sort.Slice(expected, func(i, j int) bool {
return expected[i].Name < expected[j].Name
})
sort.Slice(actual, func(i, j int) bool {
m1 := prometheusMetricToString(t, actual[i])
m2 := prometheusMetricToString(t, actual[j])
return m1 < m2
})
for i, e := range expected {
desc := actual[i].Desc()
assert.Contains(t, desc.String(), e.Name)
var d dto.Metric
err := actual[i].Write(&d)
assert.NoError(t, err)
switch e.Type {
case agentproto.Stats_Metric_COUNTER:
if e.Value != d.Counter.GetValue() {
return false
}
case agentproto.Stats_Metric_GAUGE:
if e.Value != d.Gauge.GetValue() {
return false
}
default:
assert.Failf(t, "unsupported type: %s", string(e.Type))
}
expectedLabels := make([]*agentproto.Stats_Metric_Label, len(e.Labels))
copy(expectedLabels, e.Labels)
dtoLabels := asMetricAgentLabels(d.GetLabel())
// dto labels are sorted in alphabetical order.
sortFn := func(i, j int) bool {
return expectedLabels[i].Name < expectedLabels[j].Name
}
sort.Slice(expectedLabels, sortFn)
sort.Slice(dtoLabels, sortFn)
assert.Equal(t, expectedLabels, dtoLabels, d.String())
}
return true
}
func prometheusMetricToString(t *testing.T, m prometheus.Metric) string {
var sb strings.Builder
desc := m.Desc()
_, _ = sb.WriteString(desc.String())
_ = sb.WriteByte('|')
var d dto.Metric
err := m.Write(&d)
assert.NoError(t, err)
dtoLabels := asMetricAgentLabels(d.GetLabel())
sort.Slice(dtoLabels, func(i, j int) bool {
return dtoLabels[i].Name < dtoLabels[j].Name
})
for _, dtoLabel := range dtoLabels {
if dtoLabel.Value == "" {
continue
}
_, _ = sb.WriteString(dtoLabel.Name)
_ = sb.WriteByte('=')
_, _ = sb.WriteString(prometheusmetrics.MetricLabelValueEncoder.Replace(dtoLabel.Value))
}
return strings.TrimRight(sb.String(), ",")
}
func asMetricAgentLabels(dtoLabels []*dto.LabelPair) []*agentproto.Stats_Metric_Label {
metricLabels := make([]*agentproto.Stats_Metric_Label, 0, len(dtoLabels))
for _, dtoLabel := range dtoLabels {
if dtoLabel.GetValue() == "" {
continue
}
metricLabels = append(metricLabels, &agentproto.Stats_Metric_Label{
Name: dtoLabel.GetName(),
Value: dtoLabel.GetValue(),
})
}
return metricLabels
}
func TestUpdateMetrics_MetricsExpire(t *testing.T) {
t.Parallel()
// given
registry := prometheus.NewRegistry()
metricsAggregator, err := prometheusmetrics.NewMetricsAggregator(slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}), registry, time.Millisecond, agentmetrics.LabelAll)
require.NoError(t, err)
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
closeFunc := metricsAggregator.Run(ctx)
t.Cleanup(closeFunc)
given := []*agentproto.Stats_Metric{
{Name: "a_counter_one", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
}
// when
metricsAggregator.Update(ctx, testLabels, given)
time.Sleep(time.Millisecond * 10) // Ensure that metric is expired
// then
require.Eventually(t, func() bool {
var actual []prometheus.Metric
metricsCh := make(chan prometheus.Metric)
done := make(chan struct{}, 1)
defer close(done)
go func() {
for m := range metricsCh {
actual = append(actual, m)
}
done <- struct{}{}
}()
metricsAggregator.Collect(metricsCh)
close(metricsCh)
<-done
return len(actual) == 0
}, testutil.WaitShort, testutil.IntervalFast)
}
func TestLabelsAggregation(t *testing.T) {
t.Parallel()
type statCollection struct {
labels prometheusmetrics.AgentMetricLabels
metrics []*agentproto.Stats_Metric
}
commonLabels := []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelUsername, Value: testUsername},
{Name: agentmetrics.LabelAgentName, Value: testAgentName},
{Name: agentmetrics.LabelWorkspaceName, Value: testWorkspaceName},
{Name: agentmetrics.LabelTemplateName, Value: testTemplateName},
}
tests := []struct {
name string
given []statCollection
expected []*agentproto.Stats_Metric
aggregateOn []string
}{
{
name: "label aggregations not specified, keep all (high cardinality, default behavior)",
aggregateOn: agentmetrics.LabelAll,
given: []statCollection{
{
labels: testLabels,
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
{
labels: testLabels,
metrics: []*agentproto.Stats_Metric{
{Name: "active_conns", Type: agentproto.Stats_Metric_GAUGE, Value: 4},
},
},
},
expected: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1, Labels: commonLabels},
{Name: "active_conns", Type: agentproto.Stats_Metric_GAUGE, Value: 4, Labels: commonLabels},
},
},
{
// Scenario: 2 users are using the same agent and we've configured the deployment to aggregate on the "agent_name" label.
name: "single label aggregation, aggregating to single metric",
aggregateOn: []string{agentmetrics.LabelAgentName},
given: []statCollection{
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user1",
AgentName: "agent1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user2",
AgentName: "agent1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 7},
},
},
},
expected: []*agentproto.Stats_Metric{
// We only observed one agent_name value, so all metrics are aggregated to a single series.
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 8, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: "agent1"},
}},
},
},
{
// Scenario: as above, but we're aggregating on two invariant labels.
name: "multiple label aggregation, aggregating to single metric",
aggregateOn: []string{agentmetrics.LabelAgentName, agentmetrics.LabelTemplateName},
given: []statCollection{
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user1",
AgentName: "agent1",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user2",
AgentName: "agent1",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 7},
},
},
},
expected: []*agentproto.Stats_Metric{
// We only observed one agent_name & template_name tuple, so all metrics are aggregated to a single series.
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 8, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelAgentName, Value: "agent1"},
{Name: agentmetrics.LabelTemplateName, Value: "template1"},
}},
},
},
{
// Scenario: aggregating on a label which is unique across all metrics.
name: "single label aggregation, aggregating to multiple metrics",
aggregateOn: []string{agentmetrics.LabelUsername},
given: []statCollection{
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user1",
AgentName: "agent1",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user2",
AgentName: "agent1",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 7},
},
},
},
expected: []*agentproto.Stats_Metric{
// We observed two unique username values, and therefore we have a metric for each.
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelUsername, Value: "user1"},
}},
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 7, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelUsername, Value: "user2"},
}},
},
},
{
// Scenario: aggregating on a label which is unique across all metrics, plus two invariant labels.
name: "multiple label aggregation, aggregating to multiple metrics",
aggregateOn: []string{agentmetrics.LabelUsername, agentmetrics.LabelAgentName, agentmetrics.LabelTemplateName},
given: []statCollection{
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user1",
AgentName: "agent1",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "user2",
AgentName: "agent1",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 7},
},
},
},
expected: []*agentproto.Stats_Metric{
// We observed two unique username values, and therefore we have a metric for each.
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelUsername, Value: "user1"},
{Name: agentmetrics.LabelAgentName, Value: "agent1"},
{Name: agentmetrics.LabelTemplateName, Value: "template1"},
}},
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 7, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelUsername, Value: "user2"},
{Name: agentmetrics.LabelAgentName, Value: "agent1"},
{Name: agentmetrics.LabelTemplateName, Value: "template1"},
}},
},
},
{
name: "extra labels are retained, even with label aggregations",
aggregateOn: []string{agentmetrics.LabelUsername},
given: []statCollection{
{
labels: testLabels,
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
{
labels: testLabels,
metrics: []*agentproto.Stats_Metric{
{Name: "extra_label", Type: agentproto.Stats_Metric_COUNTER, Value: 27, Labels: []*agentproto.Stats_Metric_Label{
{Name: "lizz", Value: "rizz"},
}},
},
},
},
expected: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelUsername, Value: testUsername},
}},
{Name: "extra_label", Type: agentproto.Stats_Metric_COUNTER, Value: 27, Labels: []*agentproto.Stats_Metric_Label{
{Name: "lizz", Value: "rizz"},
{Name: agentmetrics.LabelUsername, Value: testUsername},
}},
},
},
{
// Both counters and gauges should have all their values summed to produce the correct output.
name: "counters & gauges behave identically",
aggregateOn: []string{agentmetrics.LabelTemplateName},
given: []statCollection{
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "username1",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
{Name: "active_conns", Type: agentproto.Stats_Metric_GAUGE, Value: 3},
},
},
{
labels: prometheusmetrics.AgentMetricLabels{
Username: "username2",
TemplateName: "template1",
},
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 2},
{Name: "active_conns", Type: agentproto.Stats_Metric_GAUGE, Value: 4},
},
},
},
expected: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 3, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelTemplateName, Value: "template1"},
}},
{Name: "active_conns", Type: agentproto.Stats_Metric_GAUGE, Value: 7, Labels: []*agentproto.Stats_Metric_Label{
{Name: agentmetrics.LabelTemplateName, Value: "template1"},
}},
},
},
{
// Scenario: validation fails and an invalid label is selected for aggregation.
name: "invalid label aggregation",
aggregateOn: []string{"nonsense"},
given: []statCollection{
{
labels: testLabels,
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
},
// Nothing will be returned.
expected: []*agentproto.Stats_Metric{},
},
{
// Scenario: validation fails and an empty list is given for aggregation.
name: "empty label aggregation list",
aggregateOn: []string{},
given: []statCollection{
{
labels: testLabels,
metrics: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1},
},
},
},
// Default aggregation will be used.
expected: []*agentproto.Stats_Metric{
{Name: "user_counter", Type: agentproto.Stats_Metric_COUNTER, Value: 1, Labels: commonLabels},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// given
registry := prometheus.NewRegistry()
metricsAggregator, err := prometheusmetrics.NewMetricsAggregator(slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}), registry, time.Hour, tc.aggregateOn) // time.Hour, so metrics won't expire
require.NoError(t, err)
ctx, cancelFunc := context.WithCancel(context.Background())
t.Cleanup(cancelFunc)
closeFunc := metricsAggregator.Run(ctx)
t.Cleanup(closeFunc)
// when
for _, sc := range tc.given {
metricsAggregator.Update(ctx, sc.labels, sc.metrics)
}
// then
require.Eventually(t, func() bool {
var actual []prometheus.Metric
metricsCh := make(chan prometheus.Metric)
done := make(chan struct{}, 1)
defer close(done)
go func() {
for m := range metricsCh {
actual = append(actual, m)
}
done <- struct{}{}
}()
metricsAggregator.Collect(metricsCh)
close(metricsCh)
<-done
return verifyCollectedMetrics(t, tc.expected, actual)
}, testutil.WaitMedium, testutil.IntervalSlow)
})
}
}
func Benchmark_MetricsAggregator_Run(b *testing.B) {
benchmarkRunner(b, agentmetrics.LabelAll)
}
func Benchmark_MetricsAggregator_RunWithAggregations(b *testing.B) {
for i := 1; i <= len(agentmetrics.LabelAll); i++ {
b.Run(fmt.Sprintf("%d labels", i), func(b *testing.B) {
benchmarkRunner(b, agentmetrics.LabelAll[0:i])
})
}
}
func benchmarkRunner(b *testing.B, aggregateByLabels []string) {
b.ReportAllocs()
// Number of metrics to generate and send in each iteration.
// Hard-coded to 1024 to avoid overflowing the queue in the metrics aggregator.
numMetrics := 1024
// given
registry := prometheus.NewRegistry()
metricsAggregator := must(prometheusmetrics.NewMetricsAggregator(slogtest.Make(b, &slogtest.Options{IgnoreErrors: true}), registry, time.Hour, aggregateByLabels))
ctx, cancelFunc := context.WithCancel(context.Background())
b.Cleanup(cancelFunc)
closeFunc := metricsAggregator.Run(ctx)
b.Cleanup(closeFunc)
ch := make(chan prometheus.Metric)
go func() {
for {
select {
case <-ctx.Done():
return
default:
metricsAggregator.Collect(ch)
}
}
}()
for i := 0; i < b.N; i++ {
b.StopTimer()
b.Logf("N=%d generating %d metrics", b.N, numMetrics)
metrics := make([]*agentproto.Stats_Metric, 0, numMetrics)
for i := 0; i < numMetrics; i++ {
metrics = append(metrics, genAgentMetric(b))
}
b.Logf("N=%d sending %d metrics", b.N, numMetrics)
var nGot atomic.Int64
b.StartTimer()
metricsAggregator.Update(ctx, testLabels, metrics)
for i := 0; i < numMetrics; i++ {
select {
case <-ctx.Done():
b.FailNow()
case <-ch:
nGot.Add(1)
}
}
b.StopTimer()
b.Logf("N=%d got %d metrics", b.N, nGot.Load())
}
}
func genAgentMetric(t testing.TB) *agentproto.Stats_Metric {
t.Helper()
var metricType agentproto.Stats_Metric_Type
if must(cryptorand.Float64()) >= 0.5 {
metricType = agentproto.Stats_Metric_COUNTER
} else {
metricType = agentproto.Stats_Metric_GAUGE
}
// Ensure that metric name does not start or end with underscore, as it is not allowed by Prometheus.
metricName := "metric_" + must(cryptorand.StringCharset(cryptorand.Alpha, 80)) + "_gen"
// Generate random metric value between 0 and 1000.
metricValue := must(cryptorand.Float64()) * float64(must(cryptorand.Intn(1000)))
return &agentproto.Stats_Metric{
Name: metricName, Type: metricType, Value: metricValue, Labels: []*agentproto.Stats_Metric_Label{},
}
}
func must[T any](t T, err error) T {
if err != nil {
panic(err)
}
return t
}