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

Created dry run flag for collector #6445

16 changes: 16 additions & 0 deletions .chloggen/add-dry-run-flag-validate-all-fields.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: service

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Added dry run flag to validate config file without running collector.

# One or more tracking issues or pull requests related to the change
issues: [4671]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
12 changes: 12 additions & 0 deletions otelcol/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@ func (col *Collector) reloadConfiguration(ctx context.Context) error {

// Run starts the collector according to the given configuration, and waits for it to complete.
// Consecutive calls to Run are not allowed, Run shouldn't be called once a collector is shut down.

func (col *Collector) DryRun(ctx context.Context) error {
cfg, err := col.set.ConfigProvider.Get(ctx, col.set.Factories)
if err != nil {
return fmt.Errorf("failed to get config: %w", err)
}

cfg.DryRunValidate()

return nil
}

func (col *Collector) Run(ctx context.Context) error {
if err := col.setupConfigurationComponents(ctx); err != nil {
col.setCollectorState(StateClosed)
Expand Down
2 changes: 2 additions & 0 deletions otelcol/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ func NewCommand(set CollectorSettings) *cobra.Command {
return col.Run(cmd.Context())
},
}

rootCmd.AddCommand(newValidateSubCommand(set))
rootCmd.AddCommand(newBuildSubCommand(set))
rootCmd.Flags().AddGoFlagSet(flagSet)
return rootCmd
Expand Down
6 changes: 3 additions & 3 deletions otelcol/command_components.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ type componentsOutput struct {
Extensions []component.Type
}

// newBuildSubCommand constructs a new cobra.Command sub command using the given CollectorSettings.
// newBuildSubCommand constructs a new components sub command using the given CollectorSettings.
func newBuildSubCommand(set CollectorSettings) *cobra.Command {
buildCmd := &cobra.Command{
componentsCmd := &cobra.Command{
Use: "components",
Short: "Outputs available components in this collector distribution",
Args: cobra.ExactArgs(0),
Expand Down Expand Up @@ -61,5 +61,5 @@ func newBuildSubCommand(set CollectorSettings) *cobra.Command {
return nil
},
}
return buildCmd
return componentsCmd
}
53 changes: 53 additions & 0 deletions otelcol/command_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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 otelcol // import "go.opentelemetry.io/collector/otelcol"

import (
"errors"

"github.com/spf13/cobra"
)

// newValidateSubCommand constructs a new validate sub command using the given CollectorSettings.
func newValidateSubCommand(set CollectorSettings) *cobra.Command {
flagSet := flags()
validateCmd := &cobra.Command{
Use: "validate",
Short: "Validates the config without running the collector",
RunE: func(cmd *cobra.Command, args []string) error {

if set.ConfigProvider == nil {
var err error

configFlags := getConfigFlag(flagSet)
if len(configFlags) == 0 {
return errors.New("at least one config flag must be provided")
}

set.ConfigProvider, err = NewConfigProvider(newDefaultConfigProviderSettings(configFlags))
if err != nil {
return err
}
}
col, err := NewCollector(set)
if err != nil {
return err
}
return col.DryRun(cmd.Context())
},
}
validateCmd.Flags().AddGoFlagSet(flagSet)
return validateCmd
}
50 changes: 50 additions & 0 deletions otelcol/command_validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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 otelcol // import "go.opentelemetry.io/collector/otelcol"

import (
"path/filepath"
"testing"

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

"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/collector/confmap/converter/expandconverter"
"go.opentelemetry.io/collector/confmap/provider/fileprovider"
"go.opentelemetry.io/collector/service/servicetest"
)

func TestNewValidateSubCommand(t *testing.T) {
factories, err := servicetest.NopFactories()
require.NoError(t, err)

cfgProvider, err := NewConfigProvider(
ConfigProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{filepath.Join("testdata", "otelcol-nop.yaml")},
Providers: map[string]confmap.Provider{"file": fileprovider.New()},
Converters: []confmap.Converter{expandconverter.New()},
},
})
require.NoError(t, err)

validateCmd := newValidateSubCommand(CollectorSettings{Factories: factories, ConfigProvider: cfgProvider})

assert.Equal(t, "validate", validateCmd.Use)
assert.Equal(t, "Validates the config without running the collector", validateCmd.Short)

require.NoError(t, validateCmd.Execute())
}
6 changes: 6 additions & 0 deletions service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ key:
2. Does not support setting a key that contains a equal sign `=`.
3. The configuration key separator inside the value part of the property is "::". For example `--set "name={a::b: c}"` is equivalent with `--set name.a.b=c`.

## How to validate configuration file and return all errors without running collector

```bash
./otelcorecol validate --config=file:examples/local/otel-config.yaml
```
## How to check components available in a distribution

Use the sub command build-info. Below is an example:
Expand Down Expand Up @@ -123,4 +128,5 @@ extensions:
- zpages
- memory_ballast


```
121 changes: 121 additions & 0 deletions service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,82 @@ func (cfg *Config) Validate() error {
return nil
}

func (cfg *Config) DryRunValidate() {
// Currently, there is no default receiver enabled.
// The configuration must specify at least one receiver to be valid.
if len(cfg.Receivers) == 0 {
fmt.Printf("**..%v\n", errMissingReceivers)
} else {
for recvID, recvCfg := range cfg.Receivers {
if err := component.ValidateConfig(recvCfg); err != nil {
fmt.Printf("**..receivers::%s: %v\n", recvID, err)
}
}
}

// Currently, there is no default exporter enabled.
// The configuration must specify at least one exporter to be valid.
if len(cfg.Exporters) == 0 {
fmt.Printf("**..%v\n", errMissingExporters)
} else {
for expID, expCfg := range cfg.Exporters {
if err := component.ValidateConfig(expCfg); err != nil {
fmt.Printf("**..exporters::%s: %v\n", expID, err)
}
}
}

// Validate the processor configuration.
for procID, procCfg := range cfg.Processors {
if err := component.ValidateConfig(procCfg); err != nil {
fmt.Printf("**..processors::%s: %v\n", procID, err)
}
}

// Validate the extension configuration.
for extID, extCfg := range cfg.Extensions {
if err := component.ValidateConfig(extCfg); err != nil {
fmt.Printf("**..extensions::%s: %v\n", extID, err)
}
}

cfg.Service.DryRunValidate()

// Check that all enabled extensions in the service are configured.
for _, ref := range cfg.Service.Extensions {
// Check that the name referenced in the Service extensions exists in the top-level extensions.
if cfg.Extensions[ref] == nil {
fmt.Printf("**..service::extensions: references extension %q which is not configured\n", ref)
}
}
// Check that all pipelines reference only configured components.
for pipelineID, pipeline := range cfg.Service.Pipelines {
// Validate pipeline receiver name references.
for _, ref := range pipeline.Receivers {
// Check that the name referenced in the pipeline's receivers exists in the top-level receivers.
if cfg.Receivers[ref] == nil {
fmt.Printf("**..service::pipeline::%s: references receiver %q which is not configured\n", pipelineID, ref)
}
}

// Validate pipeline processor name references.
for _, ref := range pipeline.Processors {
// Check that the name referenced in the pipeline's processors exists in the top-level processors.
if cfg.Processors[ref] == nil {
fmt.Printf("**..service::pipeline::%s: references processor %q which is not configured\n", pipelineID, ref)
}
}

// Validate pipeline exporter name references.
for _, ref := range pipeline.Exporters {
// Check that the name referenced in the pipeline's Exporters exists in the top-level Exporters.
if cfg.Exporters[ref] == nil {
fmt.Printf("**..service::pipeline::%s: references exporter %q which is not configured\n", pipelineID, ref)
}
}
}
}

// ConfigService defines the configurable components of the service.
type ConfigService struct {
// Telemetry is the configuration for collector's own telemetry.
Expand Down Expand Up @@ -173,6 +249,29 @@ func (cfg *ConfigService) Validate() error {
return nil
}

func (cfg *ConfigService) DryRunValidate() {
// Must have at least one pipeline.
if len(cfg.Pipelines) == 0 {
fmt.Printf("..%v\n", errMissingServicePipelines)
}

// Check that all pipelines have at least one receiver and one exporter, and they reference
// only configured components.
for pipelineID, pipeline := range cfg.Pipelines {
if pipelineID.Type() != component.DataTypeTraces && pipelineID.Type() != component.DataTypeMetrics && pipelineID.Type() != component.DataTypeLogs {
fmt.Printf("**..service::pipeline::%s: unknown datatype %q\n", pipelineID, pipelineID.Type())
}

// Validate pipeline has at least one receiver.
pipeline.DryRunValidate(pipelineID)

if err := cfg.Telemetry.Validate(); err != nil {
fmt.Printf("**..service::telemetry config validation failed, %v\n", err)
}

}
}

type ConfigServicePipeline struct {
Receivers []component.ID `mapstructure:"receivers"`
Processors []component.ID `mapstructure:"processors"`
Expand Down Expand Up @@ -202,3 +301,25 @@ func (cfg *ConfigServicePipeline) Validate() error {

return nil
}

func (cfg *ConfigServicePipeline) DryRunValidate(pipelineID component.ID) {
// Validate pipeline has at least one receiver.
if len(cfg.Receivers) == 0 {
fmt.Printf("**..service::pipeline::%s: %v\n", pipelineID, errMissingServicePipelineReceivers)
}

// Validate pipeline has at least one exporter.
if len(cfg.Exporters) == 0 {
fmt.Printf("**..service::pipeline::%s: %v\n", pipelineID, errMissingServicePipelineExporters)
}

// Validate no processors are duplicated within a pipeline.
procSet := make(map[component.ID]struct{}, len(cfg.Processors))
for _, ref := range cfg.Processors {
// Ensure no processors are duplicated within the pipeline
if _, exists := procSet[ref]; exists {
fmt.Printf("**..references processor %q multiple times\n", ref)
}
}

}
Loading