diff --git a/receiver/hostmetricsreceiver/config_test.go b/receiver/hostmetricsreceiver/config_test.go index d59568f6931..e204477cfd3 100644 --- a/receiver/hostmetricsreceiver/config_test.go +++ b/receiver/hostmetricsreceiver/config_test.go @@ -28,6 +28,7 @@ import ( "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/cpuscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/diskscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/filesystemscraper" + "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/loadscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/memoryscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/networkscraper" ) @@ -63,6 +64,7 @@ func TestLoadConfig(t *testing.T) { Scrapers: map[string]internal.Config{ cpuscraper.TypeStr: &cpuscraper.Config{ReportPerCPU: true}, diskscraper.TypeStr: &diskscraper.Config{}, + loadscraper.TypeStr: &loadscraper.Config{}, filesystemscraper.TypeStr: &filesystemscraper.Config{}, memoryscraper.TypeStr: &memoryscraper.Config{}, networkscraper.TypeStr: &networkscraper.Config{}, diff --git a/receiver/hostmetricsreceiver/example_config.yaml b/receiver/hostmetricsreceiver/example_config.yaml index ddca1c2ab1f..545bc709f18 100644 --- a/receiver/hostmetricsreceiver/example_config.yaml +++ b/receiver/hostmetricsreceiver/example_config.yaml @@ -7,7 +7,7 @@ receivers: collection_interval: 1m scrapers: cpu: - report_per_cpu: false + load: memory: disk: filesystem: diff --git a/receiver/hostmetricsreceiver/factory.go b/receiver/hostmetricsreceiver/factory.go index f8070715023..a612bde7232 100644 --- a/receiver/hostmetricsreceiver/factory.go +++ b/receiver/hostmetricsreceiver/factory.go @@ -30,6 +30,7 @@ import ( "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/cpuscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/diskscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/filesystemscraper" + "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/loadscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/memoryscraper" "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/scraper/networkscraper" ) @@ -53,6 +54,7 @@ func NewFactory() *Factory { scraperFactories: map[string]internal.Factory{ cpuscraper.TypeStr: &cpuscraper.Factory{}, diskscraper.TypeStr: &diskscraper.Factory{}, + loadscraper.TypeStr: &loadscraper.Factory{}, filesystemscraper.TypeStr: &filesystemscraper.Factory{}, memoryscraper.TypeStr: &memoryscraper.Factory{}, networkscraper.TypeStr: &networkscraper.Factory{}, diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/config.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/config.go new file mode 100644 index 00000000000..8908c903380 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/config.go @@ -0,0 +1,22 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loadscraper + +import "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal" + +// Config relating to Load Metric Scraper. +type Config struct { + internal.ConfigSettings `mapstructure:",squash"` // squash ensures fields are correctly decoded in embedded struct +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/factory.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/factory.go new file mode 100644 index 00000000000..1ef2f785120 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/factory.go @@ -0,0 +1,49 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loadscraper + +import ( + "context" + + "go.uber.org/zap" + + "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal" +) + +// This file implements Factory for Load scraper. + +const ( + // The value of "type" key in configuration. + TypeStr = "load" +) + +// Factory is the Factory for scraper. +type Factory struct { +} + +// CreateDefaultConfig creates the default configuration for the Scraper. +func (f *Factory) CreateDefaultConfig() internal.Config { + return &Config{} +} + +// CreateMetricsScraper creates a scraper based on provided config. +func (f *Factory) CreateMetricsScraper( + ctx context.Context, + logger *zap.Logger, + config internal.Config, +) (internal.Scraper, error) { + cfg := config.(*Config) + return newLoadScraper(ctx, logger, cfg), nil +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/factory_test.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/factory_test.go new file mode 100644 index 00000000000..83baa94dbe8 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/factory_test.go @@ -0,0 +1,39 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loadscraper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" +) + +func TestCreateDefaultConfig(t *testing.T) { + factory := &Factory{} + cfg := factory.CreateDefaultConfig() + assert.IsType(t, &Config{}, cfg) +} + +func TestCreateMetricsScraper(t *testing.T) { + factory := &Factory{} + cfg := &Config{} + + scraper, err := factory.CreateMetricsScraper(context.Background(), zap.NewNop(), cfg) + + assert.NoError(t, err) + assert.NotNil(t, scraper) +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_constants.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_constants.go new file mode 100644 index 00000000000..3795c0229b4 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_constants.go @@ -0,0 +1,57 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loadscraper + +import ( + "go.opentelemetry.io/collector/consumer/pdata" +) + +// load metric constants + +var metric1MLoadDescriptor = createMetric1MLoadDescriptor() + +func createMetric1MLoadDescriptor() pdata.MetricDescriptor { + descriptor := pdata.NewMetricDescriptor() + descriptor.InitEmpty() + descriptor.SetName("host/load/1m") + descriptor.SetDescription("Average CPU Load over 1 minute.") + descriptor.SetUnit("1") + descriptor.SetType(pdata.MetricTypeGaugeDouble) + return descriptor +} + +var metric5MLoadDescriptor = createMetric5MLoadDescriptor() + +func createMetric5MLoadDescriptor() pdata.MetricDescriptor { + descriptor := pdata.NewMetricDescriptor() + descriptor.InitEmpty() + descriptor.SetName("host/load/5m") + descriptor.SetDescription("Average CPU Load over 5 minutes.") + descriptor.SetUnit("1") + descriptor.SetType(pdata.MetricTypeGaugeDouble) + return descriptor +} + +var metric15MLoadDescriptor = createMetric15MLoadDescriptor() + +func createMetric15MLoadDescriptor() pdata.MetricDescriptor { + descriptor := pdata.NewMetricDescriptor() + descriptor.InitEmpty() + descriptor.SetName("host/load/15m") + descriptor.SetDescription("Average CPU Load over 15 minutes.") + descriptor.SetUnit("1") + descriptor.SetType(pdata.MetricTypeGaugeDouble) + return descriptor +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper.go new file mode 100644 index 00000000000..62dd2a40c4a --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper.go @@ -0,0 +1,75 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loadscraper + +import ( + "context" + "time" + + "go.opencensus.io/trace" + "go.uber.org/zap" + + "go.opentelemetry.io/collector/consumer/pdata" +) + +// scraper for Load Metrics +type scraper struct { + logger *zap.Logger + config *Config +} + +// newLoadScraper creates a set of Load related metrics +func newLoadScraper(_ context.Context, logger *zap.Logger, cfg *Config) *scraper { + return &scraper{logger: logger, config: cfg} +} + +// Initialize +func (s *scraper) Initialize(ctx context.Context) error { + return startSampling(ctx, s.logger) +} + +// Close +func (s *scraper) Close(ctx context.Context) error { + return stopSampling(ctx) +} + +// ScrapeMetrics +func (s *scraper) ScrapeMetrics(ctx context.Context) (pdata.MetricSlice, error) { + _, span := trace.StartSpan(ctx, "loadscraper.ScrapeMetrics") + defer span.End() + + metrics := pdata.NewMetricSlice() + + avgLoadValues, err := getSampledLoadAverages() + if err != nil { + return metrics, err + } + + metrics.Resize(3) + initializeLoadMetric(metrics.At(0), metric1MLoadDescriptor, avgLoadValues.Load1) + initializeLoadMetric(metrics.At(1), metric5MLoadDescriptor, avgLoadValues.Load5) + initializeLoadMetric(metrics.At(2), metric15MLoadDescriptor, avgLoadValues.Load15) + return metrics, nil +} + +func initializeLoadMetric(metric pdata.Metric, metricDescriptor pdata.MetricDescriptor, value float64) { + metricDescriptor.CopyTo(metric.MetricDescriptor()) + + idps := metric.DoubleDataPoints() + idps.Resize(1) + dp := idps.At(0) + dp.SetTimestamp(pdata.TimestampUnixNano(uint64(time.Now().UnixNano()))) + dp.SetValue(value) +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_others.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_others.go new file mode 100644 index 00000000000..9e8a4f951c8 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_others.go @@ -0,0 +1,37 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build !windows + +package loadscraper + +import ( + "context" + + "github.com/shirou/gopsutil/load" + "go.uber.org/zap" +) + +// unix based systems sample & compute load averages in the kernel, so nothing to do here +func startSampling(_ context.Context, _ *zap.Logger) error { + return nil +} + +func stopSampling(_ context.Context) error { + return nil +} + +func getSampledLoadAverages() (*load.AvgStat, error) { + return load.Avg() +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_test.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_test.go new file mode 100644 index 00000000000..d9ad07c0153 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_test.go @@ -0,0 +1,58 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package loadscraper + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "go.opentelemetry.io/collector/consumer/pdata" + "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal" +) + +type validationFn func(*testing.T, pdata.MetricSlice) + +func TestScrapeMetrics(t *testing.T) { + createScraperAndValidateScrapedMetrics(t, &Config{}, func(t *testing.T, metrics pdata.MetricSlice) { + // expect 3 metrics + assert.Equal(t, 3, metrics.Len()) + + // expect a single datapoint for 1m, 5m & 15m load metrics + assertMetricHasSingleDatapoint(t, metrics.At(0), metric1MLoadDescriptor) + assertMetricHasSingleDatapoint(t, metrics.At(1), metric5MLoadDescriptor) + assertMetricHasSingleDatapoint(t, metrics.At(2), metric15MLoadDescriptor) + }) +} + +func assertMetricHasSingleDatapoint(t *testing.T, metric pdata.Metric, descriptor pdata.MetricDescriptor) { + internal.AssertDescriptorEqual(t, descriptor, metric.MetricDescriptor()) + assert.Equal(t, 1, metric.DoubleDataPoints().Len()) +} + +func createScraperAndValidateScrapedMetrics(t *testing.T, config *Config, assertFn validationFn) { + scraper := newLoadScraper(context.Background(), zap.NewNop(), config) + err := scraper.Initialize(context.Background()) + require.NoError(t, err, "Failed to initialize load scraper: %v", err) + defer func() { assert.NoError(t, scraper.Close(context.Background())) }() + + metrics, err := scraper.ScrapeMetrics(context.Background()) + require.NoError(t, err, "Failed to scrape metrics: %v", err) + + assertFn(t, metrics) +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_windows.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_windows.go new file mode 100644 index 00000000000..96e4b70cf8b --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_windows.go @@ -0,0 +1,147 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build windows + +package loadscraper + +import ( + "context" + "math" + "sync" + "time" + + "github.com/shirou/gopsutil/load" + "go.uber.org/zap" + + "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/windows/pdh" +) + +// Sample processor queue length at a 5s frequency, and calculate exponentially weighted moving averages +// as per https://en.wikipedia.org/wiki/Load_(computing)#Unix-style_load_calculation + +const processorQueueLengthPath = `\System\Processor Queue Length` + +var ( + samplingFrequency = 5 * time.Second + + loadAvgFactor1m = 1 / math.Exp(samplingFrequency.Seconds()/time.Minute.Seconds()) + loadAvgFactor5m = 1 / math.Exp(samplingFrequency.Seconds()/(5*time.Minute).Seconds()) + loadAvgFactor15m = 1 / math.Exp(samplingFrequency.Seconds()/(15*time.Minute).Seconds()) +) + +var ( + scraperCount int + startupLock sync.Mutex + + samplerInstance *sampler +) + +type sampler struct { + done chan struct{} + logger *zap.Logger + processorQueueLengthCounter pdh.PerfCounterScraper + loadAvg1m float64 + loadAvg5m float64 + loadAvg15m float64 + lock sync.RWMutex +} + +func startSampling(ctx context.Context, logger *zap.Logger) error { + startupLock.Lock() + defer startupLock.Unlock() + + // startSampling may be called multiple times if multiple scrapers are + // initialized - but we only want to initialize a single load sampler + scraperCount++ + if samplerInstance != nil { + return nil + } + + processorQueueLengthCounter, err := pdh.NewPerfCounter(processorQueueLengthPath, false) + if err != nil { + return err + } + + samplerInstance = &sampler{ + logger: logger, + processorQueueLengthCounter: processorQueueLengthCounter, + done: make(chan struct{}), + } + + samplerInstance.startSamplingTicker() + return nil +} + +func (sw *sampler) startSamplingTicker() { + go func() { + ticker := time.NewTicker(samplingFrequency) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + sw.sampleLoad() + case <-sw.done: + return + } + } + }() +} + +func (sw *sampler) sampleLoad() { + counterValues, err := sw.processorQueueLengthCounter.ScrapeData() + if err != nil { + sw.logger.Error("Failed to measure processor queue length", zap.Error(err)) + return + } + + currentLoad := counterValues[0].Value + + sw.lock.Lock() + defer sw.lock.Unlock() + sw.loadAvg1m = sw.loadAvg1m*loadAvgFactor1m + currentLoad*(1-loadAvgFactor1m) + sw.loadAvg5m = sw.loadAvg5m*loadAvgFactor5m + currentLoad*(1-loadAvgFactor5m) + sw.loadAvg15m = sw.loadAvg15m*loadAvgFactor15m + currentLoad*(1-loadAvgFactor15m) +} + +func stopSampling(_ context.Context) error { + startupLock.Lock() + defer startupLock.Unlock() + + // only stop sampling if all load scrapers have been closed + scraperCount-- + if scraperCount > 0 { + return nil + } + + close(samplerInstance.done) + + err := samplerInstance.processorQueueLengthCounter.Close() + samplerInstance = nil + return err +} + +func getSampledLoadAverages() (*load.AvgStat, error) { + samplerInstance.lock.RLock() + defer samplerInstance.lock.RUnlock() + + avgStat := &load.AvgStat{ + Load1: samplerInstance.loadAvg1m, + Load5: samplerInstance.loadAvg5m, + Load15: samplerInstance.loadAvg15m, + } + + return avgStat, nil +} diff --git a/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_windows_test.go b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_windows_test.go new file mode 100644 index 00000000000..981e59ac307 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/scraper/loadscraper/load_scraper_windows_test.go @@ -0,0 +1,101 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build windows + +package loadscraper + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" + + "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/windows/pdh" +) + +func TestStartSampling(t *testing.T) { + // override sampling frequency to 2ms + samplingFrequency = 2 * time.Millisecond + + // startSampling should set up perf counter and start sampling + startSampling(context.Background(), zap.NewNop()) + assertSamplingUnderway(t) + + // override the processor queue length perf counter with a mock + // that will ensure a positive value is returned + assert.IsType(t, &pdh.PerfCounter{}, samplerInstance.processorQueueLengthCounter) + samplerInstance.processorQueueLengthCounter = pdh.NewMockPerfCounter(100) + + // second call to startSampling should succeed, but not do anything + startSampling(context.Background(), zap.NewNop()) + assertSamplingUnderway(t) + assert.IsType(t, &pdh.MockPerfCounter{}, samplerInstance.processorQueueLengthCounter) + + // ensure that a positive load avg is returned by a call to + // "getSampledLoadAverages" which validates the value from the + // mock perf counter was used + require.Eventually(t, func() bool { + avgLoadValues, err := getSampledLoadAverages() + assert.NoError(t, err) + return avgLoadValues.Load1 > 0 && avgLoadValues.Load5 > 0 && avgLoadValues.Load15 > 0 + }, time.Second, time.Millisecond, "Load Avg was not set after 1s") + + // sampling should continue after first call to stopSampling since + // startSampling was called twice + stopSampling(context.Background()) + assertSamplingUnderway(t) + + // second call to stopSampling should close perf counter, stop + // sampling, and clean up the sampler + stopSampling(context.Background()) + assert.Nil(t, samplerInstance) +} + +func assertSamplingUnderway(t *testing.T) { + assert.NotNil(t, samplerInstance) + assert.NotNil(t, samplerInstance.processorQueueLengthCounter) + + select { + case <-samplerInstance.done: + assert.Fail(t, "Load scraper sampling done channel unexpectedly closed") + default: + } +} + +func TestSampleLoad(t *testing.T) { + mockCounter := pdh.NewMockPerfCounter(10, 20, 30, 40, 50) + samplerInstance = &sampler{processorQueueLengthCounter: mockCounter} + + for i := 0; i < len(mockCounter.ReturnValues); i++ { + samplerInstance.sampleLoad() + } + + assert.Equal(t, calcExpectedLoad(mockCounter.ReturnValues, loadAvgFactor1m), samplerInstance.loadAvg1m) + assert.Equal(t, calcExpectedLoad(mockCounter.ReturnValues, loadAvgFactor5m), samplerInstance.loadAvg5m) + assert.Equal(t, calcExpectedLoad(mockCounter.ReturnValues, loadAvgFactor15m), samplerInstance.loadAvg15m) +} + +func calcExpectedLoad(scrapedValues []float64, loadAvgFactor float64) float64 { + // replicate the calculations that should be performed to determine the exponentially + // weighted moving averages based on the specified scraped values + var expectedLoad float64 + for i := 0; i < len(scrapedValues); i++ { + expectedLoad = expectedLoad*loadAvgFactor + scrapedValues[i]*(1-loadAvgFactor) + } + return expectedLoad +} diff --git a/receiver/hostmetricsreceiver/internal/windows/pdh/performance_counter.go b/receiver/hostmetricsreceiver/internal/windows/pdh/performance_counter.go index 113758fa141..7bddae5d7ba 100644 --- a/receiver/hostmetricsreceiver/internal/windows/pdh/performance_counter.go +++ b/receiver/hostmetricsreceiver/internal/windows/pdh/performance_counter.go @@ -20,6 +20,13 @@ import "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/thir const totalInstanceName = "_Total" +type PerfCounterScraper interface { + // ScrapeData collects a measurement and returns the value(s). + ScrapeData() ([]win_perf_counters.CounterValue, error) + // Close all counters/handles related to the query and free all associated memory. + Close() error +} + type PerfCounter struct { query win_perf_counters.PerformanceQuery handle win_perf_counters.PDH_HCOUNTER @@ -56,12 +63,12 @@ func NewPerfCounter(counterPath string, collectOnStartup bool) (*PerfCounter, er return counter, nil } -// Close all counters/handles related to the query and free all associated memory. +// Close func (pc *PerfCounter) Close() error { return pc.query.Close() } -// ScrapeData collects a measurement and returns the value(s). +// ScrapeData func (pc *PerfCounter) ScrapeData() ([]win_perf_counters.CounterValue, error) { err := pc.query.CollectData() if err != nil { diff --git a/receiver/hostmetricsreceiver/internal/windows/pdh/performance_counter_mock.go b/receiver/hostmetricsreceiver/internal/windows/pdh/performance_counter_mock.go new file mode 100644 index 00000000000..95fea8aba21 --- /dev/null +++ b/receiver/hostmetricsreceiver/internal/windows/pdh/performance_counter_mock.go @@ -0,0 +1,57 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +build windows + +package pdh + +import "go.opentelemetry.io/collector/receiver/hostmetricsreceiver/internal/third_party/telegraf/win_perf_counters" + +type MockPerfCounter struct { + ReturnValues []float64 + + timesCalled int +} + +// NewMockPerfCounter creates a MockPerfCounter that returns the supplied +// values on each successive call to ScrapeData in the specified order. +// +// If ScrapeData is called more times than the number of values supplied, +// the last supplied value will be returned for all subsequent calls. +func NewMockPerfCounter(valuesToBeReturned ...float64) *MockPerfCounter { + return &MockPerfCounter{ReturnValues: valuesToBeReturned} +} + +// Close +func (pc *MockPerfCounter) Close() error { + return nil +} + +// ScrapeData +func (pc *MockPerfCounter) ScrapeData() ([]win_perf_counters.CounterValue, error) { + returnIndex := pc.timesCalled + if returnIndex >= len(pc.ReturnValues) { + returnIndex = len(pc.ReturnValues) - 1 + } + + returnValue := pc.ReturnValues[returnIndex] + + pc.timesCalled++ + + returnValueArray := []win_perf_counters.CounterValue{ + win_perf_counters.CounterValue{Value: returnValue}, + } + + return returnValueArray, nil +} diff --git a/receiver/hostmetricsreceiver/testdata/config.yaml b/receiver/hostmetricsreceiver/testdata/config.yaml index 87014216e8d..ea44b2d1950 100644 --- a/receiver/hostmetricsreceiver/testdata/config.yaml +++ b/receiver/hostmetricsreceiver/testdata/config.yaml @@ -8,6 +8,7 @@ receivers: cpu: report_per_cpu: true disk: + load: filesystem: memory: network: