diff --git a/receiver/windowsperfcountersreceiver/README.md b/receiver/windowsperfcountersreceiver/README.md index 5864b84c3005..56af617e366c 100644 --- a/receiver/windowsperfcountersreceiver/README.md +++ b/receiver/windowsperfcountersreceiver/README.md @@ -1,10 +1,18 @@ # Windows Performance Counters Receiver -#### :warning: This receiver is still under construction. It currently only supports very basic functionality, i.e. performance counters with no 'Instance'. +#### :warning: This receiver is still under construction. This receiver, for Windows only, captures the configured system, application, or custom performance counter data from the Windows registry using the [PDH interface](https://docs.microsoft.com/en-us/windows/win32/perfctrs/using-the-pdh-functions-to-consume-counter-data). +It is based on the [Telegraf Windows Performance Counters Input +Plugin](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/win_perf_counters). + +Metrics will be generated with names and labels that match the performance +counter path, i.e. + +- `Memory\Committed Bytes` +- `Processor\% Processor Time`, with a datapoint for each `Instance` label = (`_Total`, `1`, `2`, `3`, ... ) ## Configuration @@ -16,10 +24,23 @@ windowsperfcounters: collection_interval: # default = "1m" counters: - object: + instances: []* counters: - ``` +*Note `instances` can have several special values depending on the type of +counter: + +Value | Interpretation +-- | -- +`""` (or not specified) | This is the only valid value if the counter has no instances +`"*"` | All instances +`"_Total"` | The "total" instance +`"instance1"` | A single instance +`["instance1", "instance2", ...]` | A set of instances +`["_Total", "instance1", "instance2", ...]` | A set of instances including the "total" instance + ### Scraping at different frequencies If you would like to scrape some counters at a different frequency than others, @@ -31,27 +52,36 @@ receivers: windowsperfcounters/memory: collection_interval: 30s counters: - - object: Memory - counters: - - Committed Bytes + - object: Memory + counters: + - Committed Bytes - windowsperfcounters/connections: + windowsperfcounters/processor: collection_interval: 1m counters: - - object: TCPv4 - counters: - - Connections Established + - object: "Processor" + instances: "*" + counters: + - "% Processor Time" + - object: "Processor" + instances: [1, 2] + counters: + - "% Idle Time" service: pipelines: metrics: - receivers: [windowsperfcounters/memory, windowsperfcounters/connections] + receivers: [windowsperfcounters/memory, windowsperfcounters/processor] ``` ### Changing metric format To report metrics in the desired output format, it's recommended you use this -receiver with the metrics transform processor, e.g.: +receiver with the [metrics transform +processor](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/master/processor/metricstransformprocessor). + +e.g. To output the `Memory/Committed Bytes` counter as a metric with the name +`system.memory.usage`: ```yaml receivers: @@ -65,7 +95,6 @@ receivers: processors: metricstransformprocessor: transforms: - # rename "Memory/Committed Bytes" -> system.memory.usage - metric_name: "Memory/Committed Bytes" action: update new_name: system.memory.usage diff --git a/receiver/windowsperfcountersreceiver/config.go b/receiver/windowsperfcountersreceiver/config.go index 690e403e62cb..74bc5f11ec11 100644 --- a/receiver/windowsperfcountersreceiver/config.go +++ b/receiver/windowsperfcountersreceiver/config.go @@ -22,7 +22,7 @@ import ( "go.opentelemetry.io/collector/receiver/receiverhelper" ) -// Config defines configuration for HostMetrics receiver. +// Config defines configuration for WindowsPerfCounters receiver. type Config struct { configmodels.ReceiverSettings `mapstructure:",squash"` receiverhelper.ScraperControllerSettings `mapstructure:",squash"` @@ -30,12 +30,16 @@ type Config struct { PerfCounters []PerfCounterConfig `mapstructure:"perfcounters"` } +// PerfCounterConfig defines configuration for a perf counter object. type PerfCounterConfig struct { - Object string `mapstructure:"object"` - Counters []string `mapstructure:"counters"` + Object string `mapstructure:"object"` + Instances []string `mapstructure:"instances"` + Counters []string `mapstructure:"counters"` } func (c *Config) validate() error { + // TODO: consider validating duplicate configuration of counters + var errors []error if c.CollectionInterval <= 0 { @@ -53,6 +57,13 @@ func (c *Config) validate() error { continue } + for _, instance := range pc.Instances { + if instance == "" { + errors = append(errors, fmt.Errorf("perf counter for object %q includes an empty instance", pc.Object)) + break + } + } + if len(pc.Counters) == 0 { errors = append(errors, fmt.Errorf("perf counter for object %q does not specify any counters", pc.Object)) } diff --git a/receiver/windowsperfcountersreceiver/config_test.go b/receiver/windowsperfcountersreceiver/config_test.go index 2c0040456f65..b90faff88638 100644 --- a/receiver/windowsperfcountersreceiver/config_test.go +++ b/receiver/windowsperfcountersreceiver/config_test.go @@ -84,6 +84,7 @@ func TestLoadConfig_Error(t *testing.T) { noPerfCountersErr = "must specify at least one perf counter" noObjectNameErr = "must specify object name for all perf counters" noCountersErr = `perf counter for object "%s" does not specify any counters` + emptyInstanceErr = `perf counter for object "%s" includes an empty instance` ) testCases := []testCase{ @@ -108,9 +109,21 @@ func TestLoadConfig_Error(t *testing.T) { expectedErr: fmt.Sprintf("%s: %s", errorPrefix, fmt.Sprintf(noCountersErr, "object")), }, { - name: "AllErrors", - cfgFile: "config-allerrors.yaml", - expectedErr: fmt.Sprintf("%s: [%s; %s; %s]", errorPrefix, negativeCollectionIntervalErr, fmt.Sprintf(noCountersErr, "object"), noObjectNameErr), + name: "EmptyInstance", + cfgFile: "config-emptyinstance.yaml", + expectedErr: fmt.Sprintf("%s: %s", errorPrefix, fmt.Sprintf(emptyInstanceErr, "object")), + }, + { + name: "AllErrors", + cfgFile: "config-allerrors.yaml", + expectedErr: fmt.Sprintf( + "%s: [%s; %s; %s; %s]", + errorPrefix, + negativeCollectionIntervalErr, + fmt.Sprintf(emptyInstanceErr, "object"), + fmt.Sprintf(noCountersErr, "object"), + noObjectNameErr, + ), }, } diff --git a/receiver/windowsperfcountersreceiver/config_windows.go b/receiver/windowsperfcountersreceiver/config_windows.go new file mode 100644 index 000000000000..5ab3fdeb0421 --- /dev/null +++ b/receiver/windowsperfcountersreceiver/config_windows.go @@ -0,0 +1,31 @@ +// Copyright The 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 windowsperfcountersreceiver + +func (pc *PerfCounterConfig) instances() []string { + if len(pc.Instances) == 0 { + return []string{""} + } + + for _, instance := range pc.Instances { + if instance == "*" { + return []string{"*"} + } + } + + return pc.Instances +} diff --git a/receiver/windowsperfcountersreceiver/example_config.yaml b/receiver/windowsperfcountersreceiver/example_config.yaml index e34d0642ca4b..5fc4c959e280 100644 --- a/receiver/windowsperfcountersreceiver/example_config.yaml +++ b/receiver/windowsperfcountersreceiver/example_config.yaml @@ -9,6 +9,14 @@ receivers: - object: "Memory" counters: - "Committed Bytes" + - object: "Processor" + instances: "*" + counters: + - "% Processor Time" + - object: "Processor" + instances: [1, 2] + counters: + - "% Idle Time" exporters: logging: diff --git a/receiver/windowsperfcountersreceiver/testdata/config-allerrors.yaml b/receiver/windowsperfcountersreceiver/testdata/config-allerrors.yaml index 18ce54a632d3..e19c8cb2f8d1 100644 --- a/receiver/windowsperfcountersreceiver/testdata/config-allerrors.yaml +++ b/receiver/windowsperfcountersreceiver/testdata/config-allerrors.yaml @@ -3,7 +3,8 @@ receivers: collection_interval: -1m perfcounters: - - - Object: "object" + - object: "object" + instances: [ "instance", "", "*" ] processors: exampleprocessor: diff --git a/receiver/windowsperfcountersreceiver/testdata/config-emptyinstance.yaml b/receiver/windowsperfcountersreceiver/testdata/config-emptyinstance.yaml new file mode 100644 index 000000000000..5bada836124d --- /dev/null +++ b/receiver/windowsperfcountersreceiver/testdata/config-emptyinstance.yaml @@ -0,0 +1,20 @@ +receivers: + windowsperfcounters: + perfcounters: + - object: "object" + instances: [""] + counters: + - "counter" + +processors: + exampleprocessor: + +exporters: + exampleexporter: + +service: + pipelines: + metrics: + receivers: [windowsperfcounters] + processors: [exampleprocessor] + exporters: [exampleexporter] diff --git a/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper.go b/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper.go index 461ae46c7245..e897fe28ea89 100644 --- a/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper.go +++ b/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper.go @@ -58,12 +58,16 @@ func (s *scraper) initialize(ctx context.Context) error { var errors []error for _, perfCounterCfg := range s.cfg.PerfCounters { - for _, counterName := range perfCounterCfg.Counters { - c, err := pdh.NewPerfCounter(fmt.Sprintf("\\%s\\%s", perfCounterCfg.Object, counterName), true) - if err != nil { - errors = append(errors, err) - } else { - s.counters = append(s.counters, c) + for _, instance := range perfCounterCfg.instances() { + for _, counterName := range perfCounterCfg.Counters { + counterPath := counterPath(perfCounterCfg.Object, instance, counterName) + + c, err := pdh.NewPerfCounter(counterPath, true) + if err != nil { + errors = append(errors, fmt.Errorf("error initializing counter %v: %w", counterPath, err)) + } else { + s.counters = append(s.counters, c) + } } } } @@ -71,6 +75,14 @@ func (s *scraper) initialize(ctx context.Context) error { return componenterror.CombineErrors(errors) } +func counterPath(object, instance, counterName string) string { + if instance != "" { + instance = fmt.Sprintf("(%s)", instance) + } + + return fmt.Sprintf("\\%s%s\\%s", object, instance, counterName) +} + func (s *scraper) close(ctx context.Context) error { var errors []error diff --git a/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper_test.go b/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper_test.go index 384ba9baa102..b90130ee2090 100644 --- a/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper_test.go +++ b/receiver/windowsperfcountersreceiver/windowsperfcounters_scraper_test.go @@ -30,17 +30,18 @@ import ( ) type mockPerfCounter struct { + path string scrapeErr error closeErr error } -func newMockPerfCounter(scrapeErr, closeErr error) *mockPerfCounter { - return &mockPerfCounter{scrapeErr: scrapeErr, closeErr: closeErr} +func newMockPerfCounter(path string, scrapeErr, closeErr error) *mockPerfCounter { + return &mockPerfCounter{path: path, scrapeErr: scrapeErr, closeErr: closeErr} } // Path func (mpc *mockPerfCounter) Path() string { - return "" + return mpc.path } // ScrapeData @@ -54,16 +55,22 @@ func (mpc *mockPerfCounter) Close() error { } func Test_WindowsPerfCounterScraper(t *testing.T) { + type expectedMetric struct { + name string + instanceLabelValues []string + } + type testCase struct { name string cfg *Config - newErr string - initializeErr string - scrapeErr error - closeErr error + newErr string + mockCounterPath string + initializeErr string + scrapeErr error + closeErr error - expectedMetrics int + expectedMetrics []expectedMetric } defaultConfig := &Config{ @@ -75,8 +82,21 @@ func Test_WindowsPerfCounterScraper(t *testing.T) { testCases := []testCase{ { - name: "Standard", - expectedMetrics: 1, + name: "Standard", + cfg: &Config{ + PerfCounters: []PerfCounterConfig{ + {Object: "Memory", Counters: []string{"Committed Bytes"}}, + {Object: "Processor", Instances: []string{"*"}, Counters: []string{"% Processor Time"}}, + {Object: "Processor", Instances: []string{"1", "2"}, Counters: []string{"% Idle Time"}}, + }, + ScraperControllerSettings: receiverhelper.ScraperControllerSettings{CollectionInterval: time.Minute}, + }, + expectedMetrics: []expectedMetric{ + {name: `\Memory\Committed Bytes`}, + {name: `\Processor(*)\% Processor Time`, instanceLabelValues: []string{"*"}}, + {name: `\Processor(1)\% Idle Time`, instanceLabelValues: []string{"1"}}, + {name: `\Processor(2)\% Idle Time`, instanceLabelValues: []string{"2"}}, + }, }, { name: "InvalidCounter", @@ -93,7 +113,7 @@ func Test_WindowsPerfCounterScraper(t *testing.T) { }, ScraperControllerSettings: receiverhelper.ScraperControllerSettings{CollectionInterval: time.Minute}, }, - initializeErr: "The specified object was not found on the computer.\r\n", + initializeErr: "error initializing counter \\Invalid Object\\Invalid Counter: The specified object was not found on the computer.\r\n", }, { name: "ScrapeError", @@ -101,7 +121,7 @@ func Test_WindowsPerfCounterScraper(t *testing.T) { }, { name: "CloseError", - expectedMetrics: 1, + expectedMetrics: []expectedMetric{{name: ""}}, closeErr: errors.New("err1"), }, } @@ -125,9 +145,9 @@ func Test_WindowsPerfCounterScraper(t *testing.T) { } require.NoError(t, err) - if test.scrapeErr != nil || test.closeErr != nil { + if test.mockCounterPath != "" || test.scrapeErr != nil || test.closeErr != nil { for i := range scraper.counters { - scraper.counters[i] = newMockPerfCounter(test.scrapeErr, test.closeErr) + scraper.counters[i] = newMockPerfCounter(test.mockCounterPath, test.scrapeErr, test.closeErr) } } @@ -138,7 +158,47 @@ func Test_WindowsPerfCounterScraper(t *testing.T) { require.NoError(t, err) } - assert.Equal(t, test.expectedMetrics, metrics.Len()) + require.Equal(t, len(test.expectedMetrics), metrics.Len()) + for i, e := range test.expectedMetrics { + metric := metrics.At(i) + assert.Equal(t, e.name, metric.Name()) + + ddp := metric.DoubleGauge().DataPoints() + + var allInstances bool + for _, v := range e.instanceLabelValues { + if v == "*" { + allInstances = true + break + } + } + + if allInstances { + require.GreaterOrEqual(t, ddp.Len(), 1) + } else { + expectedDataPoints := 1 + if len(e.instanceLabelValues) > 0 { + expectedDataPoints = len(e.instanceLabelValues) + } + + require.Equal(t, expectedDataPoints, ddp.Len()) + } + + if len(e.instanceLabelValues) > 0 { + instanceLabelValues := make([]string, 0, ddp.Len()) + for i := 0; i < ddp.Len(); i++ { + instanceLabelValue, ok := ddp.At(i).LabelsMap().Get(instanceLabelName) + require.Truef(t, ok, "data point was missing %q label", instanceLabelName) + instanceLabelValues = append(instanceLabelValues, instanceLabelValue) + } + + if !allInstances { + for _, v := range e.instanceLabelValues { + assert.Contains(t, instanceLabelValues, v) + } + } + } + } err = scraper.close(context.Background()) if test.closeErr != nil {