| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598 |
- package synthetic
- import (
- "maps"
- "math"
- "testing"
- "time"
- "github.com/opencost/opencost/core/pkg/source"
- "github.com/opencost/opencost/core/pkg/util"
- "github.com/opencost/opencost/modules/collector-source/pkg/metric"
- )
- var _ metric.Updater = (*FuncUpdater)(nil)
- type FuncUpdater struct {
- f func(*metric.UpdateSet)
- }
- func NewFuncUpdater(f func(*metric.UpdateSet)) *FuncUpdater {
- return &FuncUpdater{f}
- }
- func (fu *FuncUpdater) Update(set *metric.UpdateSet) {
- fu.f(set)
- }
- func toMemoryResource(m map[string]string) map[string]string {
- mm := maps.Clone(m)
- mm[source.ResourceLabel] = "memory"
- mm[source.UnitLabel] = "byte"
- return mm
- }
- func toCpuResource(m map[string]string) map[string]string {
- mm := maps.Clone(m)
- mm[source.ResourceLabel] = "cpu"
- mm[source.UnitLabel] = "core"
- return mm
- }
- func findMetric(t *testing.T, set *metric.UpdateSet, name string, container string) *metric.Update {
- t.Helper()
- var metric *metric.Update
- for _, update := range set.Updates {
- if update.Name == name && update.Labels[source.ContainerLabel] == container {
- metric = &update
- break
- }
- }
- return metric
- }
- func assertMetricValue(t *testing.T, set *metric.UpdateSet, name string, container string, value float64) {
- t.Helper()
- metric := findMetric(t, set, name, container)
- if metric == nil {
- t.Fatalf("Failed to Locate a %s Metric for Container: %s\n", name, container)
- return
- }
- if !util.IsApproximately(metric.Value, value) {
- t.Fatalf("Expected %f for %s [Container: %s], got: %f\n", value, name, container, metric.Value)
- return
- }
- }
- func assertMetricExists(t *testing.T, set *metric.UpdateSet, name string, container string) {
- t.Helper()
- metric := findMetric(t, set, name, container)
- if metric == nil {
- t.Fatalf("Failed to Locate a %s Metric for Container: %s\n", name, container)
- return
- }
- }
- func assertNoMetricExists(t *testing.T, set *metric.UpdateSet, name string, container string) {
- t.Helper()
- metric := findMetric(t, set, name, container)
- if metric != nil {
- t.Fatalf("Expected metric to not exist: %s Metric for Container: %s\n", name, container)
- return
- }
- }
- func TestMetricSynthesizerRAMAllocation(t *testing.T) {
- pod1Info := map[string]string{
- source.NamespaceLabel: "namespace1",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod1",
- source.UIDLabel: "pod-uuid1",
- }
- container1Info := map[string]string{
- source.NamespaceLabel: "namespace1",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod1",
- source.UIDLabel: "pod-uuid1",
- source.ContainerLabel: "container1",
- }
- container2Info := map[string]string{
- source.NamespaceLabel: "kube-system",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod2",
- source.UIDLabel: "pod-uuid2",
- source.ContainerLabel: "container2",
- }
- const startingCPUSeconds float64 = 506000.0
- updateSet1 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC),
- Updates: []metric.Update{
- // container1 has both requests and usage
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toMemoryResource(container1Info),
- Value: 4.0 * 1024 * 1024 * 1024,
- },
- {
- Name: metric.ContainerMemoryWorkingSetBytes,
- Labels: maps.Clone(container1Info),
- Value: 5.5 * 1024 * 1024 * 1024,
- },
- // container2 only has usage
- {
- Name: metric.ContainerMemoryWorkingSetBytes,
- Labels: maps.Clone(container2Info),
- Value: 1.5 * 1024 * 1024 * 1024,
- },
- // add some additional metrics to test filtering
- {
- Name: metric.KubeNamespaceLabels,
- Labels: maps.Clone(pod1Info),
- Value: 0,
- },
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 20,
- },
- },
- }
- updateSet2 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 30, 0, time.UTC),
- Updates: []metric.Update{
- // container1 has both requests and usage
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toMemoryResource(container1Info),
- Value: 4.0 * 1024 * 1024 * 1024,
- },
- {
- Name: metric.ContainerMemoryWorkingSetBytes,
- Labels: maps.Clone(container1Info),
- Value: 3.0 * 1024 * 1024 * 1024,
- },
- // container2 only has usage
- {
- Name: metric.ContainerMemoryWorkingSetBytes,
- Labels: maps.Clone(container2Info),
- Value: 2.5 * 1024 * 1024 * 1024,
- },
- // add some additional metrics to test filtering
- {
- Name: metric.KubeNamespaceLabels,
- Labels: maps.Clone(pod1Info),
- Value: 0,
- },
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 75,
- },
- },
- }
- updateSet3 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 1, 0, 0, time.UTC),
- Updates: []metric.Update{
- // container1 has both requests and usage
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toMemoryResource(container1Info),
- Value: 4.0 * 1024 * 1024 * 1024,
- },
- {
- Name: metric.ContainerMemoryWorkingSetBytes,
- Labels: maps.Clone(container1Info),
- Value: 6.0 * 1024 * 1024 * 1024,
- },
- // container2 only has usage
- {
- Name: metric.ContainerMemoryWorkingSetBytes,
- Labels: maps.Clone(container2Info),
- Value: 1.75 * 1024 * 1024 * 1024,
- },
- // add some additional metrics to test filtering
- {
- Name: metric.KubeNamespaceLabels,
- Labels: maps.Clone(pod1Info),
- Value: 0,
- },
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 135,
- },
- },
- }
- scrape := 0
- updater := NewFuncUpdater(func(us *metric.UpdateSet) {
- // first scrape:
- // - container1: max(4.0gb, 5.5gb)
- // - container2: 1.5gb
- if scrape == 0 {
- assertMetricValue(t, us, metric.ContainerMemoryAllocationBytes, "container1", 5.5*1024*1024*1024)
- assertMetricValue(t, us, metric.ContainerMemoryAllocationBytes, "container2", 1.5*1024*1024*1024)
- }
- // second scrape
- // - container1: max(4.0gb, 3.5gb)
- // - container2: 2.5gb
- if scrape == 1 {
- assertMetricValue(t, us, metric.ContainerMemoryAllocationBytes, "container1", 4.0*1024*1024*1024)
- assertMetricValue(t, us, metric.ContainerMemoryAllocationBytes, "container2", 2.5*1024*1024*1024)
- }
- // third scrape
- // - container1: max(4.0gb, 6.0gb)
- // - container2: 1.75gb
- if scrape == 2 {
- assertMetricValue(t, us, metric.ContainerMemoryAllocationBytes, "container1", 6.0*1024*1024*1024)
- assertMetricValue(t, us, metric.ContainerMemoryAllocationBytes, "container2", 1.75*1024*1024*1024)
- }
- scrape += 1
- })
- metricSynth := NewMetricSynthesizers(updater, NewContainerCpuAllocationSynthesizer(), NewContainerMemoryAllocationSynthesizer())
- metricSynth.Update(updateSet1)
- metricSynth.Update(updateSet2)
- metricSynth.Update(updateSet3)
- }
- func TestMetricSynthesizerCPUAllocation(t *testing.T) {
- pod1Info := map[string]string{
- source.NamespaceLabel: "namespace1",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod1",
- source.UIDLabel: "pod-uuid1",
- }
- container1Info := map[string]string{
- source.NamespaceLabel: "namespace1",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod1",
- source.UIDLabel: "pod-uuid1",
- source.ContainerLabel: "container1",
- }
- container2Info := map[string]string{
- source.NamespaceLabel: "kube-system",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod2",
- source.UIDLabel: "pod-uuid2",
- source.ContainerLabel: "container2",
- }
- const startingCPUSeconds float64 = 506000.0
- updateSet1 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC),
- Updates: []metric.Update{
- // container1 has both requests and usage
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 0.2,
- },
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: startingCPUSeconds,
- },
- // container2 only has usage
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container2Info),
- Value: startingCPUSeconds,
- },
- // add some additional metrics to test filtering
- {
- Name: metric.KubeNamespaceLabels,
- Labels: maps.Clone(pod1Info),
- Value: 0,
- },
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toMemoryResource(container1Info),
- Value: 2.5 * 1024.0 * 1024.0 * 1024.0,
- },
- },
- }
- updateSet2 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 30, 0, time.UTC),
- Updates: []metric.Update{
- // container1 has both requests and usage
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 0.2,
- },
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: startingCPUSeconds + 40.0,
- },
- // container2 only has usage
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container2Info),
- Value: startingCPUSeconds + 30.0,
- },
- // add some additional metrics to test filtering
- {
- Name: metric.KubeNamespaceLabels,
- Labels: maps.Clone(pod1Info),
- Value: 0,
- },
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toMemoryResource(container1Info),
- Value: 2.5 * 1024.0 * 1024.0 * 1024.0,
- },
- },
- }
- updateSet3 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 1, 0, 0, time.UTC),
- Updates: []metric.Update{
- // container1 has both requests and usage
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 0.2,
- },
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: startingCPUSeconds + 40.0 + 5.0,
- },
- // container2 only has usage
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container2Info),
- Value: startingCPUSeconds + 30.0 + 30.0,
- },
- // add some additional metrics to test filtering
- {
- Name: metric.KubeNamespaceLabels,
- Labels: maps.Clone(pod1Info),
- Value: 0,
- },
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toMemoryResource(container1Info),
- Value: 2.5 * 1024.0 * 1024.0 * 1024.0,
- },
- },
- }
- scrape := 0
- updater := NewFuncUpdater(func(us *metric.UpdateSet) {
- // first scrape:
- // - container1: alloc = request
- // - container2: no metric
- if scrape == 0 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 0.2)
- assertNoMetricExists(t, us, metric.ContainerCPUAllocation, "container2")
- }
- // second scrape
- // - container1: alloc = 40s/30s = 1.33
- // - container2: alloc = 30s/30s = 1.0
- if scrape == 1 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 1.33333333)
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container2", 1.0)
- }
- // third scrape
- // - container1: alloc = 5.0/30.0s = 0.13, so alloc = request again (0.2)
- // - container2: alloc = 30s/30s = 1.0
- if scrape == 2 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 0.2)
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container2", 1.0)
- }
- scrape += 1
- })
- metricSynth := NewMetricSynthesizers(updater, NewContainerCpuAllocationSynthesizer(), NewContainerMemoryAllocationSynthesizer())
- metricSynth.Update(updateSet1)
- metricSynth.Update(updateSet2)
- metricSynth.Update(updateSet3)
- }
- func TestMetricSynthesizerCPUAllocation_UsageOverflow(t *testing.T) {
- container1Info := map[string]string{
- source.NamespaceLabel: "namespace1",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod1",
- source.UIDLabel: "pod-uuid1",
- source.ContainerLabel: "container1",
- }
- // start a max uint64 nanoseconds -> seconds
- // since the source metrics use nanoseconds, that's where overflow would occur.
- var startingCPUNanoSeconds uint64 = math.MaxUint64
- const nanosIncrement uint64 = 40 * 1e9
- toSeconds := func(nanos uint64) float64 {
- return float64(nanos) * 1e-9
- }
- updateSet1 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC),
- Updates: []metric.Update{
- // First Update has requests AND 1 usage sample
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 0.2,
- },
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: toSeconds(startingCPUNanoSeconds),
- },
- },
- }
- updateSet2 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 30, 0, time.UTC),
- Updates: []metric.Update{
- // Second Update doesn't have request, and has the second usage sample
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: toSeconds(startingCPUNanoSeconds + nanosIncrement),
- },
- },
- }
- updateSet3 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 1, 0, 0, time.UTC),
- Updates: []metric.Update{
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: toSeconds(startingCPUNanoSeconds + nanosIncrement + nanosIncrement),
- },
- },
- }
- scrape := 0
- updater := NewFuncUpdater(func(us *metric.UpdateSet) {
- // first scrape:
- // - container1: alloc = request
- if scrape == 0 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 0.2)
- }
- // second scrape
- // - container1: alloc = overflow, reset to current sample
- if scrape == 1 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 0.0)
- }
- // third scrape
- // - container1: alloc = 40.0/30s = 1.33333
- if scrape == 2 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 1.33333333)
- }
- scrape += 1
- })
- metricSynth := NewMetricSynthesizers(updater, NewContainerCpuAllocationSynthesizer(), NewContainerMemoryAllocationSynthesizer())
- metricSynth.Update(updateSet1)
- metricSynth.Update(updateSet2)
- metricSynth.Update(updateSet3)
- }
- func TestMetricSynthesizerCPUAllocation_UsageCounterReset(t *testing.T) {
- const nanosIncrement uint64 = 40 * 1e9
- container1Info := map[string]string{
- source.NamespaceLabel: "namespace1",
- source.NodeLabel: "node1",
- source.InstanceLabel: "node1",
- source.PodLabel: "pod1",
- source.UIDLabel: "pod-uuid1",
- source.ContainerLabel: "container1",
- }
- // Starting CPU Total Seconds
- const startingCPUSeconds float64 = 506000.0
- updateSet1 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 0, 0, time.UTC),
- Updates: []metric.Update{
- // First Update has requests AND 1 usage sample
- {
- Name: metric.KubePodContainerResourceRequests,
- Labels: toCpuResource(container1Info),
- Value: 0.2,
- },
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: startingCPUSeconds,
- },
- },
- }
- updateSet2 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 0, 30, 0, time.UTC),
- Updates: []metric.Update{
- // Second Update doesn't have request, and has the second usage sample
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: startingCPUSeconds - 1000.0,
- },
- },
- }
- updateSet3 := &metric.UpdateSet{
- Timestamp: time.Date(2026, time.January, 1, 0, 1, 0, 0, time.UTC),
- Updates: []metric.Update{
- {
- Name: metric.ContainerCPUUsageSecondsTotal,
- Labels: maps.Clone(container1Info),
- Value: (startingCPUSeconds - 1000.0) + 40.0,
- },
- },
- }
- scrape := 0
- updater := NewFuncUpdater(func(us *metric.UpdateSet) {
- // first scrape:
- // - container1: alloc = request
- if scrape == 0 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 0.2)
- }
- // second scrape
- // - container1: alloc = (subtract 1000s - usage sample is less than last recorded), reset to 0.0
- if scrape == 1 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 0.0)
- }
- // third scrape
- // - container1: alloc = 40.0/30s = 1.33333
- if scrape == 2 {
- assertMetricValue(t, us, metric.ContainerCPUAllocation, "container1", 1.33333333)
- }
- scrape += 1
- })
- metricSynth := NewMetricSynthesizers(updater, NewContainerCpuAllocationSynthesizer(), NewContainerMemoryAllocationSynthesizer())
- metricSynth.Update(updateSet1)
- metricSynth.Update(updateSet2)
- metricSynth.Update(updateSet3)
- }
|