Skip to content

Commit

Permalink
[receiver/hostmetrics] Add an optional root_path configuration (#16026)
Browse files Browse the repository at this point in the history
- Make the receiver configurable to be aware of the root filesystem, for Linux only.
- Give the ability to the filesystem scraper to translate mountpoints between host and container
mountpoint attribute emitted without the root path prefix, i.e. from the host's perspective
- No longer requires specification of where to find the mountinfo file through env var in order to avoid errors.
- All scrapers can see RootPath - useful for other scrapers e.g. pagingscraper as discussed in Ability to scrape filesystem host metrics from a container
- Set the environment variables for the user if they are not set.
- Validate that the root_path aligns with the gopsutil env vars (if they are previously set).
  • Loading branch information
jamesmoessis authored Nov 8, 2022
1 parent 9f68e4e commit eee0daf
Show file tree
Hide file tree
Showing 24 changed files with 405 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .chloggen/root-path-1.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
change_type: enhancement
component: hostmetricsreceiver
note: "Added `root_path` config option, allowing the user to specify where the host filesystem is."
issues: [5879, 16026]
30 changes: 29 additions & 1 deletion receiver/hostmetricsreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ deployed as an agent.

## Getting Started

The collection interval and the categories of metrics to be scraped can be
The collection interval, root path, and the categories of metrics to be scraped can be
configured:

```yaml
hostmetrics:
collection_interval: <duration> # default = 1m
root_path: <string>
scrapers:
<scraper1>:
<scraper2>:
Expand Down Expand Up @@ -141,6 +142,33 @@ service:
receivers: [hostmetrics, hostmetrics/disk]
```

### Collecting host metrics from inside a container (Linux only)

Host metrics are collected from the Linux system directories on the filesystem.
You likely want to collect metrics about the host system and not the container.
This is achievable by following these steps:

#### 1. Bind mount the host filesystem

The simplest configuration is to mount the entire host filesystem when running
the container. e.g. `docker run -v /:/hostfs ...`.

You can also choose which parts of the host filesystem to mount, if you know
exactly what you'll need. e.g. `docker run -v /proc:/hostfs/proc`.

#### 2. Configure `root_path`

Configure `root_path` so the hostmetrics receiver knows where the root filesystem is.
Note: if running multiple instances of the host metrics receiver, they must all have
the same `root_path`.

Example:
```yaml
receivers:
hostmetrics:
root_path: /hostfs
```

## Resource attributes

Currently, the hostmetrics receiver does not set any Resource attributes on the exported metrics. However, if you want to set Resource attributes, you can provide them via environment variables via the [resourcedetection](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/resourcedetectionprocessor#environment-variable) processor. For example, you can add the following resource attributes to adhere to [Resource Semantic Conventions](https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/):
Expand Down
12 changes: 9 additions & 3 deletions receiver/hostmetricsreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/receiver/scraperhelper"
"go.uber.org/multierr"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal"
)
Expand All @@ -33,18 +34,21 @@ const (
type Config struct {
scraperhelper.ScraperControllerSettings `mapstructure:",squash"`
Scrapers map[string]internal.Config `mapstructure:"-"`
// RootPath is the host's root directory (linux only).
RootPath string `mapstructure:"root_path"`
}

var _ config.Receiver = (*Config)(nil)
var _ confmap.Unmarshaler = (*Config)(nil)

// Validate checks the receiver configuration is valid
func (cfg *Config) Validate() error {
var err error
if len(cfg.Scrapers) == 0 {
return errors.New("must specify at least one scraper when using hostmetrics receiver")
err = multierr.Append(err, errors.New("must specify at least one scraper when using hostmetrics receiver"))
}

return nil
err = multierr.Append(err, validateRootPath(cfg.RootPath, &osEnv{}))
return err
}

// Unmarshal a config.Parser into the config struct.
Expand Down Expand Up @@ -84,6 +88,8 @@ func (cfg *Config) Unmarshal(componentParser *confmap.Conf) error {
return fmt.Errorf("error reading settings for scraper type %q: %w", key, err)
}

collectorCfg.SetRootPath(cfg.RootPath)

cfg.Scrapers[key] = collectorCfg
}

Expand Down
22 changes: 22 additions & 0 deletions receiver/hostmetricsreceiver/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package hostmetricsreceiver // import "github.com/open-telemetry/opentelemetry-c
import (
"context"
"fmt"
"os"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config"
Expand Down Expand Up @@ -93,6 +94,10 @@ func createMetricsReceiver(
return nil, err
}

if err = setGoPsutilEnvVars(oCfg.RootPath, &osEnv{}); err != nil {
return nil, err
}

return scraperhelper.NewScraperControllerReceiver(
&oCfg.ScraperControllerSettings,
set,
Expand Down Expand Up @@ -137,3 +142,20 @@ func createHostMetricsScraper(ctx context.Context, set component.ReceiverCreateS
scraper, err = factory.CreateMetricsScraper(ctx, set, cfg)
return
}

type environment interface {
Lookup(k string) (string, bool)
Set(k, v string) error
}

type osEnv struct{}

var _ environment = (*osEnv)(nil)

func (e *osEnv) Set(k, v string) error {
return os.Setenv(k, v)
}

func (e *osEnv) Lookup(k string) (string, bool) {
return os.LookupEnv(k)
}
2 changes: 1 addition & 1 deletion receiver/hostmetricsreceiver/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
go.opentelemetry.io/collector v0.63.2-0.20221104003159-6b27644724d8
go.opentelemetry.io/collector/pdata v0.63.2-0.20221104003159-6b27644724d8
go.opentelemetry.io/collector/semconv v0.63.2-0.20221104003159-6b27644724d8
go.uber.org/multierr v1.8.0
go.uber.org/zap v1.23.0
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8

Expand Down Expand Up @@ -63,7 +64,6 @@ require (
go.opentelemetry.io/otel/sdk/metric v0.33.0 // indirect
go.opentelemetry.io/otel/trace v1.11.1 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
golang.org/x/text v0.4.0 // indirect
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
Expand Down
71 changes: 71 additions & 0 deletions receiver/hostmetricsreceiver/hostmetrics_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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.

//go:build linux

package hostmetricsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver"

import (
"fmt"
"os"
"path/filepath"
)

var gopsutilEnvVars = map[string]string{
"HOST_PROC": "/proc",
"HOST_SYS": "/sys",
"HOST_ETC": "/etc",
"HOST_VAR": "/var",
"HOST_RUN": "/run",
"HOST_DEV": "/dev",
}

// This exists to validate that different instances of the hostmetricsreceiver do not
// have inconsistent root_path configurations. The root_path is passed down to gopsutil
// through env vars, so it must be consistent across the process.
var globalRootPath string

func validateRootPath(rootPath string, env environment) error {
if rootPath == "" || rootPath == "/" {
return nil
}

if globalRootPath != "" && rootPath != globalRootPath {
return fmt.Errorf("inconsistent root_path configuration detected between hostmetricsreceivers: `%s` != `%s`", globalRootPath, rootPath)
}
globalRootPath = rootPath

if _, err := os.Stat(rootPath); err != nil {
return fmt.Errorf("invalid root_path: %w", err)
}

return nil
}

func setGoPsutilEnvVars(rootPath string, env environment) error {
if rootPath == "" || rootPath == "/" {
return nil
}

for envVarKey, defaultValue := range gopsutilEnvVars {
_, ok := env.Lookup(envVarKey)
if ok {
continue // don't override if existing env var is set
}
if err := env.Set(envVarKey, filepath.Join(rootPath, defaultValue)); err != nil {
return err
}
}
return nil
}
79 changes: 79 additions & 0 deletions receiver/hostmetricsreceiver/hostmetrics_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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.

//go:build linux

package hostmetricsreceiver

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/collector/component/componenttest"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/service/servicetest"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal"
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver/internal/scraper/cpuscraper"
)

func TestConsistentRootPaths(t *testing.T) {
env := &testEnv{env: map[string]string{"HOST_PROC": "testdata"}}
// use testdata because it's a directory that exists - don't actually use any files in it
assert.Nil(t, testValidate("testdata", env))
assert.Nil(t, testValidate("", env))
assert.Nil(t, testValidate("/", env))
}

func TestInconsistentRootPaths(t *testing.T) {
globalRootPath = "foo"
err := testValidate("testdata", &testEnv{})
assert.EqualError(t, err, "inconsistent root_path configuration detected between hostmetricsreceivers: `foo` != `testdata`")
}

func TestLoadConfigRootPath(t *testing.T) {
t.Setenv("HOST_PROC", "testdata")
factories, _ := componenttest.NopFactories()
factory := NewFactory()
factories.Receivers[typeStr] = factory
cfg, err := servicetest.LoadConfigAndValidate(filepath.Join("testdata", "config-root-path.yaml"), factories)
require.NoError(t, err)
globalRootPath = ""

r := cfg.Receivers[config.NewComponentID(typeStr)].(*Config)
expectedConfig := factory.CreateDefaultConfig().(*Config)
expectedConfig.RootPath = "testdata"
cpuScraperCfg := (&cpuscraper.Factory{}).CreateDefaultConfig()
cpuScraperCfg.SetRootPath("testdata")
expectedConfig.Scrapers = map[string]internal.Config{cpuscraper.TypeStr: cpuScraperCfg}

assert.Equal(t, expectedConfig, r)
}

func TestLoadInvalidConfig_RootPathNotExist(t *testing.T) {
factories, _ := componenttest.NopFactories()
factory := NewFactory()
factories.Receivers[typeStr] = factory
_, err := servicetest.LoadConfigAndValidate(filepath.Join("testdata", "config-bad-root-path.yaml"), factories)
assert.ErrorContains(t, err, "invalid root_path:")
globalRootPath = ""
}

func testValidate(rootPath string, env environment) error {
err := validateRootPath(rootPath, env)
globalRootPath = ""
return err
}
30 changes: 30 additions & 0 deletions receiver/hostmetricsreceiver/hostmetrics_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.

//go:build !linux

package hostmetricsreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/hostmetricsreceiver"

import "fmt"

func validateRootPath(rootPath string, _ environment) error {
if rootPath == "" {
return nil
}
return fmt.Errorf("root_path is supported on linux only")
}

func setGoPsutilEnvVars(_ string, _ environment) error {
return nil
}
31 changes: 31 additions & 0 deletions receiver/hostmetricsreceiver/hostmetrics_others_test.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.

//go:build !linux

package hostmetricsreceiver

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRootPathNotAllowedOnOS(t *testing.T) {
assert.NotNil(t, validateRootPath("testdata", &testEnv{}))
}

func TestRootPathUnset(t *testing.T) {
assert.Nil(t, validateRootPath("", &testEnv{}))
}
18 changes: 18 additions & 0 deletions receiver/hostmetricsreceiver/hostmetrics_receiver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ var factories = map[string]internal.ScraperFactory{
processscraper.TypeStr: &processscraper.Factory{},
}

type testEnv struct {
env map[string]string
}

var _ environment = (*testEnv)(nil)

func (e *testEnv) Lookup(k string) (string, bool) {
v, ok := e.env[k]
return v, ok
}

func (e *testEnv) Set(k, v string) error {
e.env[k] = v
return nil
}

func TestGatherMetrics_EndToEnd(t *testing.T) {
scraperFactories = factories

Expand Down Expand Up @@ -208,6 +224,8 @@ const mockTypeStr = "mock"

type mockConfig struct{}

func (m *mockConfig) SetRootPath(_ string) {}

type mockFactory struct{ mock.Mock }
type mockScraper struct{ mock.Mock }

Expand Down
Loading

0 comments on commit eee0daf

Please sign in to comment.