Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3/3 Extend Windows Performance Counters Receiver to support counters with instances #1229

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions receiver/windowsperfcountersreceiver/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -16,10 +24,23 @@ windowsperfcounters:
collection_interval: <duration> # default = "1m"
counters:
- object: <object name>
instances: [<instance name>]*
counters:
- <counter name>
```
*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,
Expand All @@ -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:
Expand All @@ -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
Expand Down
17 changes: 14 additions & 3 deletions receiver/windowsperfcountersreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,24 @@ 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"`

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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should consider being able to collect certain counters from only certain instances in the config? Or would one do this by using different receiver configs? For example, a case where one is interested in collecting counterA and counterB from instanceA only, and counterA and counterC from instanceB only.

Copy link
Member Author

@james-bebbington james-bebbington Nov 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand you correctly, this is supported, as per the example here and test here. You can achieve what you want as there's no limitation that you can only specify an Object once. Let me know if I missed something there.

Aside: there's currently no validation to stop you setting up completely duplicate counters, although I did leave a comment here mentioning we should consider adding such validation in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, that's what I was referring to, thanks for pointing me to it!

Counters []string `mapstructure:"counters"`
}

func (c *Config) validate() error {
// TODO: consider validating duplicate configuration of counters

var errors []error

if c.CollectionInterval <= 0 {
Expand All @@ -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))
}
Expand Down
19 changes: 16 additions & 3 deletions receiver/windowsperfcountersreceiver/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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,
),
},
}

Expand Down
31 changes: 31 additions & 0 deletions receiver/windowsperfcountersreceiver/config_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
8 changes: 8 additions & 0 deletions receiver/windowsperfcountersreceiver/example_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ receivers:
collection_interval: -1m
perfcounters:
-
- Object: "object"
- object: "object"
instances: [ "instance", "", "*" ]

processors:
exampleprocessor:
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,31 @@ 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)
}
}
}
}

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

Expand Down
Loading