diff --git a/.testcoverage.yaml b/.testcoverage.yaml index 923d20a28b..2e26acc499 100644 --- a/.testcoverage.yaml +++ b/.testcoverage.yaml @@ -40,6 +40,8 @@ exclude: - ^test/.*$ - app.go # app.go and main.go should be tested by integration tests. - main.go + # ignore metadata generated files + - metadata/generated_.*\.go # ignore wrappers around gopsutil - internal/datasource/host - internal/watcher/process diff --git a/go.mod b/go.mod index 6008d07f5d..8c16eab1b2 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( go.opentelemetry.io/collector/receiver/receivertest v0.124.0 go.opentelemetry.io/collector/scraper v0.124.0 go.opentelemetry.io/collector/scraper/scraperhelper v0.124.0 + go.opentelemetry.io/collector/scraper/scrapertest v0.124.0 go.opentelemetry.io/otel v1.35.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 diff --git a/internal/collector/containermetricsreceiver/README.md b/internal/collector/containermetricsreceiver/README.md new file mode 100644 index 0000000000..ad09ab5822 --- /dev/null +++ b/internal/collector/containermetricsreceiver/README.md @@ -0,0 +1,18 @@ +# Container Metrics Receiver + +The Container Metrics receiver generates metrics about the container scraped from the cgroup files. + +## Configuration + +### Receiver Config + +The following settings are optional: +- `collection_interval` (default = `10s`): This receiver collects metrics on an interval. This value must be a string readable by Golang's [time.ParseDuration](https://pkg.go.dev/time#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. +- `initial_delay` (default = `1s`): defines how long this receiver waits before starting. + +Example: +```yaml +containermetrics: + collection_interval: # default = 1m + initial_delay: # default = 1s +``` diff --git a/internal/collector/containermetricsreceiver/doc.go b/internal/collector/containermetricsreceiver/doc.go new file mode 100644 index 0000000000..9c40ac9d84 --- /dev/null +++ b/internal/collector/containermetricsreceiver/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +//go:generate mdatagen metadata.yaml + +package containermetricsreceiver diff --git a/internal/collector/containermetricsreceiver/documentation.md b/internal/collector/containermetricsreceiver/documentation.md new file mode 100644 index 0000000000..90b182d765 --- /dev/null +++ b/internal/collector/containermetricsreceiver/documentation.md @@ -0,0 +1,33 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# containermetrics + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +### system.memory.usage + +Bytes of memory in use. + +| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | +| ---- | ----------- | ---------- | ----------------------- | --------- | +| By | Sum | Int | Cumulative | false | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| state | Breakdown of memory usage by type. | Str: ``buffered``, ``cached``, ``inactive``, ``free``, ``slab_reclaimable``, ``slab_unreclaimable``, ``used`` | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| resource.id | The resource id. | Any Str | false | diff --git a/internal/collector/containermetricsreceiver/factory.go b/internal/collector/containermetricsreceiver/factory.go new file mode 100644 index 0000000000..0b9a1ac04e --- /dev/null +++ b/internal/collector/containermetricsreceiver/factory.go @@ -0,0 +1,55 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package containermetricsreceiver + +import ( + "context" + "errors" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/consumer" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/scraper/scraperhelper" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/config" + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/metadata" +) + +// nolint: ireturn +func NewFactory() receiver.Factory { + return receiver.NewFactory( + metadata.Type, + config.CreateDefaultConfig, + receiver.WithMetrics( + createMetricsReceiver, + metadata.MetricsStability, + ), + ) +} + +// nolint: ireturn +func createMetricsReceiver( + _ context.Context, + params receiver.Settings, + rConf component.Config, + cons consumer.Metrics, +) (receiver.Metrics, error) { + cfg, ok := rConf.(*config.Config) + if !ok { + return nil, errors.New("cast to metrics receiver config failed") + } + + return scraperhelper.NewMetricsController( + &cfg.ControllerConfig, + params, + cons, + scraperhelper.AddFactoryWithConfig(cpuscraper.NewFactory(), cpuscraper.NewConfig(cfg)), + scraperhelper.AddFactoryWithConfig(memoryscraper.NewFactory(), memoryscraper.NewConfig(cfg)), + ) +} diff --git a/internal/collector/containermetricsreceiver/generated_component_test.go b/internal/collector/containermetricsreceiver/generated_component_test.go new file mode 100644 index 0000000000..ca6d506cad --- /dev/null +++ b/internal/collector/containermetricsreceiver/generated_component_test.go @@ -0,0 +1,71 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package containermetricsreceiver + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap/confmaptest" + "go.opentelemetry.io/collector/consumer/consumertest" + "go.opentelemetry.io/collector/receiver" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +var typ = component.MustNewType("containermetrics") + +func TestComponentFactoryType(t *testing.T) { + require.Equal(t, typ, NewFactory().Type()) +} + +func TestComponentConfigStruct(t *testing.T) { + require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig())) +} + +func TestComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + createFn func(ctx context.Context, set receiver.Settings, cfg component.Config) (component.Component, error) + name string + }{ + + { + name: "metrics", + createFn: func(ctx context.Context, set receiver.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateMetrics(ctx, set, cfg, consumertest.NewNop()) + }, + }, + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(&cfg)) + + for _, tt := range tests { + t.Run(tt.name+"-shutdown", func(t *testing.T) { + c, err := tt.createFn(context.Background(), receivertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + t.Run(tt.name+"-lifecycle", func(t *testing.T) { + firstRcvr, err := tt.createFn(context.Background(), receivertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + host := componenttest.NewNopHost() + require.NoError(t, err) + require.NoError(t, firstRcvr.Start(context.Background(), host)) + require.NoError(t, firstRcvr.Shutdown(context.Background())) + secondRcvr, err := tt.createFn(context.Background(), receivertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + require.NoError(t, secondRcvr.Start(context.Background(), host)) + require.NoError(t, secondRcvr.Shutdown(context.Background())) + }) + } +} diff --git a/internal/collector/containermetricsreceiver/generated_package_test.go b/internal/collector/containermetricsreceiver/generated_package_test.go new file mode 100644 index 0000000000..48100706cd --- /dev/null +++ b/internal/collector/containermetricsreceiver/generated_package_test.go @@ -0,0 +1,12 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package containermetricsreceiver + +import ( + "go.uber.org/goleak" + "testing" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/internal/collector/containermetricsreceiver/internal/config/config.go b/internal/collector/containermetricsreceiver/internal/config/config.go new file mode 100644 index 0000000000..2268b78cd5 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/config/config.go @@ -0,0 +1,23 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package config + +import ( + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/scraper/scraperhelper" +) + +type Config struct { + scraperhelper.ControllerConfig `mapstructure:",squash"` +} + +// nolint: ireturn +func CreateDefaultConfig() component.Config { + cfg := scraperhelper.NewDefaultControllerConfig() + return &Config{ + ControllerConfig: cfg, + } +} diff --git a/internal/collector/containermetricsreceiver/internal/metadata/generated_status.go b/internal/collector/containermetricsreceiver/internal/metadata/generated_status.go new file mode 100644 index 0000000000..cbcce0113e --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/metadata/generated_status.go @@ -0,0 +1,16 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("containermetrics") + ScopeName = "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver" +) + +const ( + MetricsStability = component.StabilityLevelBeta +) diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/config.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/config.go new file mode 100644 index 0000000000..a9b9707c6a --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/config.go @@ -0,0 +1,29 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cpuscraper + +import ( + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/config" + "go.opentelemetry.io/collector/scraper/scraperhelper" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata" +) + +type Config struct { + MetricsBuilderConfig metadata.MetricsBuilderConfig `mapstructure:",squash"` + scraperhelper.ControllerConfig `mapstructure:",squash"` +} + +func NewConfig(cfg *config.Config) *Config { + return &Config{ + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: cfg.CollectionInterval, + InitialDelay: cfg.InitialDelay, + Timeout: cfg.Timeout, + }, + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/doc.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/doc.go new file mode 100644 index 0000000000..47111f6bdd --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +//go:generate mdatagen metadata.yaml + +package cpuscraper diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/documentation.md b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/documentation.md new file mode 100644 index 0000000000..924cc70696 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/documentation.md @@ -0,0 +1,41 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# cpu + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +### system.cpu.logical.count + +Number of available logical CPUs. + +| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | +| ---- | ----------- | ---------- | ----------------------- | --------- | +| {cpu} | Sum | Int | Cumulative | false | + +### system.cpu.utilization + +Difference in system.cpu.time since the last measurement per logical CPU, divided by the elapsed time (value in interval [0,1]). + +| Unit | Metric Type | Value Type | +| ---- | ----------- | ---------- | +| 1 | Gauge | Double | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| state | CPU usage type. | Str: ``idle``, ``interrupt``, ``nice``, ``softirq``, ``steal``, ``system``, ``user``, ``wait`` | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| resource.id | The resource id. | Any Str | false | diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/factory.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/factory.go new file mode 100644 index 0000000000..e16db61661 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/factory.go @@ -0,0 +1,54 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cpuscraper + +import ( + "context" + "errors" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/scraper" +) + +// NewFactory for CPU scraper. +// nolint: ireturn +func NewFactory() scraper.Factory { + return scraper.NewFactory( + metadata.Type, + createDefaultConfig, + scraper.WithMetrics(createMetricsScraper, metadata.MetricsStability), + ) +} + +// createDefaultConfig creates the default configuration for the Scraper. +// nolint: ireturn +func createDefaultConfig() component.Config { + return &Config{ + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + } +} + +// createMetricsScraper creates a scraper based on provided config. +// nolint: ireturn +func createMetricsScraper( + ctx context.Context, + settings scraper.Settings, + config component.Config, +) (scraper.Metrics, error) { + cfg, ok := config.(*Config) + if !ok { + return nil, errors.New("cast to metrics scraper config") + } + + s := NewScraper(ctx, settings, cfg) + + return scraper.NewMetrics( + s.Scrape, + scraper.WithStart(s.Start), + ) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/generated_component_test.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/generated_component_test.go new file mode 100644 index 0000000000..41cd8cf8bf --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/generated_component_test.go @@ -0,0 +1,70 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package cpuscraper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap/confmaptest" + "go.opentelemetry.io/collector/scraper" + "go.opentelemetry.io/collector/scraper/scrapertest" +) + +var typ = component.MustNewType("cpu") + +func TestComponentFactoryType(t *testing.T) { + require.Equal(t, typ, NewFactory().Type()) +} + +func TestComponentConfigStruct(t *testing.T) { + require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig())) +} + +func TestComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + createFn func(ctx context.Context, set scraper.Settings, cfg component.Config) (component.Component, error) + name string + }{ + + { + name: "metrics", + createFn: func(ctx context.Context, set scraper.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateMetrics(ctx, set, cfg) + }, + }, + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(&cfg)) + + for _, tt := range tests { + t.Run(tt.name+"-shutdown", func(t *testing.T) { + c, err := tt.createFn(context.Background(), scrapertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + t.Run(tt.name+"-lifecycle", func(t *testing.T) { + firstRcvr, err := tt.createFn(context.Background(), scrapertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + host := componenttest.NewNopHost() + require.NoError(t, err) + require.NoError(t, firstRcvr.Start(context.Background(), host)) + require.NoError(t, firstRcvr.Shutdown(context.Background())) + secondRcvr, err := tt.createFn(context.Background(), scrapertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + require.NoError(t, secondRcvr.Start(context.Background(), host)) + require.NoError(t, secondRcvr.Shutdown(context.Background())) + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/generated_package_test.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/generated_package_test.go new file mode 100644 index 0000000000..e628a022a3 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/generated_package_test.go @@ -0,0 +1,12 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package cpuscraper + +import ( + "go.uber.org/goleak" + "testing" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup/cpu.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup/cpu.go new file mode 100644 index 0000000000..90aa5cb70f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup/cpu.go @@ -0,0 +1,205 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cgroup + +import ( + "bytes" + "errors" + "os/exec" + "path" + "runtime" + "strconv" + "strings" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal" +) + +const ( + V1CpuacctStatFile = "cpuacct/cpuacct.stat" + V1UserKey = "user" + V1SystemKey = "system" + + V2CpuStat = "cpu.stat" + V2UserKey = "user_usec" + V2SystemKey = "system_usec" + + CPUStatsFileLineLength = 8 + nanoSecondsPerSecond = 1e9 +) + +var ( + CPUStatsPath = "/proc/stat" + GetNumberOfCores = runtime.NumCPU +) + +type ( + ContainerCPUTimes struct { + userUsage float64 + systemUsage float64 + hostSystemUsage float64 + } + + ContainerCPUStats struct { + NumberOfLogicalCPUs int + User float64 + System float64 + } + + CPUSource struct { + previous *ContainerCPUTimes + basePath string + isCgroupV2 bool + } +) + +func NewCPUSource(basePath string) *CPUSource { + return &CPUSource{ + basePath: basePath, + isCgroupV2: internal.IsCgroupV2(basePath), + previous: &ContainerCPUTimes{}, + } +} + +func (cs *CPUSource) Collect() (ContainerCPUStats, error) { + cpuStats, err := cs.collectCPUStats() + if err != nil { + return ContainerCPUStats{}, err + } + + return cpuStats, nil +} + +// nolint: mnd +func (cs *CPUSource) collectCPUStats() (ContainerCPUStats, error) { + clockTicks, err := getClockTicks() + if err != nil { + return ContainerCPUStats{}, err + } + + // cgroup v2 by default + filepath := path.Join(cs.basePath, V2CpuStat) + userKey := V2UserKey + sysKey := V2SystemKey + convertUsage := func(usage float64) float64 { + return usage * 1000 + } + + if !cs.isCgroupV2 { // cgroup v1 + filepath = path.Join(cs.basePath, V1CpuacctStatFile) + userKey = V1UserKey + sysKey = V1SystemKey + convertUsage = func(usage float64) float64 { + return usage * nanoSecondsPerSecond / float64(clockTicks) + } + } + + cpuTimes, err := cs.cpuUsageTimes( + filepath, + userKey, + sysKey, + ) + if err != nil { + return ContainerCPUStats{}, err + } + + cpuTimes.userUsage = convertUsage(cpuTimes.userUsage) + cpuTimes.systemUsage = convertUsage(cpuTimes.systemUsage) + hostSystemUsage, err := getSystemCPUUsage(clockTicks) + if err != nil { + return ContainerCPUStats{}, err + } + cpuTimes.hostSystemUsage = hostSystemUsage + + // calculate deltas + userDelta := cpuTimes.userUsage - cs.previous.userUsage + systemDelta := cpuTimes.systemUsage - cs.previous.systemUsage + hostSystemDelta := cpuTimes.hostSystemUsage - cs.previous.hostSystemUsage + + numCores := GetNumberOfCores() + userPercent := (userDelta / hostSystemDelta) * float64(numCores) + systemPercent := (systemDelta / hostSystemDelta) * float64(numCores) + + cpuStats := ContainerCPUStats{ + NumberOfLogicalCPUs: numCores, + User: userPercent, + System: systemPercent, + } + + cs.previous = cpuTimes + + return cpuStats, nil +} + +func (cs *CPUSource) cpuUsageTimes(filePath, userKey, systemKey string) (*ContainerCPUTimes, error) { + cpuTimes := &ContainerCPUTimes{} + lines, err := internal.ReadLines(filePath) + if err != nil { + return cpuTimes, err + } + + for _, line := range lines { + fields := strings.Fields(line) + switch fields[0] { + case userKey: + user, parseErr := strconv.ParseFloat(fields[1], 64) + if parseErr != nil { + return cpuTimes, parseErr + } + cpuTimes.userUsage = user + case systemKey: + system, parseErr := strconv.ParseFloat(fields[1], 64) + if parseErr != nil { + return cpuTimes, parseErr + } + cpuTimes.systemUsage = system + } + } + + return cpuTimes, nil +} + +// nolint: revive, gocritic +func getSystemCPUUsage(clockTicks int) (float64, error) { + lines, err := internal.ReadLines(CPUStatsPath) + if err != nil { + return 0, err + } + + for _, line := range lines { + parts := strings.Fields(line) + switch parts[0] { + case "cpu": + if len(parts) < CPUStatsFileLineLength { + return 0, errors.New("unable to process " + CPUStatsPath + ". Invalid number of fields for cpu line") + } + var totalClockTicks float64 + for _, i := range parts[1:CPUStatsFileLineLength] { + v, parseErr := strconv.ParseFloat(i, 64) + if parseErr != nil { + return 0, err + } + totalClockTicks += v + } + + return (totalClockTicks * nanoSecondsPerSecond) / float64(clockTicks), nil + } + } + + return 0, errors.New("unable to process " + CPUStatsPath + ". No cpu found") +} + +func getClockTicks() (int, error) { + cmd := exec.Command("getconf", "CLK_TCK") + out := new(bytes.Buffer) + cmd.Stdout = out + + err := cmd.Run() + if err != nil { + return 0, err + } + + return strconv.Atoi(strings.TrimSuffix(out.String(), "\n")) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup/cpu_test.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup/cpu_test.go new file mode 100644 index 0000000000..1c9327ac37 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup/cpu_test.go @@ -0,0 +1,85 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cgroup + +import ( + "os" + "path" + "runtime" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCollectCPUStats(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + localDirectory := path.Dir(filename) + + tests := []struct { + errorType error + name string + basePath string + cpuStat ContainerCPUStats + }{ + { + name: "Test 1: v1 good data", + basePath: localDirectory + "/../../../testdata/good_data/v1/", + cpuStat: ContainerCPUStats{ + NumberOfLogicalCPUs: 2, + User: 0.006712570862198262, + System: 0.0020429056808044366, + }, + errorType: nil, + }, + { + name: "Test 2: v1 bad data", + basePath: localDirectory + "/../../../testdata/bad_data/v1/", + cpuStat: ContainerCPUStats{}, + errorType: &strconv.NumError{}, + }, + { + name: "Test 3: v2 good data", + basePath: localDirectory + "/../../../testdata/good_data/v2/", + cpuStat: ContainerCPUStats{ + NumberOfLogicalCPUs: 2, + User: 0.04627063395919899, + System: 0.04250076104937527, + }, + errorType: nil, + }, + { + name: "Test 4: v2 bad data", + basePath: localDirectory + "/../../../testdata/bad_data/v2/", + cpuStat: ContainerCPUStats{}, + errorType: &strconv.NumError{}, + }, + { + name: "Test 5: no file", + basePath: localDirectory + "/unknown/", + cpuStat: ContainerCPUStats{}, + errorType: &os.PathError{}, + }, + } + + GetNumberOfCores = func() int { + return 2 + } + CPUStatsPath = localDirectory + "/../../../testdata/proc/stat" + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + cgroupCPUSource := NewCPUSource(test.basePath) + cpuStat, err := cgroupCPUSource.collectCPUStats() + + // Assert error + assert.IsType(tt, test.errorType, err) + + // Assert result + assert.Equal(tt, test.cpuStat, cpuStat) + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config.go new file mode 100644 index 0000000000..6a4051486b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config.go @@ -0,0 +1,96 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/filter" +) + +// MetricConfig provides common config for a particular metric. +type MetricConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(ms) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for cpu metrics. +type MetricsConfig struct { + SystemCPULogicalCount MetricConfig `mapstructure:"system.cpu.logical.count"` + SystemCPUUtilization MetricConfig `mapstructure:"system.cpu.utilization"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + SystemCPULogicalCount: MetricConfig{ + Enabled: true, + }, + SystemCPUUtilization: MetricConfig{ + Enabled: true, + }, + } +} + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` + // Experimental: MetricsInclude defines a list of filters for attribute values. + // If the list is not empty, only metrics with matching resource attribute values will be emitted. + MetricsInclude []filter.Config `mapstructure:"metrics_include"` + // Experimental: MetricsExclude defines a list of filters for attribute values. + // If the list is not empty, metrics with matching resource attribute values will not be emitted. + // MetricsInclude has higher priority than MetricsExclude. + MetricsExclude []filter.Config `mapstructure:"metrics_exclude"` + + enabledSetByUser bool +} + +func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(rac) + if err != nil { + return err + } + rac.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// ResourceAttributesConfig provides config for cpu resource attributes. +type ResourceAttributesConfig struct { + ResourceID ResourceAttributeConfig `mapstructure:"resource.id"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{ + Enabled: false, + }, + } +} + +// MetricsBuilderConfig is a configuration for cpu metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` + ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig{ + Metrics: DefaultMetricsConfig(), + ResourceAttributes: DefaultResourceAttributesConfig(), + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config_test.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config_test.go new file mode 100644 index 0000000000..03a411c81f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_config_test.go @@ -0,0 +1,109 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func TestMetricsBuilderConfig(t *testing.T) { + tests := []struct { + name string + want MetricsBuilderConfig + }{ + { + name: "default", + want: DefaultMetricsBuilderConfig(), + }, + { + name: "all_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemCPULogicalCount: MetricConfig{Enabled: true}, + SystemCPUUtilization: MetricConfig{Enabled: true}, + }, + ResourceAttributes: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemCPULogicalCount: MetricConfig{Enabled: false}, + SystemCPUUtilization: MetricConfig{Enabled: false}, + }, + ResourceAttributes: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: false}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadMetricsBuilderConfig(t, tt.name) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{}, ResourceAttributeConfig{})) + require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) + }) + } +} + +func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + cfg := DefaultMetricsBuilderConfig() + require.NoError(t, sub.Unmarshal(&cfg)) + return cfg +} + +func TestResourceAttributesConfig(t *testing.T) { + tests := []struct { + name string + want ResourceAttributesConfig + }{ + { + name: "default", + want: DefaultResourceAttributesConfig(), + }, + { + name: "all_set", + want: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: true}, + }, + }, + { + name: "none_set", + want: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: false}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt.name) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})) + require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) + }) + } +} + +func loadResourceAttributesConfig(t *testing.T, name string) ResourceAttributesConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + sub, err = sub.Sub("resource_attributes") + require.NoError(t, err) + cfg := DefaultResourceAttributesConfig() + require.NoError(t, sub.Unmarshal(&cfg)) + return cfg +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics.go new file mode 100644 index 0000000000..2cce54542c --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics.go @@ -0,0 +1,352 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/filter" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/scraper" +) + +// AttributeState specifies the value state attribute. +type AttributeState int + +const ( + _ AttributeState = iota + AttributeStateIdle + AttributeStateInterrupt + AttributeStateNice + AttributeStateSoftirq + AttributeStateSteal + AttributeStateSystem + AttributeStateUser + AttributeStateWait +) + +// String returns the string representation of the AttributeState. +func (av AttributeState) String() string { + switch av { + case AttributeStateIdle: + return "idle" + case AttributeStateInterrupt: + return "interrupt" + case AttributeStateNice: + return "nice" + case AttributeStateSoftirq: + return "softirq" + case AttributeStateSteal: + return "steal" + case AttributeStateSystem: + return "system" + case AttributeStateUser: + return "user" + case AttributeStateWait: + return "wait" + } + return "" +} + +// MapAttributeState is a helper map of string to AttributeState attribute value. +var MapAttributeState = map[string]AttributeState{ + "idle": AttributeStateIdle, + "interrupt": AttributeStateInterrupt, + "nice": AttributeStateNice, + "softirq": AttributeStateSoftirq, + "steal": AttributeStateSteal, + "system": AttributeStateSystem, + "user": AttributeStateUser, + "wait": AttributeStateWait, +} + +var MetricsInfo = metricsInfo{ + SystemCPULogicalCount: metricInfo{ + Name: "system.cpu.logical.count", + }, + SystemCPUUtilization: metricInfo{ + Name: "system.cpu.utilization", + }, +} + +type metricsInfo struct { + SystemCPULogicalCount metricInfo + SystemCPUUtilization metricInfo +} + +type metricInfo struct { + Name string +} + +type metricSystemCPULogicalCount struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills system.cpu.logical.count metric with initial data. +func (m *metricSystemCPULogicalCount) init() { + m.data.SetName("system.cpu.logical.count") + m.data.SetDescription("Number of available logical CPUs.") + m.data.SetUnit("{cpu}") + m.data.SetEmptySum() + m.data.Sum().SetIsMonotonic(false) + m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) +} + +func (m *metricSystemCPULogicalCount) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64) { + if !m.config.Enabled { + return + } + dp := m.data.Sum().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricSystemCPULogicalCount) updateCapacity() { + if m.data.Sum().DataPoints().Len() > m.capacity { + m.capacity = m.data.Sum().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricSystemCPULogicalCount) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricSystemCPULogicalCount(cfg MetricConfig) metricSystemCPULogicalCount { + m := metricSystemCPULogicalCount{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +type metricSystemCPUUtilization struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills system.cpu.utilization metric with initial data. +func (m *metricSystemCPUUtilization) init() { + m.data.SetName("system.cpu.utilization") + m.data.SetDescription("Difference in system.cpu.time since the last measurement per logical CPU, divided by the elapsed time (value in interval [0,1]).") + m.data.SetUnit("1") + m.data.SetEmptyGauge() + m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricSystemCPUUtilization) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val float64, stateAttributeValue string) { + if !m.config.Enabled { + return + } + dp := m.data.Gauge().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetDoubleValue(val) + dp.Attributes().PutStr("state", stateAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricSystemCPUUtilization) updateCapacity() { + if m.data.Gauge().DataPoints().Len() > m.capacity { + m.capacity = m.data.Gauge().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricSystemCPUUtilization) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Gauge().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricSystemCPUUtilization(cfg MetricConfig) metricSystemCPUUtilization { + m := metricSystemCPUUtilization{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +// MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations +// required to produce metric representation defined in metadata and user config. +type MetricsBuilder struct { + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + resourceAttributeIncludeFilter map[string]filter.Filter + resourceAttributeExcludeFilter map[string]filter.Filter + metricSystemCPULogicalCount metricSystemCPULogicalCount + metricSystemCPUUtilization metricSystemCPUUtilization +} + +// MetricBuilderOption applies changes to default metrics builder. +type MetricBuilderOption interface { + apply(*MetricsBuilder) +} + +type metricBuilderOptionFunc func(mb *MetricsBuilder) + +func (mbof metricBuilderOptionFunc) apply(mb *MetricsBuilder) { + mbof(mb) +} + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) MetricBuilderOption { + return metricBuilderOptionFunc(func(mb *MetricsBuilder) { + mb.startTime = startTime + }) +} +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings scraper.Settings, options ...MetricBuilderOption) *MetricsBuilder { + mb := &MetricsBuilder{ + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + metricSystemCPULogicalCount: newMetricSystemCPULogicalCount(mbc.Metrics.SystemCPULogicalCount), + metricSystemCPUUtilization: newMetricSystemCPUUtilization(mbc.Metrics.SystemCPUUtilization), + resourceAttributeIncludeFilter: make(map[string]filter.Filter), + resourceAttributeExcludeFilter: make(map[string]filter.Filter), + } + if mbc.ResourceAttributes.ResourceID.MetricsInclude != nil { + mb.resourceAttributeIncludeFilter["resource.id"] = filter.CreateFilter(mbc.ResourceAttributes.ResourceID.MetricsInclude) + } + if mbc.ResourceAttributes.ResourceID.MetricsExclude != nil { + mb.resourceAttributeExcludeFilter["resource.id"] = filter.CreateFilter(mbc.ResourceAttributes.ResourceID.MetricsExclude) + } + + for _, op := range options { + op.apply(mb) + } + return mb +} + +// NewResourceBuilder returns a new resource builder that should be used to build a resource associated with for the emitted metrics. +func (mb *MetricsBuilder) NewResourceBuilder() *ResourceBuilder { + return NewResourceBuilder(mb.config.ResourceAttributes) +} + +// updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. +func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { + if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { + mb.metricsCapacity = rm.ScopeMetrics().At(0).Metrics().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption interface { + apply(pmetric.ResourceMetrics) +} + +type resourceMetricsOptionFunc func(pmetric.ResourceMetrics) + +func (rmof resourceMetricsOptionFunc) apply(rm pmetric.ResourceMetrics) { + rmof(rm) +} + +// WithResource sets the provided resource on the emitted ResourceMetrics. +// It's recommended to use ResourceBuilder to create the resource. +func WithResource(res pcommon.Resource) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + res.CopyTo(rm.Resource()) + }) +} + +// WithStartTimeOverride overrides start time for all the resource metrics data points. +// This option should be only used if different start time has to be set on metrics coming from different resources. +func WithStartTimeOverride(start pcommon.Timestamp) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + var dps pmetric.NumberDataPointSlice + metrics := rm.ScopeMetrics().At(0).Metrics() + for i := 0; i < metrics.Len(); i++ { + switch metrics.At(i).Type() { + case pmetric.MetricTypeGauge: + dps = metrics.At(i).Gauge().DataPoints() + case pmetric.MetricTypeSum: + dps = metrics.At(i).Sum().DataPoints() + } + for j := 0; j < dps.Len(); j++ { + dps.At(j).SetStartTimestamp(start) + } + } + }) +} + +// EmitForResource saves all the generated metrics under a new resource and updates the internal state to be ready for +// recording another set of data points as part of another resource. This function can be helpful when one scraper +// needs to emit metrics from several resources. Otherwise calling this function is not required, +// just `Emit` function can be called instead. +// Resource attributes should be provided as ResourceMetricsOption arguments. +func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName(ScopeName) + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricSystemCPULogicalCount.emit(ils.Metrics()) + mb.metricSystemCPUUtilization.emit(ils.Metrics()) + + for _, op := range options { + op.apply(rm) + } + for attr, filter := range mb.resourceAttributeIncludeFilter { + if val, ok := rm.Resource().Attributes().Get(attr); ok && !filter.Matches(val.AsString()) { + return + } + } + for attr, filter := range mb.resourceAttributeExcludeFilter { + if val, ok := rm.Resource().Attributes().Get(attr); ok && filter.Matches(val.AsString()) { + return + } + } + + if ils.Metrics().Len() > 0 { + mb.updateCapacity(rm) + rm.MoveTo(mb.metricsBuffer.ResourceMetrics().AppendEmpty()) + } +} + +// Emit returns all the metrics accumulated by the metrics builder and updates the internal state to be ready for +// recording another set of metrics. This function will be responsible for applying all the transformations required to +// produce metric representation defined in metadata and user config, e.g. delta or cumulative. +func (mb *MetricsBuilder) Emit(options ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(options...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +// RecordSystemCPULogicalCountDataPoint adds a data point to system.cpu.logical.count metric. +func (mb *MetricsBuilder) RecordSystemCPULogicalCountDataPoint(ts pcommon.Timestamp, val int64) { + mb.metricSystemCPULogicalCount.recordDataPoint(mb.startTime, ts, val) +} + +// RecordSystemCPUUtilizationDataPoint adds a data point to system.cpu.utilization metric. +func (mb *MetricsBuilder) RecordSystemCPUUtilizationDataPoint(ts pcommon.Timestamp, val float64, stateAttributeValue AttributeState) { + mb.metricSystemCPUUtilization.recordDataPoint(mb.startTime, ts, val, stateAttributeValue.String()) +} + +// Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, +// and metrics builder should update its startTime and reset it's internal state accordingly. +func (mb *MetricsBuilder) Reset(options ...MetricBuilderOption) { + mb.startTime = pcommon.NewTimestampFromTime(time.Now()) + for _, op := range options { + op.apply(mb) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics_test.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics_test.go new file mode 100644 index 0000000000..512c0fcb1e --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_metrics_test.go @@ -0,0 +1,136 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/scraper/scrapertest" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +type testDataSet int + +const ( + testDataSetDefault testDataSet = iota + testDataSetAll + testDataSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + metricsSet testDataSet + resAttrsSet testDataSet + expectEmpty bool + }{ + { + name: "default", + }, + { + name: "all_set", + metricsSet: testDataSetAll, + resAttrsSet: testDataSetAll, + }, + { + name: "none_set", + metricsSet: testDataSetNone, + resAttrsSet: testDataSetNone, + expectEmpty: true, + }, + { + name: "filter_set_include", + resAttrsSet: testDataSetAll, + }, + { + name: "filter_set_exclude", + resAttrsSet: testDataSetAll, + expectEmpty: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start := pcommon.Timestamp(1_000_000_000) + ts := pcommon.Timestamp(1_000_001_000) + observedZapCore, observedLogs := observer.New(zap.WarnLevel) + settings := scrapertest.NewNopSettings(scrapertest.NopType) + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, tt.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordSystemCPULogicalCountDataPoint(ts, 1) + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordSystemCPUUtilizationDataPoint(ts, 1, AttributeStateIdle) + + rb := mb.NewResourceBuilder() + rb.SetResourceID("resource.id-val") + res := rb.Emit() + metrics := mb.Emit(WithResource(res)) + + if tt.expectEmpty { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + assert.Equal(t, res, rm.Resource()) + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if tt.metricsSet == testDataSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if tt.metricsSet == testDataSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + case "system.cpu.logical.count": + assert.False(t, validatedMetrics["system.cpu.logical.count"], "Found a duplicate in the metrics slice: system.cpu.logical.count") + validatedMetrics["system.cpu.logical.count"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, "Number of available logical CPUs.", ms.At(i).Description()) + assert.Equal(t, "{cpu}", ms.At(i).Unit()) + assert.False(t, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + case "system.cpu.utilization": + assert.False(t, validatedMetrics["system.cpu.utilization"], "Found a duplicate in the metrics slice: system.cpu.utilization") + validatedMetrics["system.cpu.utilization"] = true + assert.Equal(t, pmetric.MetricTypeGauge, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Gauge().DataPoints().Len()) + assert.Equal(t, "Difference in system.cpu.time since the last measurement per logical CPU, divided by the elapsed time (value in interval [0,1]).", ms.At(i).Description()) + assert.Equal(t, "1", ms.At(i).Unit()) + dp := ms.At(i).Gauge().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeDouble, dp.ValueType()) + assert.InDelta(t, float64(1), dp.DoubleValue(), 0.01) + attrVal, ok := dp.Attributes().Get("state") + assert.True(t, ok) + assert.Equal(t, "idle", attrVal.Str()) + } + } + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_resource.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_resource.go new file mode 100644 index 0000000000..dfe3b94f0f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_resource.go @@ -0,0 +1,36 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" +) + +// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml. +// The ResourceBuilder is not thread-safe and must not to be used in multiple goroutines. +type ResourceBuilder struct { + config ResourceAttributesConfig + res pcommon.Resource +} + +// NewResourceBuilder creates a new ResourceBuilder. This method should be called on the start of the application. +func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { + return &ResourceBuilder{ + config: rac, + res: pcommon.NewResource(), + } +} + +// SetResourceID sets provided value as "resource.id" attribute. +func (rb *ResourceBuilder) SetResourceID(val string) { + if rb.config.ResourceID.Enabled { + rb.res.Attributes().PutStr("resource.id", val) + } +} + +// Emit returns the built resource and resets the internal builder state. +func (rb *ResourceBuilder) Emit() pcommon.Resource { + r := rb.res + rb.res = pcommon.NewResource() + return r +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_resource_test.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_resource_test.go new file mode 100644 index 0000000000..78a3714845 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_resource_test.go @@ -0,0 +1,40 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResourceBuilder(t *testing.T) { + for _, tt := range []string{"default", "all_set", "none_set"} { + t.Run(tt, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt) + rb := NewResourceBuilder(cfg) + rb.SetResourceID("resource.id-val") + + res := rb.Emit() + assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource + + switch tt { + case "default": + assert.Equal(t, 0, res.Attributes().Len()) + case "all_set": + assert.Equal(t, 1, res.Attributes().Len()) + case "none_set": + assert.Equal(t, 0, res.Attributes().Len()) + return + default: + assert.Failf(t, "unexpected test case: %s", tt) + } + + val, ok := res.Attributes().Get("resource.id") + assert.Equal(t, tt == "all_set", ok) + if ok { + assert.Equal(t, "resource.id-val", val.Str()) + } + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_status.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_status.go new file mode 100644 index 0000000000..53f2a370d0 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/generated_status.go @@ -0,0 +1,16 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("cpu") + ScopeName = "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper" +) + +const ( + MetricsStability = component.StabilityLevelBeta +) diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/testdata/config.yaml b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/testdata/config.yaml new file mode 100644 index 0000000000..fe8a58a20b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata/testdata/config.yaml @@ -0,0 +1,31 @@ +default: +all_set: + metrics: + system.cpu.logical.count: + enabled: true + system.cpu.utilization: + enabled: true + resource_attributes: + resource.id: + enabled: true +none_set: + metrics: + system.cpu.logical.count: + enabled: false + system.cpu.utilization: + enabled: false + resource_attributes: + resource.id: + enabled: false +filter_set_include: + resource_attributes: + resource.id: + enabled: true + metrics_include: + - regexp: ".*" +filter_set_exclude: + resource_attributes: + resource.id: + enabled: true + metrics_exclude: + - strict: "resource.id-val" diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/metadata.yaml b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/metadata.yaml new file mode 100644 index 0000000000..19ab645491 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/metadata.yaml @@ -0,0 +1,38 @@ +type: cpu + +status: + class: scraper + stability: + beta: [metrics] + distributions: [contrib] + codeowners: + active: [ aphralG, dhurley, craigell, sean-breen, CVanF5 ] + +resource_attributes: + resource.id: + description: The resource id. + type: string + +attributes: + state: + description: CPU usage type. + type: string + enum: [idle, interrupt, nice, softirq, steal, system, user, wait] + +metrics: + system.cpu.utilization: + enabled: true + description: Difference in system.cpu.time since the last measurement per logical CPU, divided by the elapsed time (value in interval [0,1]). + unit: "1" + gauge: + value_type: double + attributes: [ state ] + system.cpu.logical.count: + enabled: true + description: Number of available logical CPUs. + unit: "{cpu}" + sum: + value_type: int + monotonic: false + aggregation_temporality: cumulative + diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/scraper.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/scraper.go new file mode 100644 index 0000000000..0e92bf463f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/scraper.go @@ -0,0 +1,76 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cpuscraper + +import ( + "context" + "time" + + "go.opentelemetry.io/collector/scraper" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.uber.org/zap" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pmetric" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/metadata" +) + +var basePath = "/sys/fs/cgroup/" + +type CPUScraper struct { + cfg *Config + mb *metadata.MetricsBuilder + rb *metadata.ResourceBuilder + cpuSource *cgroup.CPUSource + settings scraper.Settings +} + +func NewScraper( + _ context.Context, + settings scraper.Settings, + cfg *Config, +) *CPUScraper { + logger := settings.Logger + logger.Info("Creating container CPU scraper") + + mb := metadata.NewMetricsBuilder(cfg.MetricsBuilderConfig, settings) + rb := mb.NewResourceBuilder() + + return &CPUScraper{ + settings: settings, + cfg: cfg, + mb: mb, + rb: rb, + } +} + +func (s *CPUScraper) Start(_ context.Context, _ component.Host) error { + s.settings.Logger.Info("Starting container CPU scraper") + s.cpuSource = cgroup.NewCPUSource(basePath) + + return nil +} + +func (s *CPUScraper) Scrape(context.Context) (pmetric.Metrics, error) { + s.settings.Logger.Debug("Scraping container CPU metrics") + + now := pcommon.NewTimestampFromTime(time.Now()) + + stats, err := s.cpuSource.Collect() + if err != nil { + return pmetric.NewMetrics(), err + } + + s.settings.Logger.Debug("Collected container CPU metrics", zap.Any("cpu", stats)) + + s.mb.RecordSystemCPUUtilizationDataPoint(now, stats.User, metadata.AttributeStateUser) + s.mb.RecordSystemCPUUtilizationDataPoint(now, stats.System, metadata.AttributeStateSystem) + + return s.mb.Emit(metadata.WithResource(s.rb.Emit())), nil +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/scraper_test.go b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/scraper_test.go new file mode 100644 index 0000000000..c0b5d0a289 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/scraper_test.go @@ -0,0 +1,42 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cpuscraper + +import ( + "context" + "path" + "runtime" + "testing" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/config" + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/cpuscraper/internal/cgroup" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/scraper/scrapertest" +) + +func TestScrape(t *testing.T) { + ctx := context.Background() + + _, filename, _, _ := runtime.Caller(0) + localDirectory := path.Dir(filename) + basePath = localDirectory + "/../testdata/good_data/v1/" + cgroup.CPUStatsPath = localDirectory + "/../testdata/proc/stat" + + scraper := NewScraper( + ctx, + scrapertest.NewNopSettings(component.Type{}), + NewConfig(&config.Config{}), + ) + + err := scraper.Start(ctx, componenttest.NewNopHost()) + require.NoError(t, err) + + metrics, err := scraper.Scrape(ctx) + require.NotNil(t, metrics) + require.NoError(t, err) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/config.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/config.go new file mode 100644 index 0000000000..708010fe91 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/config.go @@ -0,0 +1,29 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package memoryscraper + +import ( + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/config" + "go.opentelemetry.io/collector/scraper/scraperhelper" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata" +) + +type Config struct { + MetricsBuilderConfig metadata.MetricsBuilderConfig `mapstructure:",squash"` + scraperhelper.ControllerConfig `mapstructure:",squash"` +} + +func NewConfig(cfg *config.Config) *Config { + return &Config{ + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + ControllerConfig: scraperhelper.ControllerConfig{ + CollectionInterval: cfg.CollectionInterval, + InitialDelay: cfg.InitialDelay, + Timeout: cfg.Timeout, + }, + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/doc.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/doc.go new file mode 100644 index 0000000000..245ff66e84 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/doc.go @@ -0,0 +1,8 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +//go:generate mdatagen metadata.yaml + +package memoryscraper diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/documentation.md b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/documentation.md new file mode 100644 index 0000000000..8fabb2f734 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/documentation.md @@ -0,0 +1,33 @@ +[comment]: <> (Code generated by mdatagen. DO NOT EDIT.) + +# memory + +## Default Metrics + +The following metrics are emitted by default. Each of them can be disabled by applying the following configuration: + +```yaml +metrics: + : + enabled: false +``` + +### system.memory.usage + +Bytes of memory in use. + +| Unit | Metric Type | Value Type | Aggregation Temporality | Monotonic | +| ---- | ----------- | ---------- | ----------------------- | --------- | +| By | Sum | Int | Cumulative | false | + +#### Attributes + +| Name | Description | Values | +| ---- | ----------- | ------ | +| state | Breakdown of memory usage by type. | Str: ``buffered``, ``cached``, ``inactive``, ``free``, ``slab_reclaimable``, ``slab_unreclaimable``, ``used`` | + +## Resource Attributes + +| Name | Description | Values | Enabled | +| ---- | ----------- | ------ | ------- | +| resource.id | The resource id. | Any Str | false | diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/factory.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/factory.go new file mode 100644 index 0000000000..2901eacdfe --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/factory.go @@ -0,0 +1,54 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package memoryscraper + +import ( + "context" + "errors" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/scraper" +) + +// NewFactory for CPU scraper. +// nolint: ireturn +func NewFactory() scraper.Factory { + return scraper.NewFactory( + metadata.Type, + createDefaultConfig, + scraper.WithMetrics(createMetricsScraper, metadata.MetricsStability), + ) +} + +// createDefaultConfig creates the default configuration for the Scraper. +// nolint: ireturn +func createDefaultConfig() component.Config { + return &Config{ + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + } +} + +// createMetricsScraper creates a scraper based on provided config. +// nolint: ireturn +func createMetricsScraper( + ctx context.Context, + settings scraper.Settings, + config component.Config, +) (scraper.Metrics, error) { + cfg, ok := config.(*Config) + if !ok { + return nil, errors.New("cast to metrics scraper config") + } + + s := NewScraper(ctx, settings, cfg) + + return scraper.NewMetrics( + s.Scrape, + scraper.WithStart(s.Start), + ) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/generated_component_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/generated_component_test.go new file mode 100644 index 0000000000..323b92a0b9 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/generated_component_test.go @@ -0,0 +1,70 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package memoryscraper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap/confmaptest" + "go.opentelemetry.io/collector/scraper" + "go.opentelemetry.io/collector/scraper/scrapertest" +) + +var typ = component.MustNewType("memory") + +func TestComponentFactoryType(t *testing.T) { + require.Equal(t, typ, NewFactory().Type()) +} + +func TestComponentConfigStruct(t *testing.T) { + require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig())) +} + +func TestComponentLifecycle(t *testing.T) { + factory := NewFactory() + + tests := []struct { + createFn func(ctx context.Context, set scraper.Settings, cfg component.Config) (component.Component, error) + name string + }{ + + { + name: "metrics", + createFn: func(ctx context.Context, set scraper.Settings, cfg component.Config) (component.Component, error) { + return factory.CreateMetrics(ctx, set, cfg) + }, + }, + } + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(&cfg)) + + for _, tt := range tests { + t.Run(tt.name+"-shutdown", func(t *testing.T) { + c, err := tt.createFn(context.Background(), scrapertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + err = c.Shutdown(context.Background()) + require.NoError(t, err) + }) + t.Run(tt.name+"-lifecycle", func(t *testing.T) { + firstRcvr, err := tt.createFn(context.Background(), scrapertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + host := componenttest.NewNopHost() + require.NoError(t, err) + require.NoError(t, firstRcvr.Start(context.Background(), host)) + require.NoError(t, firstRcvr.Shutdown(context.Background())) + secondRcvr, err := tt.createFn(context.Background(), scrapertest.NewNopSettings(typ), cfg) + require.NoError(t, err) + require.NoError(t, secondRcvr.Start(context.Background(), host)) + require.NoError(t, secondRcvr.Shutdown(context.Background())) + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/generated_package_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/generated_package_test.go new file mode 100644 index 0000000000..d659373975 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/generated_package_test.go @@ -0,0 +1,12 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package memoryscraper + +import ( + "go.uber.org/goleak" + "testing" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory.go new file mode 100644 index 0000000000..f9be934a1c --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory.go @@ -0,0 +1,184 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cgroup + +import ( + "context" + "fmt" + "log/slog" + "math" + "os" + "path" + "strconv" + "strings" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal" + + "github.com/shirou/gopsutil/v4/mem" +) + +const ( + V1MemStatFile = "memory/memory.stat" + V1MemTotalFile = "memory/memory.limit_in_bytes" + V1MemUsageFile = "memory/memory.usage_in_bytes" + V1CachedKey = "cache" + V1SharedKey = "total_shmem" + + V2MemStatFile = "memory.stat" + V2MemTotalFile = "memory.max" + V2MemUsageFile = "memory.current" + V2CachedKey = "file" + V2SharedKey = "shmem" + V2DefaultMaxValue = "max" +) + +var pageSize = int64(os.Getpagesize()) + +type MemorySource struct { + basePath string + isCgroupV2 bool +} + +type MemoryStat struct { + cached uint64 + shared uint64 +} + +var getHostMemoryStats = mem.VirtualMemoryWithContext + +func NewMemorySource(basePath string) *MemorySource { + return &MemorySource{ + basePath: basePath, + isCgroupV2: internal.IsCgroupV2(basePath), + } +} + +func (ms *MemorySource) Collect() { + _, err := ms.VirtualMemoryStatWithContext(context.Background()) + if err != nil { + slog.Error(err.Error()) + return + } +} + +// nolint: unparam +func (ms *MemorySource) VirtualMemoryStatWithContext(ctx context.Context) (*mem.VirtualMemoryStat, error) { + var cgroupStat mem.VirtualMemoryStat + var memoryStat MemoryStat + + // cgroup v2 by default + memTotalFile := V2MemTotalFile + memUsageFile := V2MemUsageFile + memStatFile := V2MemStatFile + memCachedKey := V2CachedKey + memSharedKey := V2SharedKey + + if !ms.isCgroupV2 { + memTotalFile = V1MemTotalFile + memUsageFile = V1MemUsageFile + memStatFile = V1MemStatFile + memCachedKey = V1CachedKey + memSharedKey = V1SharedKey + } + + memoryLimitInBytes, err := MemoryLimitInBytes(ctx, path.Join(ms.basePath, memTotalFile)) + if err != nil { + return &mem.VirtualMemoryStat{}, err + } + + memoryUsageInBytes, err := internal.ReadIntegerValueCgroupFile(path.Join(ms.basePath, memUsageFile)) + if err != nil { + return &mem.VirtualMemoryStat{}, err + } + + memoryStat, err = GetMemoryStat( + path.Join(ms.basePath, memStatFile), + memCachedKey, + memSharedKey, + ) + if err != nil { + return &mem.VirtualMemoryStat{}, err + } + + var usedMemoryPercent float64 + + usedMemory := memoryUsageInBytes - memoryStat.cached + + if memoryLimitInBytes > 0 { + usedMemoryPercent = float64(100 * usedMemory / memoryLimitInBytes) + } + + cgroupStat.Total = memoryLimitInBytes + cgroupStat.Available = memoryLimitInBytes - usedMemory + cgroupStat.Used = usedMemory + cgroupStat.Cached = memoryStat.cached + cgroupStat.Shared = memoryStat.shared + cgroupStat.UsedPercent = usedMemoryPercent + cgroupStat.Free = memoryLimitInBytes - usedMemory + + return &cgroupStat, nil +} + +func (ms *MemorySource) VirtualMemoryStat() (*mem.VirtualMemoryStat, error) { + ctx := context.Background() + defer ctx.Done() + + return ms.VirtualMemoryStatWithContext(ctx) +} + +func MemoryLimitInBytes(ctx context.Context, filePath string) (uint64, error) { + memTotalString, err := internal.ReadSingleValueCgroupFile(filePath) + if err != nil { + return 0, err + } + if memTotalString == V2DefaultMaxValue || memTotalString == V1DefaultMaxValue() { + hostMemoryStats, hostErr := getHostMemoryStats(ctx) + if hostErr != nil { + return 0, hostErr + } + + return hostMemoryStats.Total, nil + } + + return strconv.ParseUint(memTotalString, 10, 64) +} + +// nolint: revive, mnd +func GetMemoryStat(statFile, cachedKey, sharedKey string) (MemoryStat, error) { + memoryStat := MemoryStat{} + lines, err := internal.ReadLines(statFile) + if err != nil { + return memoryStat, err + } + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) != 2 { + return memoryStat, fmt.Errorf("%+v required 2 fields", fields) + } + + switch fields[0] { + case cachedKey: + cached, parseErr := strconv.ParseUint(fields[1], 10, 64) + if parseErr != nil { + return memoryStat, parseErr + } + memoryStat.cached = cached + case sharedKey: + shared, parseErr := strconv.ParseUint(fields[1], 10, 64) + if parseErr != nil { + return memoryStat, parseErr + } + memoryStat.shared = shared + } + } + + return memoryStat, nil +} + +func V1DefaultMaxValue() string { + maxInt := int64(math.MaxInt64) + return strconv.FormatInt((maxInt/pageSize)*pageSize, 10) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory_test.go new file mode 100644 index 0000000000..fd4ec3a918 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup/memory_test.go @@ -0,0 +1,124 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package cgroup + +import ( + "context" + "os" + "path" + "runtime" + "strconv" + "testing" + + "github.com/shirou/gopsutil/v4/mem" + "github.com/stretchr/testify/assert" +) + +func TestVirtualMemoryStat(t *testing.T) { + _, filename, _, _ := runtime.Caller(0) + localDirectory := path.Dir(filename) + + tests := []struct { + errorType error + name string + basePath string + virtualMemoryStat mem.VirtualMemoryStat + }{ + { + name: "Test 1: v1 good data", + basePath: localDirectory + "/../../../testdata/good_data/v1/", + virtualMemoryStat: mem.VirtualMemoryStat{ + Total: 536870912, + Free: 420200448, + Available: 420200448, + Used: 116670464, + Cached: 275480576, + Shared: 53805056, + UsedPercent: 21, + }, + errorType: nil, + }, + { + name: "Test 2: v1 good data no limits", + basePath: localDirectory + "/../../../testdata/good_data_no_limits/v1/", + virtualMemoryStat: mem.VirtualMemoryStat{ + Total: 636870912, + Free: 520200448, + Available: 520200448, + Used: 116670464, + Cached: 275480576, + Shared: 53805056, + UsedPercent: 18, + }, + errorType: nil, + }, + { + name: "Test 3: v1 bad data", + basePath: localDirectory + "/../../../testdata/bad_data/v1/", + virtualMemoryStat: mem.VirtualMemoryStat{}, + errorType: &strconv.NumError{}, + }, + { + name: "Test 4: v2 good data", + basePath: localDirectory + "/../../../testdata/good_data/v2/", + virtualMemoryStat: mem.VirtualMemoryStat{ + Total: 536870912, + Free: 420200448, + Available: 420200448, + Used: 116670464, + Cached: 275480576, + Shared: 53805056, + UsedPercent: 21, + }, + errorType: nil, + }, + { + name: "Test 5: v2 good data no limits", + basePath: localDirectory + "/../../../testdata/good_data_no_limits/v2/", + virtualMemoryStat: mem.VirtualMemoryStat{ + Total: 636870912, + Free: 520200448, + Available: 520200448, + Used: 116670464, + Cached: 275480576, + Shared: 53805056, + UsedPercent: 18, + }, + errorType: nil, + }, + { + name: "Test 6: v2 bad data", + basePath: localDirectory + "/../../../testdata/bad_data/v2/", + virtualMemoryStat: mem.VirtualMemoryStat{}, + errorType: &strconv.NumError{}, + }, + { + name: "Test 7: no file", + basePath: localDirectory + "/unknown/", + virtualMemoryStat: mem.VirtualMemoryStat{}, + errorType: &os.PathError{}, + }, + } + + getHostMemoryStats = func(ctx context.Context) (*mem.VirtualMemoryStat, error) { + return &mem.VirtualMemoryStat{Total: 636870912}, nil + } + + pageSize = 65536 + + for _, test := range tests { + t.Run(test.name, func(tt *testing.T) { + cgroupCPUSource := NewMemorySource(test.basePath) + virtualMemoryStat, err := cgroupCPUSource.VirtualMemoryStat() + + // Assert error + assert.IsType(tt, test.errorType, err) + + // Assert result + assert.Equal(tt, test.virtualMemoryStat, *virtualMemoryStat) + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_config.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_config.go new file mode 100644 index 0000000000..cdbb79da9f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_config.go @@ -0,0 +1,92 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/filter" +) + +// MetricConfig provides common config for a particular metric. +type MetricConfig struct { + Enabled bool `mapstructure:"enabled"` + + enabledSetByUser bool +} + +func (ms *MetricConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(ms) + if err != nil { + return err + } + ms.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// MetricsConfig provides config for memory metrics. +type MetricsConfig struct { + SystemMemoryUsage MetricConfig `mapstructure:"system.memory.usage"` +} + +func DefaultMetricsConfig() MetricsConfig { + return MetricsConfig{ + SystemMemoryUsage: MetricConfig{ + Enabled: true, + }, + } +} + +// ResourceAttributeConfig provides common config for a particular resource attribute. +type ResourceAttributeConfig struct { + Enabled bool `mapstructure:"enabled"` + // Experimental: MetricsInclude defines a list of filters for attribute values. + // If the list is not empty, only metrics with matching resource attribute values will be emitted. + MetricsInclude []filter.Config `mapstructure:"metrics_include"` + // Experimental: MetricsExclude defines a list of filters for attribute values. + // If the list is not empty, metrics with matching resource attribute values will not be emitted. + // MetricsInclude has higher priority than MetricsExclude. + MetricsExclude []filter.Config `mapstructure:"metrics_exclude"` + + enabledSetByUser bool +} + +func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { + if parser == nil { + return nil + } + err := parser.Unmarshal(rac) + if err != nil { + return err + } + rac.enabledSetByUser = parser.IsSet("enabled") + return nil +} + +// ResourceAttributesConfig provides config for memory resource attributes. +type ResourceAttributesConfig struct { + ResourceID ResourceAttributeConfig `mapstructure:"resource.id"` +} + +func DefaultResourceAttributesConfig() ResourceAttributesConfig { + return ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{ + Enabled: false, + }, + } +} + +// MetricsBuilderConfig is a configuration for memory metrics builder. +type MetricsBuilderConfig struct { + Metrics MetricsConfig `mapstructure:"metrics"` + ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` +} + +func DefaultMetricsBuilderConfig() MetricsBuilderConfig { + return MetricsBuilderConfig{ + Metrics: DefaultMetricsConfig(), + ResourceAttributes: DefaultResourceAttributesConfig(), + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_config_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_config_test.go new file mode 100644 index 0000000000..49ddecbb1d --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_config_test.go @@ -0,0 +1,107 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +func TestMetricsBuilderConfig(t *testing.T) { + tests := []struct { + name string + want MetricsBuilderConfig + }{ + { + name: "default", + want: DefaultMetricsBuilderConfig(), + }, + { + name: "all_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemMemoryUsage: MetricConfig{Enabled: true}, + }, + ResourceAttributes: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: true}, + }, + }, + }, + { + name: "none_set", + want: MetricsBuilderConfig{ + Metrics: MetricsConfig{ + SystemMemoryUsage: MetricConfig{Enabled: false}, + }, + ResourceAttributes: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: false}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadMetricsBuilderConfig(t, tt.name) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{}, ResourceAttributeConfig{})) + require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) + }) + } +} + +func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + cfg := DefaultMetricsBuilderConfig() + require.NoError(t, sub.Unmarshal(&cfg)) + return cfg +} + +func TestResourceAttributesConfig(t *testing.T) { + tests := []struct { + name string + want ResourceAttributesConfig + }{ + { + name: "default", + want: DefaultResourceAttributesConfig(), + }, + { + name: "all_set", + want: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: true}, + }, + }, + { + name: "none_set", + want: ResourceAttributesConfig{ + ResourceID: ResourceAttributeConfig{Enabled: false}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt.name) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})) + require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) + }) + } +} + +func loadResourceAttributesConfig(t *testing.T, name string) ResourceAttributesConfig { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + sub, err := cm.Sub(name) + require.NoError(t, err) + sub, err = sub.Sub("resource_attributes") + require.NoError(t, err) + cfg := DefaultResourceAttributesConfig() + require.NoError(t, sub.Unmarshal(&cfg)) + return cfg +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_metrics.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_metrics.go new file mode 100644 index 0000000000..a68b267c64 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_metrics.go @@ -0,0 +1,287 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/filter" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/scraper" +) + +// AttributeState specifies the value state attribute. +type AttributeState int + +const ( + _ AttributeState = iota + AttributeStateBuffered + AttributeStateCached + AttributeStateInactive + AttributeStateFree + AttributeStateSlabReclaimable + AttributeStateSlabUnreclaimable + AttributeStateUsed +) + +// String returns the string representation of the AttributeState. +func (av AttributeState) String() string { + switch av { + case AttributeStateBuffered: + return "buffered" + case AttributeStateCached: + return "cached" + case AttributeStateInactive: + return "inactive" + case AttributeStateFree: + return "free" + case AttributeStateSlabReclaimable: + return "slab_reclaimable" + case AttributeStateSlabUnreclaimable: + return "slab_unreclaimable" + case AttributeStateUsed: + return "used" + } + return "" +} + +// MapAttributeState is a helper map of string to AttributeState attribute value. +var MapAttributeState = map[string]AttributeState{ + "buffered": AttributeStateBuffered, + "cached": AttributeStateCached, + "inactive": AttributeStateInactive, + "free": AttributeStateFree, + "slab_reclaimable": AttributeStateSlabReclaimable, + "slab_unreclaimable": AttributeStateSlabUnreclaimable, + "used": AttributeStateUsed, +} + +var MetricsInfo = metricsInfo{ + SystemMemoryUsage: metricInfo{ + Name: "system.memory.usage", + }, +} + +type metricsInfo struct { + SystemMemoryUsage metricInfo +} + +type metricInfo struct { + Name string +} + +type metricSystemMemoryUsage struct { + data pmetric.Metric // data buffer for generated metric. + config MetricConfig // metric config provided by user. + capacity int // max observed number of data points added to the metric. +} + +// init fills system.memory.usage metric with initial data. +func (m *metricSystemMemoryUsage) init() { + m.data.SetName("system.memory.usage") + m.data.SetDescription("Bytes of memory in use.") + m.data.SetUnit("By") + m.data.SetEmptySum() + m.data.Sum().SetIsMonotonic(false) + m.data.Sum().SetAggregationTemporality(pmetric.AggregationTemporalityCumulative) + m.data.Sum().DataPoints().EnsureCapacity(m.capacity) +} + +func (m *metricSystemMemoryUsage) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, stateAttributeValue string) { + if !m.config.Enabled { + return + } + dp := m.data.Sum().DataPoints().AppendEmpty() + dp.SetStartTimestamp(start) + dp.SetTimestamp(ts) + dp.SetIntValue(val) + dp.Attributes().PutStr("state", stateAttributeValue) +} + +// updateCapacity saves max length of data point slices that will be used for the slice capacity. +func (m *metricSystemMemoryUsage) updateCapacity() { + if m.data.Sum().DataPoints().Len() > m.capacity { + m.capacity = m.data.Sum().DataPoints().Len() + } +} + +// emit appends recorded metric data to a metrics slice and prepares it for recording another set of data points. +func (m *metricSystemMemoryUsage) emit(metrics pmetric.MetricSlice) { + if m.config.Enabled && m.data.Sum().DataPoints().Len() > 0 { + m.updateCapacity() + m.data.MoveTo(metrics.AppendEmpty()) + m.init() + } +} + +func newMetricSystemMemoryUsage(cfg MetricConfig) metricSystemMemoryUsage { + m := metricSystemMemoryUsage{config: cfg} + if cfg.Enabled { + m.data = pmetric.NewMetric() + m.init() + } + return m +} + +// MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations +// required to produce metric representation defined in metadata and user config. +type MetricsBuilder struct { + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + resourceAttributeIncludeFilter map[string]filter.Filter + resourceAttributeExcludeFilter map[string]filter.Filter + metricSystemMemoryUsage metricSystemMemoryUsage +} + +// MetricBuilderOption applies changes to default metrics builder. +type MetricBuilderOption interface { + apply(*MetricsBuilder) +} + +type metricBuilderOptionFunc func(mb *MetricsBuilder) + +func (mbof metricBuilderOptionFunc) apply(mb *MetricsBuilder) { + mbof(mb) +} + +// WithStartTime sets startTime on the metrics builder. +func WithStartTime(startTime pcommon.Timestamp) MetricBuilderOption { + return metricBuilderOptionFunc(func(mb *MetricsBuilder) { + mb.startTime = startTime + }) +} +func NewMetricsBuilder(mbc MetricsBuilderConfig, settings scraper.Settings, options ...MetricBuilderOption) *MetricsBuilder { + mb := &MetricsBuilder{ + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + metricSystemMemoryUsage: newMetricSystemMemoryUsage(mbc.Metrics.SystemMemoryUsage), + resourceAttributeIncludeFilter: make(map[string]filter.Filter), + resourceAttributeExcludeFilter: make(map[string]filter.Filter), + } + if mbc.ResourceAttributes.ResourceID.MetricsInclude != nil { + mb.resourceAttributeIncludeFilter["resource.id"] = filter.CreateFilter(mbc.ResourceAttributes.ResourceID.MetricsInclude) + } + if mbc.ResourceAttributes.ResourceID.MetricsExclude != nil { + mb.resourceAttributeExcludeFilter["resource.id"] = filter.CreateFilter(mbc.ResourceAttributes.ResourceID.MetricsExclude) + } + + for _, op := range options { + op.apply(mb) + } + return mb +} + +// NewResourceBuilder returns a new resource builder that should be used to build a resource associated with for the emitted metrics. +func (mb *MetricsBuilder) NewResourceBuilder() *ResourceBuilder { + return NewResourceBuilder(mb.config.ResourceAttributes) +} + +// updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. +func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { + if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { + mb.metricsCapacity = rm.ScopeMetrics().At(0).Metrics().Len() + } +} + +// ResourceMetricsOption applies changes to provided resource metrics. +type ResourceMetricsOption interface { + apply(pmetric.ResourceMetrics) +} + +type resourceMetricsOptionFunc func(pmetric.ResourceMetrics) + +func (rmof resourceMetricsOptionFunc) apply(rm pmetric.ResourceMetrics) { + rmof(rm) +} + +// WithResource sets the provided resource on the emitted ResourceMetrics. +// It's recommended to use ResourceBuilder to create the resource. +func WithResource(res pcommon.Resource) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + res.CopyTo(rm.Resource()) + }) +} + +// WithStartTimeOverride overrides start time for all the resource metrics data points. +// This option should be only used if different start time has to be set on metrics coming from different resources. +func WithStartTimeOverride(start pcommon.Timestamp) ResourceMetricsOption { + return resourceMetricsOptionFunc(func(rm pmetric.ResourceMetrics) { + var dps pmetric.NumberDataPointSlice + metrics := rm.ScopeMetrics().At(0).Metrics() + for i := 0; i < metrics.Len(); i++ { + switch metrics.At(i).Type() { + case pmetric.MetricTypeGauge: + dps = metrics.At(i).Gauge().DataPoints() + case pmetric.MetricTypeSum: + dps = metrics.At(i).Sum().DataPoints() + } + for j := 0; j < dps.Len(); j++ { + dps.At(j).SetStartTimestamp(start) + } + } + }) +} + +// EmitForResource saves all the generated metrics under a new resource and updates the internal state to be ready for +// recording another set of data points as part of another resource. This function can be helpful when one scraper +// needs to emit metrics from several resources. Otherwise calling this function is not required, +// just `Emit` function can be called instead. +// Resource attributes should be provided as ResourceMetricsOption arguments. +func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { + rm := pmetric.NewResourceMetrics() + ils := rm.ScopeMetrics().AppendEmpty() + ils.Scope().SetName(ScopeName) + ils.Scope().SetVersion(mb.buildInfo.Version) + ils.Metrics().EnsureCapacity(mb.metricsCapacity) + mb.metricSystemMemoryUsage.emit(ils.Metrics()) + + for _, op := range options { + op.apply(rm) + } + for attr, filter := range mb.resourceAttributeIncludeFilter { + if val, ok := rm.Resource().Attributes().Get(attr); ok && !filter.Matches(val.AsString()) { + return + } + } + for attr, filter := range mb.resourceAttributeExcludeFilter { + if val, ok := rm.Resource().Attributes().Get(attr); ok && filter.Matches(val.AsString()) { + return + } + } + + if ils.Metrics().Len() > 0 { + mb.updateCapacity(rm) + rm.MoveTo(mb.metricsBuffer.ResourceMetrics().AppendEmpty()) + } +} + +// Emit returns all the metrics accumulated by the metrics builder and updates the internal state to be ready for +// recording another set of metrics. This function will be responsible for applying all the transformations required to +// produce metric representation defined in metadata and user config, e.g. delta or cumulative. +func (mb *MetricsBuilder) Emit(options ...ResourceMetricsOption) pmetric.Metrics { + mb.EmitForResource(options...) + metrics := mb.metricsBuffer + mb.metricsBuffer = pmetric.NewMetrics() + return metrics +} + +// RecordSystemMemoryUsageDataPoint adds a data point to system.memory.usage metric. +func (mb *MetricsBuilder) RecordSystemMemoryUsageDataPoint(ts pcommon.Timestamp, val int64, stateAttributeValue AttributeState) { + mb.metricSystemMemoryUsage.recordDataPoint(mb.startTime, ts, val, stateAttributeValue.String()) +} + +// Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, +// and metrics builder should update its startTime and reset it's internal state accordingly. +func (mb *MetricsBuilder) Reset(options ...MetricBuilderOption) { + mb.startTime = pcommon.NewTimestampFromTime(time.Now()) + for _, op := range options { + op.apply(mb) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_metrics_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_metrics_test.go new file mode 100644 index 0000000000..bd031bb2d3 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_metrics_test.go @@ -0,0 +1,120 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.opentelemetry.io/collector/scraper/scrapertest" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +type testDataSet int + +const ( + testDataSetDefault testDataSet = iota + testDataSetAll + testDataSetNone +) + +func TestMetricsBuilder(t *testing.T) { + tests := []struct { + name string + metricsSet testDataSet + resAttrsSet testDataSet + expectEmpty bool + }{ + { + name: "default", + }, + { + name: "all_set", + metricsSet: testDataSetAll, + resAttrsSet: testDataSetAll, + }, + { + name: "none_set", + metricsSet: testDataSetNone, + resAttrsSet: testDataSetNone, + expectEmpty: true, + }, + { + name: "filter_set_include", + resAttrsSet: testDataSetAll, + }, + { + name: "filter_set_exclude", + resAttrsSet: testDataSetAll, + expectEmpty: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start := pcommon.Timestamp(1_000_000_000) + ts := pcommon.Timestamp(1_000_001_000) + observedZapCore, observedLogs := observer.New(zap.WarnLevel) + settings := scrapertest.NewNopSettings(scrapertest.NopType) + settings.Logger = zap.New(observedZapCore) + mb := NewMetricsBuilder(loadMetricsBuilderConfig(t, tt.name), settings, WithStartTime(start)) + + expectedWarnings := 0 + + assert.Equal(t, expectedWarnings, observedLogs.Len()) + + defaultMetricsCount := 0 + allMetricsCount := 0 + + defaultMetricsCount++ + allMetricsCount++ + mb.RecordSystemMemoryUsageDataPoint(ts, 1, AttributeStateBuffered) + + rb := mb.NewResourceBuilder() + rb.SetResourceID("resource.id-val") + res := rb.Emit() + metrics := mb.Emit(WithResource(res)) + + if tt.expectEmpty { + assert.Equal(t, 0, metrics.ResourceMetrics().Len()) + return + } + + assert.Equal(t, 1, metrics.ResourceMetrics().Len()) + rm := metrics.ResourceMetrics().At(0) + assert.Equal(t, res, rm.Resource()) + assert.Equal(t, 1, rm.ScopeMetrics().Len()) + ms := rm.ScopeMetrics().At(0).Metrics() + if tt.metricsSet == testDataSetDefault { + assert.Equal(t, defaultMetricsCount, ms.Len()) + } + if tt.metricsSet == testDataSetAll { + assert.Equal(t, allMetricsCount, ms.Len()) + } + validatedMetrics := make(map[string]bool) + for i := 0; i < ms.Len(); i++ { + switch ms.At(i).Name() { + case "system.memory.usage": + assert.False(t, validatedMetrics["system.memory.usage"], "Found a duplicate in the metrics slice: system.memory.usage") + validatedMetrics["system.memory.usage"] = true + assert.Equal(t, pmetric.MetricTypeSum, ms.At(i).Type()) + assert.Equal(t, 1, ms.At(i).Sum().DataPoints().Len()) + assert.Equal(t, "Bytes of memory in use.", ms.At(i).Description()) + assert.Equal(t, "By", ms.At(i).Unit()) + assert.False(t, ms.At(i).Sum().IsMonotonic()) + assert.Equal(t, pmetric.AggregationTemporalityCumulative, ms.At(i).Sum().AggregationTemporality()) + dp := ms.At(i).Sum().DataPoints().At(0) + assert.Equal(t, start, dp.StartTimestamp()) + assert.Equal(t, ts, dp.Timestamp()) + assert.Equal(t, pmetric.NumberDataPointValueTypeInt, dp.ValueType()) + assert.Equal(t, int64(1), dp.IntValue()) + attrVal, ok := dp.Attributes().Get("state") + assert.True(t, ok) + assert.Equal(t, "buffered", attrVal.Str()) + } + } + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_resource.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_resource.go new file mode 100644 index 0000000000..dfe3b94f0f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_resource.go @@ -0,0 +1,36 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" +) + +// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml. +// The ResourceBuilder is not thread-safe and must not to be used in multiple goroutines. +type ResourceBuilder struct { + config ResourceAttributesConfig + res pcommon.Resource +} + +// NewResourceBuilder creates a new ResourceBuilder. This method should be called on the start of the application. +func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { + return &ResourceBuilder{ + config: rac, + res: pcommon.NewResource(), + } +} + +// SetResourceID sets provided value as "resource.id" attribute. +func (rb *ResourceBuilder) SetResourceID(val string) { + if rb.config.ResourceID.Enabled { + rb.res.Attributes().PutStr("resource.id", val) + } +} + +// Emit returns the built resource and resets the internal builder state. +func (rb *ResourceBuilder) Emit() pcommon.Resource { + r := rb.res + rb.res = pcommon.NewResource() + return r +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_resource_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_resource_test.go new file mode 100644 index 0000000000..78a3714845 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_resource_test.go @@ -0,0 +1,40 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResourceBuilder(t *testing.T) { + for _, tt := range []string{"default", "all_set", "none_set"} { + t.Run(tt, func(t *testing.T) { + cfg := loadResourceAttributesConfig(t, tt) + rb := NewResourceBuilder(cfg) + rb.SetResourceID("resource.id-val") + + res := rb.Emit() + assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource + + switch tt { + case "default": + assert.Equal(t, 0, res.Attributes().Len()) + case "all_set": + assert.Equal(t, 1, res.Attributes().Len()) + case "none_set": + assert.Equal(t, 0, res.Attributes().Len()) + return + default: + assert.Failf(t, "unexpected test case: %s", tt) + } + + val, ok := res.Attributes().Get("resource.id") + assert.Equal(t, tt == "all_set", ok) + if ok { + assert.Equal(t, "resource.id-val", val.Str()) + } + }) + } +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_status.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_status.go new file mode 100644 index 0000000000..55ef64c64d --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/generated_status.go @@ -0,0 +1,16 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("memory") + ScopeName = "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper" +) + +const ( + MetricsStability = component.StabilityLevelBeta +) diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/testdata/config.yaml b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/testdata/config.yaml new file mode 100644 index 0000000000..cfc03f430f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata/testdata/config.yaml @@ -0,0 +1,27 @@ +default: +all_set: + metrics: + system.memory.usage: + enabled: true + resource_attributes: + resource.id: + enabled: true +none_set: + metrics: + system.memory.usage: + enabled: false + resource_attributes: + resource.id: + enabled: false +filter_set_include: + resource_attributes: + resource.id: + enabled: true + metrics_include: + - regexp: ".*" +filter_set_exclude: + resource_attributes: + resource.id: + enabled: true + metrics_exclude: + - strict: "resource.id-val" diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/metadata.yaml b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/metadata.yaml new file mode 100644 index 0000000000..bdac21dfdb --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/metadata.yaml @@ -0,0 +1,32 @@ +type: memory + +status: + class: scraper + stability: + beta: [metrics] + distributions: [contrib] + codeowners: + active: [ aphralG, dhurley, craigell, sean-breen, CVanF5 ] + +resource_attributes: + resource.id: + description: The resource id. + type: string + +attributes: + state: + description: Breakdown of memory usage by type. + type: string + enum: [ buffered, cached, inactive, free, slab_reclaimable, slab_unreclaimable, used ] + +metrics: + system.memory.usage: + enabled: true + description: Bytes of memory in use. + unit: By + sum: + value_type: int + aggregation_temporality: cumulative + monotonic: false + attributes: [state] + diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/scraper.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/scraper.go new file mode 100644 index 0000000000..cb68332a77 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/scraper.go @@ -0,0 +1,75 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package memoryscraper + +import ( + "context" + "time" + + "go.opentelemetry.io/collector/scraper" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/cgroup" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.uber.org/zap" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/internal/metadata" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +var basePath = "/sys/fs/cgroup/" + +type CPUScraper struct { + cfg *Config + mb *metadata.MetricsBuilder + rb *metadata.ResourceBuilder + memorySource *cgroup.MemorySource + settings scraper.Settings +} + +func NewScraper( + _ context.Context, + settings scraper.Settings, + cfg *Config, +) *CPUScraper { + logger := settings.Logger + logger.Info("Creating container memory scraper") + + mb := metadata.NewMetricsBuilder(cfg.MetricsBuilderConfig, settings) + rb := mb.NewResourceBuilder() + + return &CPUScraper{ + settings: settings, + cfg: cfg, + mb: mb, + rb: rb, + } +} + +func (s *CPUScraper) Start(_ context.Context, _ component.Host) error { + s.settings.Logger.Info("Starting container memory scraper") + s.memorySource = cgroup.NewMemorySource(basePath) + + return nil +} + +func (s *CPUScraper) Scrape(ctx context.Context) (pmetric.Metrics, error) { + s.settings.Logger.Debug("Scraping container memory metrics") + + now := pcommon.NewTimestampFromTime(time.Now()) + + stats, err := s.memorySource.VirtualMemoryStatWithContext(ctx) + if err != nil { + return pmetric.NewMetrics(), err + } + + s.settings.Logger.Debug("Collected container memory metrics", zap.Any("metrics", stats)) + + s.mb.RecordSystemMemoryUsageDataPoint(now, int64(stats.Used), metadata.AttributeStateUsed) + s.mb.RecordSystemMemoryUsageDataPoint(now, int64(stats.Free), metadata.AttributeStateFree) + + return s.mb.Emit(metadata.WithResource(s.rb.Emit())), nil +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/scraper_test.go b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/scraper_test.go new file mode 100644 index 0000000000..f52385932f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/memoryscraper/scraper_test.go @@ -0,0 +1,40 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package memoryscraper + +import ( + "context" + "path" + "runtime" + "testing" + + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver/internal/config" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/scraper/scrapertest" +) + +func TestScrape(t *testing.T) { + ctx := context.Background() + + _, filename, _, _ := runtime.Caller(0) + localDirectory := path.Dir(filename) + basePath = localDirectory + "/../testdata/good_data/v1/" + + scraper := NewScraper( + ctx, + scrapertest.NewNopSettings(component.Type{}), + NewConfig(&config.Config{}), + ) + + err := scraper.Start(ctx, componenttest.NewNopHost()) + require.NoError(t, err) + + metrics, err := scraper.Scrape(ctx) + require.NotNil(t, metrics) + require.NoError(t, err) +} diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.cfs_period_us b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.cfs_period_us new file mode 100644 index 0000000000..3546645658 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.cfs_period_us @@ -0,0 +1 @@ +unknown diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.cfs_quota_us b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.cfs_quota_us new file mode 100644 index 0000000000..3546645658 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.cfs_quota_us @@ -0,0 +1 @@ +unknown diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.shares b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.shares new file mode 100644 index 0000000000..3546645658 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.shares @@ -0,0 +1 @@ +unknown diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.stat new file mode 100644 index 0000000000..d4b7a99fc7 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpu/cpu.stat @@ -0,0 +1,3 @@ +unknown 500 +nr_throttled unknown +throttled_time 300 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpuacct/cpuacct.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpuacct/cpuacct.stat new file mode 100644 index 0000000000..f04f5de799 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpuacct/cpuacct.stat @@ -0,0 +1,2 @@ +unknown 6557 +system bad_value \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpuset/cpuset.cpus b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpuset/cpuset.cpus new file mode 100644 index 0000000000..82caefd92b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/cpuset/cpuset.cpus @@ -0,0 +1 @@ +unknwon diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.limit_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.limit_in_bytes new file mode 100644 index 0000000000..87edf799f4 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.limit_in_bytes @@ -0,0 +1 @@ +unknown \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.memsw.limit_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.memsw.limit_in_bytes new file mode 100644 index 0000000000..87edf799f4 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.memsw.limit_in_bytes @@ -0,0 +1 @@ +unknown \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.memsw.usage_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.memsw.usage_in_bytes new file mode 100644 index 0000000000..87edf799f4 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.memsw.usage_in_bytes @@ -0,0 +1 @@ +unknown \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.oom_control b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.oom_control new file mode 100644 index 0000000000..bf83fd708f --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.oom_control @@ -0,0 +1,2 @@ +unknown 1 +oom_kill unknown diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.stat new file mode 100644 index 0000000000..5c17f8e26e --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.stat @@ -0,0 +1,4 @@ +unknown 80830464 +unknown 1486848 +rss_huge 0 +shmem 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.usage_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.usage_in_bytes new file mode 100644 index 0000000000..87edf799f4 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v1/memory/memory.usage_in_bytes @@ -0,0 +1 @@ +unknown \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cgroup.controllers b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cgroup.controllers new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.max new file mode 100644 index 0000000000..121059c1fc --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.max @@ -0,0 +1 @@ +unknown 100000 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.stat new file mode 100644 index 0000000000..59d9ca49d1 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.stat @@ -0,0 +1,6 @@ +user_usec bad_value +system_usec bad_value +uknown 80000000 +nr_periods 0 +nr_throttled 0 +throttled_usec 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.weight b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.weight new file mode 100644 index 0000000000..3546645658 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpu.weight @@ -0,0 +1 @@ +unknown diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpuset.cpus b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpuset.cpus new file mode 100644 index 0000000000..3546645658 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/cpuset.cpus @@ -0,0 +1 @@ +unknown diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.current b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.current new file mode 100644 index 0000000000..7d327cd6dd --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.current @@ -0,0 +1 @@ +bad_value \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.events b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.events new file mode 100644 index 0000000000..d9c4b9ff72 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.events @@ -0,0 +1,2 @@ +oom unknown +unknown 3 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.max new file mode 100644 index 0000000000..7d327cd6dd --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.max @@ -0,0 +1 @@ +bad_value \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.stat new file mode 100644 index 0000000000..add15f3567 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.stat @@ -0,0 +1,28 @@ +anon bad_value +uknown 4324325532 +kernel_stack 5325325 +slab 2432523 +sock 12288 +shmem 3111333424 +file_mapped 2144421 +file_dirty 0 +file_writeback 0 +inactive_anon 0 +active_anon 0 +inactive_file 0 +active_file 0 +unevictable 0 +slab_reclaimable 0 +slab_unreclaimable 0 +pgfault 0 +pgmajfault 0 +workingset_refault 0 +workingset_activate 0 +workingset_nodereclaim 0 +pgrefill 0 +pgscan 0 +pgsteal 0 +pgactivate 0 +pgdeactivate 0 +pglazyfree 0 +pglazyfreed 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.swap.current b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.swap.current new file mode 100644 index 0000000000..7d327cd6dd --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.swap.current @@ -0,0 +1 @@ +bad_value \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.swap.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.swap.max new file mode 100644 index 0000000000..7d327cd6dd --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/bad_data/v2/memory.swap.max @@ -0,0 +1 @@ +bad_value \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.cfs_period_us b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.cfs_period_us new file mode 100644 index 0000000000..1b79f38e25 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.cfs_period_us @@ -0,0 +1 @@ +500 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.cfs_quota_us b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.cfs_quota_us new file mode 100644 index 0000000000..83b33d238d --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.cfs_quota_us @@ -0,0 +1 @@ +1000 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.shares b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.shares new file mode 100644 index 0000000000..d7b1c440c0 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.shares @@ -0,0 +1 @@ +1024 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.stat new file mode 100644 index 0000000000..8348a513f4 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpu/cpu.stat @@ -0,0 +1,3 @@ +nr_periods 500 +nr_throttled 200 +throttled_time 300 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpuacct/cpuacct.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpuacct/cpuacct.stat new file mode 100644 index 0000000000..b551341518 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpuacct/cpuacct.stat @@ -0,0 +1,2 @@ +user 5760 +system 1753 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpuset/cpuset.cpus b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpuset/cpuset.cpus new file mode 100644 index 0000000000..625215a969 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/cpuset/cpuset.cpus @@ -0,0 +1 @@ +0,1,3 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.limit_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.limit_in_bytes new file mode 100644 index 0000000000..5a62826005 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.limit_in_bytes @@ -0,0 +1 @@ +536870912 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.memsw.limit_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.memsw.limit_in_bytes new file mode 100644 index 0000000000..ac513d575a --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.memsw.limit_in_bytes @@ -0,0 +1 @@ +736870912 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.memsw.usage_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.memsw.usage_in_bytes new file mode 100644 index 0000000000..9d68dc6c0b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.memsw.usage_in_bytes @@ -0,0 +1 @@ +405020672 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.oom_control b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.oom_control new file mode 100644 index 0000000000..f2a817572c --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.oom_control @@ -0,0 +1,2 @@ +under_oom 1 +oom_kill 5 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.stat new file mode 100644 index 0000000000..5cde0a4b00 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.stat @@ -0,0 +1,36 @@ +cache 275480576 +rss 92704768 +rss_huge 0 +shmem 53805056 +mapped_file 69070848 +dirty 135168 +writeback 0 +swap 0 +pgpgin 2103354 +pgpgout 2013441 +pgfault 2526612 +pgmajfault 33 +inactive_anon 17199104 +active_anon 129626112 +inactive_file 106287104 +active_file 115240960 +unevictable 0 +hierarchical_memory_limit 536870912 +hierarchical_memsw_limit 1073741824 +total_cache 275480576 +total_rss 92704768 +total_rss_huge 0 +total_shmem 53805056 +total_mapped_file 69070848 +total_dirty 135168 +total_writeback 0 +total_swap 0 +total_pgpgin 2103354 +total_pgpgout 2013441 +total_pgfault 2526612 +total_pgmajfault 33 +total_inactive_anon 17199104 +total_active_anon 129626112 +total_inactive_file 106287104 +total_active_file 115240960 +total_unevictable 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.usage_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.usage_in_bytes new file mode 100644 index 0000000000..31e5d7a68d --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v1/memory/memory.usage_in_bytes @@ -0,0 +1 @@ +392151040 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cgroup.controllers b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cgroup.controllers new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.max new file mode 100644 index 0000000000..833a8f2d3b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.max @@ -0,0 +1 @@ +150000 100000 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.stat new file mode 100644 index 0000000000..1cf5fcd4ee --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.stat @@ -0,0 +1,6 @@ +usage_usec 761739796 +user_usec 397044377 +system_usec 364695418 +nr_periods 500 +nr_throttled 100 +throttled_usec 200 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.weight b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.weight new file mode 100644 index 0000000000..85322d0b54 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpu.weight @@ -0,0 +1 @@ +79 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpuset.cpus b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpuset.cpus new file mode 100644 index 0000000000..40c7bb2f1a --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/cpuset.cpus @@ -0,0 +1 @@ +0-3 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.current b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.current new file mode 100644 index 0000000000..31e5d7a68d --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.current @@ -0,0 +1 @@ +392151040 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.events b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.events new file mode 100644 index 0000000000..f23d7b9462 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.events @@ -0,0 +1,2 @@ +oom 1 +oom_kill 3 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.max new file mode 100644 index 0000000000..5a62826005 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.max @@ -0,0 +1 @@ +536870912 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.stat new file mode 100644 index 0000000000..4b4c61b919 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.stat @@ -0,0 +1,28 @@ +anon 92704768 +file 275480576 +kernel_stack 5325325 +slab 2432523 +sock 12288 +shmem 53805056 +file_mapped 2144421 +file_dirty 0 +file_writeback 0 +inactive_anon 0 +active_anon 0 +inactive_file 0 +active_file 0 +unevictable 0 +slab_reclaimable 0 +slab_unreclaimable 0 +pgfault 0 +pgmajfault 0 +workingset_refault 0 +workingset_activate 0 +workingset_nodereclaim 0 +pgrefill 0 +pgscan 0 +pgsteal 0 +pgactivate 0 +pgdeactivate 0 +pglazyfree 0 +pglazyfreed 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.swap.current b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.swap.current new file mode 100644 index 0000000000..9d68dc6c0b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.swap.current @@ -0,0 +1 @@ +405020672 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.swap.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.swap.max new file mode 100644 index 0000000000..ac513d575a --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data/v2/memory.swap.max @@ -0,0 +1 @@ +736870912 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/cpuacct/cpuacct.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/cpuacct/cpuacct.stat new file mode 100644 index 0000000000..b551341518 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/cpuacct/cpuacct.stat @@ -0,0 +1,2 @@ +user 5760 +system 1753 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.limit_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.limit_in_bytes new file mode 100644 index 0000000000..7cef971fd2 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.limit_in_bytes @@ -0,0 +1 @@ +9223372036854710272 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.memsw.limit_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.memsw.limit_in_bytes new file mode 100644 index 0000000000..7cef971fd2 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.memsw.limit_in_bytes @@ -0,0 +1 @@ +9223372036854710272 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.memsw.usage_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.memsw.usage_in_bytes new file mode 100644 index 0000000000..9d68dc6c0b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.memsw.usage_in_bytes @@ -0,0 +1 @@ +405020672 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.stat new file mode 100644 index 0000000000..5cde0a4b00 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.stat @@ -0,0 +1,36 @@ +cache 275480576 +rss 92704768 +rss_huge 0 +shmem 53805056 +mapped_file 69070848 +dirty 135168 +writeback 0 +swap 0 +pgpgin 2103354 +pgpgout 2013441 +pgfault 2526612 +pgmajfault 33 +inactive_anon 17199104 +active_anon 129626112 +inactive_file 106287104 +active_file 115240960 +unevictable 0 +hierarchical_memory_limit 536870912 +hierarchical_memsw_limit 1073741824 +total_cache 275480576 +total_rss 92704768 +total_rss_huge 0 +total_shmem 53805056 +total_mapped_file 69070848 +total_dirty 135168 +total_writeback 0 +total_swap 0 +total_pgpgin 2103354 +total_pgpgout 2013441 +total_pgfault 2526612 +total_pgmajfault 33 +total_inactive_anon 17199104 +total_active_anon 129626112 +total_inactive_file 106287104 +total_active_file 115240960 +total_unevictable 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.usage_in_bytes b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.usage_in_bytes new file mode 100644 index 0000000000..31e5d7a68d --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v1/memory/memory.usage_in_bytes @@ -0,0 +1 @@ +392151040 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/cgroup.controllers b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/cgroup.controllers new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/cpu.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/cpu.stat new file mode 100644 index 0000000000..bfb28213a2 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/cpu.stat @@ -0,0 +1,6 @@ +user_usec 5760 +system_usec 1753 +usage_usec 7513 +nr_periods 0 +nr_throttled 0 +throttled_usec 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.current b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.current new file mode 100644 index 0000000000..31e5d7a68d --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.current @@ -0,0 +1 @@ +392151040 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.max new file mode 100644 index 0000000000..355295a05a --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.max @@ -0,0 +1 @@ +max diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.stat new file mode 100644 index 0000000000..4b4c61b919 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.stat @@ -0,0 +1,28 @@ +anon 92704768 +file 275480576 +kernel_stack 5325325 +slab 2432523 +sock 12288 +shmem 53805056 +file_mapped 2144421 +file_dirty 0 +file_writeback 0 +inactive_anon 0 +active_anon 0 +inactive_file 0 +active_file 0 +unevictable 0 +slab_reclaimable 0 +slab_unreclaimable 0 +pgfault 0 +pgmajfault 0 +workingset_refault 0 +workingset_activate 0 +workingset_nodereclaim 0 +pgrefill 0 +pgscan 0 +pgsteal 0 +pgactivate 0 +pgdeactivate 0 +pglazyfree 0 +pglazyfreed 0 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.swap.current b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.swap.current new file mode 100644 index 0000000000..9d68dc6c0b --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.swap.current @@ -0,0 +1 @@ +405020672 \ No newline at end of file diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.swap.max b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.swap.max new file mode 100644 index 0000000000..355295a05a --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/good_data_no_limits/v2/memory.swap.max @@ -0,0 +1 @@ +max diff --git a/internal/collector/containermetricsreceiver/internal/scraper/testdata/proc/stat b/internal/collector/containermetricsreceiver/internal/scraper/testdata/proc/stat new file mode 100644 index 0000000000..9507a23a06 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/scraper/testdata/proc/stat @@ -0,0 +1,10 @@ +cpu 366663 264 272326 1072402 2744 0 1784 0 0 0 +cpu0 184150 125 135467 536200 1329 0 1054 0 0 0 +cpu1 182513 139 136858 536202 1414 0 730 0 0 0 +intr 9910796 42 9 0 0 2970 0 0 0 0 0 0 0 158 0 0 0 21178 0 0 102203 2518965 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ctxt 13799929 +btime 1647342564 +processes 42642 +procs_running 4 +procs_blocked 0 +softirq 2947921 0 1583327 3519 155547 254406 0 6578 357545 0 586999 diff --git a/internal/collector/containermetricsreceiver/internal/utils.go b/internal/collector/containermetricsreceiver/internal/utils.go new file mode 100644 index 0000000000..46dd9adc13 --- /dev/null +++ b/internal/collector/containermetricsreceiver/internal/utils.go @@ -0,0 +1,77 @@ +// Copyright (c) F5, Inc. +// +// This source code is licensed under the Apache License, Version 2.0 license found in the +// LICENSE file in the root directory of this source tree. + +package internal + +import ( + "bufio" + "io" + "os" + "strconv" + "strings" +) + +type Source interface { + Stats() float64 +} + +func ReadLines(filename string) ([]string, error) { + return ReadLinesOffsetN(filename, 0, -1) +} + +// nolint: revive +func ReadLinesOffsetN(filename string, offset uint, n int) ([]string, error) { + f, err := os.Open(filename) + if err != nil { + return []string{}, err + } + defer f.Close() + + var ret []string + + r := bufio.NewReader(f) + for i := 0; i < n+int(offset) || n < 0; i++ { + line, readErr := r.ReadString('\n') + if readErr != nil { + if readErr == io.EOF && len(line) > 0 { + ret = append(ret, strings.Trim(line, "\n")) + } + + break + } + if i < int(offset) { + continue + } + ret = append(ret, strings.Trim(line, "\n")) + } + + return ret, nil +} + +func ReadSingleValueCgroupFile(filename string) (string, error) { + lines, err := ReadLinesOffsetN(filename, 0, 1) + if err != nil { + return "", err + } + + return strings.TrimSpace(lines[0]), nil +} + +func ReadIntegerValueCgroupFile(filename string) (uint64, error) { + value, err := ReadSingleValueCgroupFile(filename) + if err != nil { + return 0, err + } + + return strconv.ParseUint(value, 10, 64) +} + +func IsCgroupV2(basePath string) bool { + if _, err := os.Stat(basePath + "/cgroup.controllers"); err == nil { + return true + } + + return false +} diff --git a/internal/collector/containermetricsreceiver/metadata.yaml b/internal/collector/containermetricsreceiver/metadata.yaml new file mode 100644 index 0000000000..fab90f74e8 --- /dev/null +++ b/internal/collector/containermetricsreceiver/metadata.yaml @@ -0,0 +1,11 @@ +type: containermetrics + +status: + class: receiver + stability: + beta: [metrics] + distributions: [contrib] + codeowners: + active: [ aphralG, dhurley, craigell, sean-breen, CVanF5 ] +tests: + config: diff --git a/internal/collector/factories.go b/internal/collector/factories.go index 285ccc14b7..7c00fe40fb 100644 --- a/internal/collector/factories.go +++ b/internal/collector/factories.go @@ -2,11 +2,14 @@ // // This source code is licensed under the Apache License, Version 2.0 license found in the // LICENSE file in the root directory of this source tree. + package collector import ( + "github.com/nginx/agent/v3/internal/collector/containermetricsreceiver" nginxreceiver "github.com/nginx/agent/v3/internal/collector/nginxossreceiver" "github.com/nginx/agent/v3/internal/collector/nginxplusreceiver" + "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/prometheusexporter" "github.com/open-telemetry/opentelemetry-collector-contrib/extension/headerssetterextension" "github.com/open-telemetry/opentelemetry-collector-contrib/extension/healthcheckextension" @@ -76,6 +79,7 @@ func createExtensionFactories() map[component.Type]extension.Factory { func createReceiverFactories() map[component.Type]receiver.Factory { receiverList := []receiver.Factory{ otlpreceiver.NewFactory(), + containermetricsreceiver.NewFactory(), hostmetricsreceiver.NewFactory(), nginxreceiver.NewFactory(), nginxplusreceiver.NewFactory(), diff --git a/internal/collector/factories_test.go b/internal/collector/factories_test.go index 140a5f873c..9d2dec3976 100644 --- a/internal/collector/factories_test.go +++ b/internal/collector/factories_test.go @@ -18,7 +18,7 @@ func TestOTelComponentFactoriesDefault(t *testing.T) { require.NoError(t, err, "OTelComponentFactories should not return an error") assert.NotNil(t, factories, "factories should not be nil") - assert.Len(t, factories.Receivers, 5) + assert.Len(t, factories.Receivers, 6) assert.Len(t, factories.Processors, 8) assert.Len(t, factories.Exporters, 4) assert.Len(t, factories.Extensions, 3) diff --git a/internal/collector/nginxplusreceiver/metadata.yaml b/internal/collector/nginxplusreceiver/metadata.yaml index 6863147e0d..61d43d588c 100644 --- a/internal/collector/nginxplusreceiver/metadata.yaml +++ b/internal/collector/nginxplusreceiver/metadata.yaml @@ -7,7 +7,7 @@ status: beta: [metrics] distributions: [contrib] codeowners: - active: [apgralG, dhurley, craigell, sean-breen, Rashmiti, CVanF5] + active: [aphralG, dhurley, craigell, sean-breen, Rashmiti, CVanF5] resource_attributes: instance.id: diff --git a/internal/collector/otelcol.tmpl b/internal/collector/otelcol.tmpl index b2f7fcf446..c17644697c 100644 --- a/internal/collector/otelcol.tmpl +++ b/internal/collector/otelcol.tmpl @@ -1,4 +1,10 @@ receivers: +{{- if ne .Receivers.ContainerMetrics nil }} + containermetrics: + {{- if .Receivers.ContainerMetrics.CollectionInterval }} + collection_interval: {{ .Receivers.ContainerMetrics.CollectionInterval }} + {{- end}} +{{- end }} {{- if ne .Receivers.HostMetrics nil }} hostmetrics: {{- if .Receivers.HostMetrics.CollectionInterval }} @@ -226,9 +232,12 @@ service: {{- end}} {{- end}} pipelines: - {{- if or (ne .Receivers.HostMetrics nil) (gt (len .Receivers.OtlpReceivers) 0) (gt (len .Receivers.NginxReceivers) 0) (gt (len .Receivers.NginxPlusReceivers) 0) }} + {{- if or (ne .Receivers.HostMetrics nil) (ne .Receivers.ContainerMetrics nil) (gt (len .Receivers.OtlpReceivers) 0) (gt (len .Receivers.NginxReceivers) 0) (gt (len .Receivers.NginxPlusReceivers) 0) }} metrics: receivers: + {{- if ne .Receivers.ContainerMetrics nil }} + - containermetrics + {{- end }} {{- if ne .Receivers.HostMetrics nil }} - hostmetrics {{- end }} diff --git a/internal/collector/settings_test.go b/internal/collector/settings_test.go index 501a5f3db9..cf4415323e 100644 --- a/internal/collector/settings_test.go +++ b/internal/collector/settings_test.go @@ -75,6 +75,10 @@ func TestTemplateWrite(t *testing.T) { cfg.Collector.Exporters.Debug = &config.DebugExporter{} + cfg.Collector.Receivers.ContainerMetrics = &config.ContainerMetricsReceiver{ + CollectionInterval: time.Second, + } + cfg.Collector.Receivers.HostMetrics = &config.HostMetrics{ CollectionInterval: time.Minute, InitialDelay: time.Second, diff --git a/internal/config/config.go b/internal/config/config.go index 24c858ddb5..96634b3296 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,6 +19,8 @@ import ( "strings" "time" + "github.com/nginx/agent/v3/internal/datasource/host" + "github.com/nginx/agent/v3/internal/datasource/file" "github.com/goccy/go-yaml" @@ -152,16 +154,29 @@ func defaultCollector(collector *Collector, config *Config) { token = pathToken } - collector.Receivers.HostMetrics = &HostMetrics{ - Scrapers: &HostMetricsScrapers{ - CPU: &CPUScraper{}, - Disk: &DiskScraper{}, - Filesystem: &FilesystemScraper{}, - Memory: &MemoryScraper{}, - Network: nil, - }, - CollectionInterval: 1 * time.Minute, - InitialDelay: 1 * time.Second, + if host.NewInfo().IsContainer() { + collector.Receivers.ContainerMetrics = &ContainerMetricsReceiver{ + CollectionInterval: 1 * time.Minute, + } + collector.Receivers.HostMetrics = &HostMetrics{ + Scrapers: &HostMetricsScrapers{ + Network: &NetworkScraper{}, + }, + CollectionInterval: 1 * time.Minute, + InitialDelay: 1 * time.Second, + } + } else { + collector.Receivers.HostMetrics = &HostMetrics{ + Scrapers: &HostMetricsScrapers{ + CPU: &CPUScraper{}, + Memory: &MemoryScraper{}, + Disk: &DiskScraper{}, + Filesystem: &FilesystemScraper{}, + Network: &NetworkScraper{}, + }, + CollectionInterval: 1 * time.Minute, + InitialDelay: 1 * time.Second, + } } collector.Exporters.OtlpExporters = append(collector.Exporters.OtlpExporters, OtlpExporter{ diff --git a/internal/config/types.go b/internal/config/types.go index 0546508703..eb3ad3c5d7 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -181,11 +181,12 @@ type ( // OTel Collector Receiver configuration. Receivers struct { - HostMetrics *HostMetrics `yaml:"host_metrics" mapstructure:"host_metrics"` - OtlpReceivers []OtlpReceiver `yaml:"otlp_receivers" mapstructure:"otlp_receivers"` - NginxReceivers []NginxReceiver `yaml:"nginx_receivers" mapstructure:"nginx_receivers"` - NginxPlusReceivers []NginxPlusReceiver `yaml:"nginx_plus_receivers" mapstructure:"nginx_plus_receivers"` - TcplogReceivers []TcplogReceiver `yaml:"tcplog_receivers" mapstructure:"tcplog_receivers"` + ContainerMetrics *ContainerMetricsReceiver `yaml:"container_metrics" mapstructure:"container_metrics"` + HostMetrics *HostMetrics `yaml:"host_metrics" mapstructure:"host_metrics"` + OtlpReceivers []OtlpReceiver `yaml:"otlp_receivers" mapstructure:"otlp_receivers"` + NginxReceivers []NginxReceiver `yaml:"nginx_receivers" mapstructure:"nginx_receivers"` + NginxPlusReceivers []NginxPlusReceiver `yaml:"nginx_plus_receivers" mapstructure:"nginx_plus_receivers"` + TcplogReceivers []TcplogReceiver `yaml:"tcplog_receivers" mapstructure:"tcplog_receivers"` } OtlpReceiver struct { @@ -229,6 +230,10 @@ type ( PlusAPI APIDetails `yaml:"api_details" mapstructure:"api_details"` } + ContainerMetricsReceiver struct { + CollectionInterval time.Duration `yaml:"-" mapstructure:"collection_interval"` + } + HostMetrics struct { Scrapers *HostMetricsScrapers `yaml:"scrapers" mapstructure:"scrapers"` CollectionInterval time.Duration `yaml:"collection_interval" mapstructure:"collection_interval"` @@ -382,6 +387,7 @@ func (c *Config) IsACollectorExporterConfigured() bool { c.Collector.Exporters.Debug != nil } +// nolint: cyclop, revive func (c *Config) AreReceiversConfigured() bool { if c.Collector == nil { return false @@ -394,6 +400,7 @@ func (c *Config) AreReceiversConfigured() bool { c.Collector.Receivers.NginxReceivers != nil || len(c.Collector.Receivers.NginxReceivers) > 0 || c.Collector.Receivers.HostMetrics != nil || + c.Collector.Receivers.ContainerMetrics != nil || c.Collector.Receivers.TcplogReceivers != nil || len(c.Collector.Receivers.TcplogReceivers) > 0 } diff --git a/test/config/collector/test-opentelemetry-collector-agent.yaml b/test/config/collector/test-opentelemetry-collector-agent.yaml index 915e64a5b2..9794369be5 100644 --- a/test/config/collector/test-opentelemetry-collector-agent.yaml +++ b/test/config/collector/test-opentelemetry-collector-agent.yaml @@ -1,4 +1,6 @@ receivers: + containermetrics: + collection_interval: 1s hostmetrics: collection_interval: 1m0s initial_delay: 1s @@ -99,6 +101,7 @@ service: pipelines: metrics: receivers: + - containermetrics - hostmetrics - otlp/0 - nginx/123 diff --git a/test/mock/collector/nginx-agent.conf b/test/mock/collector/nginx-agent.conf index c2ef38491e..096d0b8f4e 100644 --- a/test/mock/collector/nginx-agent.conf +++ b/test/mock/collector/nginx-agent.conf @@ -34,6 +34,8 @@ collector: log: level: DEBUG receivers: + container_metrics: + collection_interval: 10s host_metrics: collection_interval: 1m0s initial_delay: 1s @@ -59,6 +61,7 @@ collector: processors: batch: {} exporters: + debug: otlp_exporters: - server: host: "otel-collector"