Skip to content

Commit

Permalink
2/3 Add Windows Performance Counters Receiver skeleton implementation (
Browse files Browse the repository at this point in the history
…#1211)

Adds skeleton for Windows Performance Counters Receiver with very basic initial functionality that can read simple counters that do not include any instance into a Gauge metric - see the added README.md for more details.

**Link to tracking Issue:** #1088

Note the scaffolding code makes use of the new `receiverhelper` & `scraperhelper` functions added in the Core PRs referenced below.

**Depends on:** #1175, open-telemetry/opentelemetry-collector#1886, open-telemetry/opentelemetry-collector#1890
  • Loading branch information
james-bebbington authored Oct 30, 2020
1 parent f000242 commit 9b9dd09
Show file tree
Hide file tree
Showing 22 changed files with 2,605 additions and 7 deletions.
2 changes: 1 addition & 1 deletion receiver/windowsperfcountersreceiver/Makefile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
include ../../Makefile.Common
include ../../Makefile.Common
88 changes: 88 additions & 0 deletions receiver/windowsperfcountersreceiver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 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'.

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).

## Configuration

The collection interval and the list of performance counters to be scraped can
be configured:

```yaml
windowsperfcounters:
collection_interval: <duration> # default = "1m"
counters:
- object: <object name>
counters:
- <counter name>
```
### Scraping at different frequencies
If you would like to scrape some counters at a different frequency than others,
you can configure multiple `windowsperfcounters` receivers with different
`collection_interval` values. For example:

```yaml
receivers:
windowsperfcounters/memory:
collection_interval: 30s
counters:
- object: Memory
counters:
- Committed Bytes
windowsperfcounters/connections:
collection_interval: 1m
counters:
- object: TCPv4
counters:
- Connections Established
service:
pipelines:
metrics:
receivers: [windowsperfcounters/memory, windowsperfcounters/connections]
```

### 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.:

```yaml
receivers:
windowsperfcounters:
collection_interval: 30s
counters:
- object: Memory
counters:
- Committed Bytes
processors:
metricstransformprocessor:
transforms:
# rename "Memory/Committed Bytes" -> system.memory.usage
- metric_name: "Memory/Committed Bytes"
action: update
new_name: system.memory.usage
service:
pipelines:
metrics:
receivers: [windowsperfcounters]
processors: [metricstransformprocessor]
```

## Recommended configuration for common applications

### IIS

TODO

### SQL Server

TODO
66 changes: 66 additions & 0 deletions receiver/windowsperfcountersreceiver/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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.

package windowsperfcountersreceiver

import (
"fmt"

"go.opentelemetry.io/collector/component/componenterror"
"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/receiver/receiverhelper"
)

// Config defines configuration for HostMetrics receiver.
type Config struct {
configmodels.ReceiverSettings `mapstructure:",squash"`
receiverhelper.ScraperControllerSettings `mapstructure:",squash"`

PerfCounters []PerfCounterConfig `mapstructure:"perfcounters"`
}

type PerfCounterConfig struct {
Object string `mapstructure:"object"`
Counters []string `mapstructure:"counters"`
}

func (c *Config) validate() error {
var errors []error

if c.CollectionInterval <= 0 {
errors = append(errors, fmt.Errorf("collection_interval must be a positive duration"))
}

if len(c.PerfCounters) == 0 {
errors = append(errors, fmt.Errorf("must specify at least one perf counter"))
}

var perfCounterMissingObjectName bool
for _, pc := range c.PerfCounters {
if pc.Object == "" {
perfCounterMissingObjectName = true
continue
}

if len(pc.Counters) == 0 {
errors = append(errors, fmt.Errorf("perf counter for object %q does not specify any counters", pc.Object))
}
}

if perfCounterMissingObjectName {
errors = append(errors, fmt.Errorf("must specify object name for all perf counters"))
}

return componenterror.CombineErrors(errors)
}
129 changes: 129 additions & 0 deletions receiver/windowsperfcountersreceiver/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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.

package windowsperfcountersreceiver

import (
"fmt"
"path"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/config/configtest"
"go.opentelemetry.io/collector/receiver/receiverhelper"
)

func TestLoadConfig(t *testing.T) {
factories, err := componenttest.ExampleComponents()
require.NoError(t, err)

factory := NewFactory()
factories.Receivers[typeStr] = factory
cfg, err := configtest.LoadConfigFile(t, path.Join(".", "testdata", "config.yaml"), factories)

require.NoError(t, err)
require.NotNil(t, cfg)

assert.Equal(t, len(cfg.Receivers), 2)

r0 := cfg.Receivers["windowsperfcounters"]
defaultConfigSingleObject := factory.CreateDefaultConfig()
defaultConfigSingleObject.(*Config).PerfCounters = []PerfCounterConfig{{Object: "object", Counters: []string{"counter"}}}

assert.Equal(t, defaultConfigSingleObject, r0)

r1 := cfg.Receivers["windowsperfcounters/customname"].(*Config)
expectedConfig := &Config{
ReceiverSettings: configmodels.ReceiverSettings{
TypeVal: typeStr,
NameVal: "windowsperfcounters/customname",
},
ScraperControllerSettings: receiverhelper.ScraperControllerSettings{
CollectionInterval: 30 * time.Second,
},
PerfCounters: []PerfCounterConfig{
{
Object: "object1",
Counters: []string{"counter1"},
},
{
Object: "object2",
Counters: []string{"counter1", "counter2"},
},
},
}

assert.Equal(t, expectedConfig, r1)
}

func TestLoadConfig_Error(t *testing.T) {
type testCase struct {
name string
cfgFile string
expectedErr string
}

const (
errorPrefix = "error reading receivers configuration for windowsperfcounters"
negativeCollectionIntervalErr = "collection_interval must be a positive duration"
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`
)

testCases := []testCase{
{
name: "NegativeCollectionInterval",
cfgFile: "config-negative-collection-interval.yaml",
expectedErr: fmt.Sprintf("%s: %s", errorPrefix, negativeCollectionIntervalErr),
},
{
name: "NoPerfCounters",
cfgFile: "config-noperfcounters.yaml",
expectedErr: fmt.Sprintf("%s: %s", errorPrefix, noPerfCountersErr),
},
{
name: "NoObjectName",
cfgFile: "config-noobjectname.yaml",
expectedErr: fmt.Sprintf("%s: %s", errorPrefix, noObjectNameErr),
},
{
name: "NoCounters",
cfgFile: "config-nocounters.yaml",
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),
},
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
factories, err := componenttest.ExampleComponents()
require.NoError(t, err)

factory := NewFactory()
factories.Receivers[typeStr] = factory
_, err = configtest.LoadConfigFile(t, path.Join(".", "testdata", test.cfgFile), factories)

require.EqualError(t, err, test.expectedErr)
})
}
}
21 changes: 21 additions & 0 deletions receiver/windowsperfcountersreceiver/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 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.

// windowsperfcountersreceiver implements a collector receiver that
// collects the configured performance counter data at the configured
// collection interval and converts them into OTLP equivalent metric
// representations.
//
// This receiver is only compatible with Windows.
package windowsperfcountersreceiver
24 changes: 24 additions & 0 deletions receiver/windowsperfcountersreceiver/example_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
extensions:
zpages:
endpoint: 0.0.0.0:55679

receivers:
windowsperfcounters:
collection_interval: 1s
perfcounters:
- object: "Memory"
counters:
- "Committed Bytes"

exporters:
logging:
prometheus:
endpoint: 0.0.0.0:8889

service:
pipelines:
metrics:
receivers: [windowsperfcounters]
exporters: [prometheus, logging]

extensions: [zpages]
61 changes: 61 additions & 0 deletions receiver/windowsperfcountersreceiver/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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.

package windowsperfcountersreceiver

import (
"time"

"github.com/spf13/viper"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config/configmodels"
"go.opentelemetry.io/collector/receiver/receiverhelper"
)

// This file implements Factory for WindowsPerfCounters receiver.

// The value of "type" key in configuration.
const typeStr = "windowsperfcounters"

// NewFactory creates a new factory for windows perf counters receiver.
func NewFactory() component.ReceiverFactory {
return receiverhelper.NewFactory(
typeStr,
createDefaultConfig,
receiverhelper.WithMetrics(createMetricsReceiver),
receiverhelper.WithCustomUnmarshaler(customUnmarshaler))
}

// customUnmarshaler returns custom unmarshaler for this config.
func customUnmarshaler(componentViperSection *viper.Viper, intoCfg interface{}) error {
err := componentViperSection.Unmarshal(intoCfg)
if err != nil {
return err
}

return intoCfg.(*Config).validate()
}

// createDefaultConfig creates the default configuration for receiver.
func createDefaultConfig() configmodels.Receiver {
return &Config{
ReceiverSettings: configmodels.ReceiverSettings{
TypeVal: typeStr,
NameVal: typeStr,
},
ScraperControllerSettings: receiverhelper.ScraperControllerSettings{
CollectionInterval: time.Minute,
},
}
}
Loading

0 comments on commit 9b9dd09

Please sign in to comment.