Skip to content

Commit

Permalink
[ Chrony Receiver ] Part 1 - Adding initial component (#12101)
Browse files Browse the repository at this point in the history
[ Chrony Receiver ] Adding initial component

Adding in the conguration of the Chrony Receiver to be added along with
the metadata metric configuration.
  • Loading branch information
MovieStoreGuy authored Jul 19, 2022
1 parent a8b5fd0 commit 069d123
Show file tree
Hide file tree
Showing 22 changed files with 1,731 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ receiver/awsfirehosereceiver/ @open-telemetry/collector-c
receiver/awsxrayreceiver/ @open-telemetry/collector-contrib-approvers @willarmiros
receiver/bigipreceiver/ @open-telemetry/collector-contrib-approvers @djaglowski @StefanKurek
receiver/carbonreceiver/ @open-telemetry/collector-contrib-approvers @pjanotti
receiver/chronyreceiver/ @open-telemetry/collector-contrib-approvers @MovieStoreGuy @jamesmoessis
receiver/cloudfoundryreceiver/ @open-telemetry/collector-contrib-approvers @agoallikmaa @pellared @crobert-1
receiver/collectdreceiver/ @open-telemetry/collector-contrib-approvers @owais
receiver/couchdbreceiver/ @open-telemetry/collector-contrib-approvers @djaglowski
Expand Down
1 change: 1 addition & 0 deletions receiver/chronyreceiver/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include ../../Makefile.Common
64 changes: 64 additions & 0 deletions receiver/chronyreceiver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Chrony Receiver

| Status | |
| ------------------------ | --------- |
| Stability | [alpha] |
| Supported pipeline types | metrics |
| Distributions | [contrib] |

The [chrony] receiver is a pure go implementation of the command `chronyc tracking` to allow for
portability across systems and platforms. All of the data that would typically be captured by
the tracking command is made available in this receiver, see [documentation](./documentation.md) for
more details.

## Configuration

### Default

By default, the `chrony` receiver will default to the following configuration:

```yaml
chrony/defaults:
address: unix:///var/run/chrony/chronyd.sock # The default port by chronyd to allow cmd access
timeout: 10s # Allowing at least 10s for chronyd to respond before giving up

chrony:
# This will result in the same configuration as above
```

### Customised

The following options can be customised:

- address (required) - the address on where to communicate to `chronyd`
- The allowed formats are the following
- udp://hostname:port
- unix:///path/to/chrony.sock (Please note the triple slash)
- timeout (optional) - The total amount of time allowed to read and process the data from chronyd
- Recommendation: This value should be set above 1s to allow `chronyd` time to respond
- collection_interval (optional) - how frequent this receiver should poll [chrony]
- metrics (optional) - Which metrics should be exported, read the [documentation] for complete details

## Example

An example of the configuration is:

```yaml
receivers:
chrony:
address: unix:///var/run/chrony/chronyd.sock
timeout: 10s
collection_interval: 30s
metrics:
ntp.skew:
enabled: true
ntp.stratum:
enabled: true
```
The complete list of metrics emitted by this receiver is found in the [documentation].
[alpha]: https://github.com/open-telemetry/opentelemetry-collector#alpha
[contrib]: https://github.com/open-telemetry/opentelemetry-collector-releases/tree/main/distributions/otelcol-contrib
[documentation]: ./documentation.md
[chrony]: https://chrony.tuxfamily.org/
66 changes: 66 additions & 0 deletions receiver/chronyreceiver/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 chronyreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/chronyreceiver"

import (
"errors"
"fmt"
"time"

"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/receiver/scraperhelper"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/chronyreceiver/internal/chrony"
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/chronyreceiver/internal/metadata"
)

type Config struct {
scraperhelper.ScraperControllerSettings `mapstructure:",squash"`
metadata.MetricsSettings `mapstructure:"metrics"`
// Endpoint is the published address or unix socket
// that allows clients to connect to:
// The allowed format is:
// unix:///path/to/chronyd/unix.sock
// udp://localhost:323
//
// The default value is unix:///var/run/chrony/chronyd.sock
Endpoint string `mapstructure:"endpoint"`
// Timeout controls the max time allowed to read data from chronyd
Timeout time.Duration `mapstructure:"timeout"`
}

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

errInvalidValue = errors.New("invalid value")
)

func newDefaultCongfig() config.Receiver {
return &Config{
ScraperControllerSettings: scraperhelper.NewDefaultScraperControllerSettings(typeStr),
MetricsSettings: metadata.DefaultMetricsSettings(),

Endpoint: "unix:///var/run/chrony/chronyd.sock",
Timeout: 10 * time.Second,
}
}

func (c *Config) Validate() error {
if c.Timeout < 1 {
return fmt.Errorf("must have a positive timeout: %w", errInvalidValue)
}
_, _, err := chrony.SplitNetworkEndpoint(c.Endpoint)
return err
}
121 changes: 121 additions & 0 deletions receiver/chronyreceiver/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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 chronyreceiver

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

"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/receiver/scraperhelper"
"go.opentelemetry.io/collector/service/servicetest"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/chronyreceiver/internal/chrony"
"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/chronyreceiver/internal/metadata"
)

func TestLoadConfig(t *testing.T) {
t.Parallel()

factories, err := componenttest.NopFactories()
require.NoError(t, err, "Must not error on creating Nop factories")

factory := NewFactory()
factories.Receivers[typeStr] = factory

cfg, err := servicetest.LoadConfigAndValidate(path.Join("testdata", "config.yml"), factories)
require.NoError(t, err, "Must not error when loading configuration")

chronyConf := cfg.Receivers[config.NewComponentIDWithName(typeStr, "custom")]
expect := &Config{
ScraperControllerSettings: scraperhelper.NewDefaultScraperControllerSettings(typeStr),
MetricsSettings: metadata.DefaultMetricsSettings(),
Endpoint: "udp://localhost:3030",
Timeout: 10 * time.Second,
}
expect.SetIDName("custom")
assert.Equal(t, expect, chronyConf)
}

func TestValidate(t *testing.T) {
t.Parallel()

tests := []struct {
scenario string
conf Config
err error
}{
{
scenario: "Valid udp configuration",
conf: Config{
Endpoint: "udp://localhost:323",
Timeout: 10 * time.Second,
},
err: nil,
},
{
scenario: "Invalid udp hostname",
conf: Config{
Endpoint: "udp://:323",
Timeout: 10 * time.Second,
},
err: chrony.ErrInvalidNetwork,
},
{
scenario: "Invalid udp port",
conf: Config{
Endpoint: "udp://localhost",
Timeout: 10 * time.Second,
},
err: chrony.ErrInvalidNetwork,
},
{
scenario: "Valid unix path",
conf: Config{
Endpoint: fmt.Sprintf("unix://%s", t.TempDir()),
Timeout: 10 * time.Second,
},
err: nil,
},
{
scenario: "Invalid unix path",
conf: Config{
Endpoint: "unix:///no/dir/to/socket",
Timeout: 10 * time.Second,
},
err: os.ErrNotExist,
},
{
scenario: "Invalid timeout set",
conf: Config{
Endpoint: "unix://no/dir/to/socket",
Timeout: 0,
},
err: errInvalidValue,
},
}

for _, tc := range tests {
t.Run(tc.scenario, func(t *testing.T) {
assert.ErrorIs(t, tc.conf.Validate(), tc.err, "Must match the expected error")
})
}
}
16 changes: 16 additions & 0 deletions receiver/chronyreceiver/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// 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:generate mdatagen --experimental-gen metadata.yaml
package chronyreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/chronyreceiver"
32 changes: 32 additions & 0 deletions receiver/chronyreceiver/documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[comment]: <> (Code generated by mdatagen. DO NOT EDIT.)

# chrony receiver

## Metrics

These are the metrics available for this scraper.

| Name | Description | Unit | Type | Attributes |
| ---- | ----------- | ---- | ---- | ---------- |
| ntp.frequency.offset | The frequency is the rate by which the system s clock would be wrong if chronyd was not correcting it. It is expressed in ppm (parts per million). For example, a value of 1 ppm would mean that when the system’s clock thinks it has advanced 1 second, it has actually advanced by 1.000001 seconds relative to true time. | ppm | Gauge(Double) | <ul> <li>leap.status</li> </ul> |
| **ntp.skew** | This is the estimated error bound on the frequency. | ppm | Gauge(Double) | <ul> </ul> |
| ntp.stratum | The number of hops away from the reference system keeping the reference time To read further, refer to https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/ch-configuring_ntp_using_the_chrony_suite#sect-Checking_chrony_tracking | {count} | Gauge(Int) | <ul> </ul> |
| **ntp.time.correction** | The number of seconds difference between the system's clock and the reference clock | seconds | Gauge(Double) | <ul> <li>leap.status</li> </ul> |
| **ntp.time.last_offset** | The estimated local offset on the last clock update | seconds | Gauge(Double) | <ul> <li>leap.status</li> </ul> |
| ntp.time.rms_offset | the long term average of the offset value | seconds | Gauge(Double) | <ul> <li>leap.status</li> </ul> |
| ntp.time.root_delay | This is the total of the network path delays to the stratum-1 system from which the system is ultimately synchronised. | seconds | Gauge(Double) | <ul> <li>leap.status</li> </ul> |

**Highlighted metrics** are emitted by default. Other metrics are optional and not emitted by default.
Any metric can be enabled or disabled with the following scraper configuration:

```yaml
metrics:
<metric_name>:
enabled: <true|false>
```
## Metric attributes
| Name | Description | Values |
| ---- | ----------- | ------ |
| leap.status | how the chrony is handling leap seconds | normal, insert_second, delete_second, unsynchronised |
63 changes: 63 additions & 0 deletions receiver/chronyreceiver/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 chronyreceiver // import "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/chronyreceiver"

import (
"context"
"fmt"

"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/config"
"go.opentelemetry.io/collector/consumer"
"go.opentelemetry.io/collector/receiver/scraperhelper"
)

const (
typeStr = "chrony"
)

func NewFactory() component.ReceiverFactory {
return component.NewReceiverFactory(
typeStr,
newDefaultCongfig,
component.WithMetricsReceiver(newMetricsReceiver),
)
}

func newMetricsReceiver(
ctx context.Context,
set component.ReceiverCreateSettings,
rCfg config.Receiver,
consumer consumer.Metrics) (component.MetricsReceiver, error) {
cfg, ok := rCfg.(*Config)
if !ok {
return nil, fmt.Errorf("wrong config provided: %w", errInvalidValue)
}

scraper, err := scraperhelper.NewScraper(
typeStr,
newScraper(ctx, cfg, set).scrape,
)
if err != nil {
return nil, err
}

return scraperhelper.NewScraperControllerReceiver(
&cfg.ScraperControllerSettings,
set,
consumer,
scraperhelper.AddScraper(scraper),
)
}
Loading

0 comments on commit 069d123

Please sign in to comment.