Skip to content

Commit 6f7a116

Browse files
Support for validate subcommand for otel configuration (#4023)
* validate subcommand working * Validate subcommand for otel mode * fmt * Update internal/pkg/agent/cmd/validate.go Co-authored-by: Shaunak Kashyap <ycombinator@gmail.com> * otel default config * fix default config file * cleaner config * Update internal/pkg/agent/cmd/validate.go Co-authored-by: Shaunak Kashyap <ycombinator@gmail.com> * Moved validate under otel subcommand * revert common defaults to match main * validate expects at least one config * validate integration test checking invalid config * add wait for otel to TestOtelAPMIngestion: --------- Co-authored-by: Shaunak Kashyap <ycombinator@gmail.com>
1 parent 2555088 commit 6f7a116

File tree

9 files changed

+230
-5
lines changed

9 files changed

+230
-5
lines changed

internal/pkg/agent/application/application.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ func New(
5252
) (*coordinator.Coordinator, coordinator.ConfigManager, composable.Controller, error) {
5353

5454
err := version.InitVersionError()
55-
if err != nil {
55+
if err != nil && !runAsOtel {
56+
// ignore this error when running in otel mode
5657
// non-fatal error, log a warning and move on
5758
log.With("error.message", err).Warnf("Error initializing version information: falling back to %s", release.Version())
5859
}

internal/pkg/agent/cmd/otel.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ const (
2323
setFlagName = "set"
2424
)
2525

26-
func newOtelCommandWithArgs(_ []string, _ *cli.IOStreams) *cobra.Command {
26+
func newOtelCommandWithArgs(args []string, streams *cli.IOStreams) *cobra.Command {
2727
cmd := &cobra.Command{
2828
Use: "otel",
2929
Short: "Start the Elastic Agent in otel mode",
3030
Long: "This command starts the Elastic Agent in otel mode.",
3131
RunE: func(cmd *cobra.Command, _ []string) error {
32-
cfgFiles, err := getConfigFiles(cmd)
32+
cfgFiles, err := getConfigFiles(cmd, true)
3333
if err != nil {
3434
return err
3535
}
@@ -45,14 +45,17 @@ func newOtelCommandWithArgs(_ []string, _ *cli.IOStreams) *cobra.Command {
4545

4646
cmd.SetHelpFunc(func(c *cobra.Command, s []string) {
4747
hideInheritedFlags(c)
48-
c.Parent().HelpFunc()(c, s)
48+
c.Root().HelpFunc()(c, s)
4949
})
5050

5151
cmd.Flags().StringArray(configFlagName, []string{}, "Locations to the config file(s), note that only a"+
5252
" single location can be set per flag entry e.g. `--config=file:/path/to/first --config=file:path/to/second`.")
5353

5454
cmd.Flags().StringArray(setFlagName, []string{}, "Set arbitrary component config property. The component has to be defined in the config file and the flag"+
5555
" has a higher precedence. Array config properties are overridden and maps are joined. Example --set=processors.batch.timeout=2s")
56+
57+
cmd.AddCommand(newValidateCommandWithArgs(args, streams))
58+
5659
return cmd
5760
}
5861

internal/pkg/agent/cmd/otel_flags.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ import (
1313
"github.com/elastic/elastic-agent/internal/pkg/agent/application/paths"
1414
)
1515

16-
func getConfigFiles(cmd *cobra.Command) ([]string, error) {
16+
func getConfigFiles(cmd *cobra.Command, useDefault bool) ([]string, error) {
1717
configFiles, err := cmd.Flags().GetStringArray(configFlagName)
1818
if err != nil {
1919
return nil, fmt.Errorf("failed to retrieve config flags: %w", err)
2020
}
2121

2222
if len(configFiles) == 0 {
23+
if !useDefault {
24+
return nil, fmt.Errorf("at least one config flag must be provided")
25+
}
2326
configFiles = append(configFiles, paths.OtelConfigFile())
2427
}
2528

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
outputs:
2+
default:
3+
type: elasticsearch
4+
hosts: [127.0.0.1:9200]
5+
api_key: "example-key"
6+
preset: balanced
7+
inputs:
8+
- type: system/metrics
9+
id: unique-system-metrics-input
10+
data_stream.namespace: default
11+
use_output: default
12+
streams:
13+
- metricsets:
14+
- cpu
15+
data_stream.dataset: system.cpu
16+
- metricsets:
17+
- memory
18+
data_stream.dataset: system.memory
19+
- metricsets:
20+
- network
21+
data_stream.dataset: system.network
22+
- metricsets:
23+
- filesystem
24+
data_stream.dataset: system.filesystem
25+
agent.logging.to_stderr: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
receivers:
2+
filelog:
3+
include: [ /var/log/system.log ]
4+
start_at: beginning
5+
6+
processors:
7+
resource:
8+
attributes:
9+
- key: service.name
10+
action: insert
11+
value: elastic-otel-test
12+
13+
exporters:
14+
debug:
15+
verbosity: detailed
16+
sampling_initial: 10000
17+
sampling_thereafter: 10000
18+
19+
service:
20+
pipelines:
21+
logs:
22+
receivers: [filelog]
23+
processors: [resource]
24+
exporters:
25+
- debug

internal/pkg/agent/cmd/validate.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package cmd
6+
7+
import (
8+
"context"
9+
10+
"github.com/spf13/cobra"
11+
12+
"github.com/elastic/elastic-agent/internal/pkg/cli"
13+
"github.com/elastic/elastic-agent/internal/pkg/otel"
14+
)
15+
16+
func newValidateCommandWithArgs(_ []string, _ *cli.IOStreams) *cobra.Command {
17+
cmd := &cobra.Command{
18+
Use: "validate",
19+
Short: "Validates the OpenTelemetry collector configuration without running the collector",
20+
SilenceUsage: true, // do not display usage on error
21+
SilenceErrors: true,
22+
RunE: func(cmd *cobra.Command, _ []string) error {
23+
cfgFiles, err := getConfigFiles(cmd, false)
24+
if err != nil {
25+
return err
26+
}
27+
return validateOtelConfig(cmd.Context(), cfgFiles)
28+
},
29+
}
30+
31+
cmd.Flags().StringArray(configFlagName, []string{}, "Locations to the config file(s), note that only a"+
32+
" single location can be set per flag entry e.g. `--config=file:/path/to/first --config=file:path/to/second`.")
33+
34+
cmd.Flags().StringArray(setFlagName, []string{}, "Set arbitrary component config property. The component has to be defined in the config file and the flag"+
35+
" has a higher precedence. Array config properties are overridden and maps are joined. Example --set=processors.batch.timeout=2s")
36+
37+
cmd.SetHelpFunc(func(c *cobra.Command, s []string) {
38+
hideInheritedFlags(c)
39+
c.Root().HelpFunc()(c, s)
40+
})
41+
42+
return cmd
43+
}
44+
45+
func validateOtelConfig(ctx context.Context, cfgFiles []string) error {
46+
return otel.Validate(ctx, cfgFiles)
47+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package cmd
6+
7+
import (
8+
"context"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestValidateCommand(t *testing.T) {
16+
tt := []struct {
17+
Name string
18+
ConfigPaths []string
19+
ExpectingErr bool
20+
}{
21+
{
22+
"otel config",
23+
[]string{filepath.Join("testdata", "otel", "otel.yml")},
24+
false,
25+
},
26+
{
27+
"agent config",
28+
[]string{filepath.Join("testdata", "otel", "elastic-agent.yml")},
29+
true,
30+
},
31+
}
32+
33+
for _, tc := range tt {
34+
t.Run(tc.Name, func(t *testing.T) {
35+
err := validateOtelConfig(context.Background(), tc.ConfigPaths)
36+
require.Equal(t, tc.ExpectingErr, err != nil)
37+
})
38+
}
39+
}

internal/pkg/otel/validate.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package otel
6+
7+
import (
8+
"context"
9+
10+
"go.opentelemetry.io/collector/otelcol"
11+
12+
"github.com/elastic/elastic-agent/internal/pkg/release"
13+
)
14+
15+
func Validate(ctx context.Context, configPaths []string) error {
16+
settings, err := newSettings(release.Version(), configPaths)
17+
if err != nil {
18+
return err
19+
}
20+
21+
col, err := otelcol.NewCollector(*settings)
22+
if err != nil {
23+
return err
24+
}
25+
return col.DryRun(ctx)
26+
27+
}

testing/integration/otel_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ service:
4444
exporters:
4545
- file`)
4646

47+
var fileInvalidOtelConfig = []byte(`receivers:
48+
filelog:
49+
include: [ "/var/log/system.log", "/var/log/syslog" ]
50+
start_at: beginning
51+
52+
exporters:
53+
file:
54+
path: ` + fileProcessingFilename + `
55+
service:
56+
pipelines:
57+
logs:
58+
receivers: [filelog]
59+
processors: [nonexistingprocessor]
60+
exporters:
61+
- file`)
62+
4763
const apmProcessingContent = `2023-06-19 05:20:50 ERROR This is a test error message
4864
2023-06-20 12:50:00 DEBUG This is a test debug message 2
4965
2023-06-20 12:51:00 DEBUG This is a test debug message 3
@@ -133,6 +149,8 @@ func TestOtelFileProcessing(t *testing.T) {
133149
`"stringValue":"system.log"`, // system.log is being processed
134150
})
135151

152+
validateCommandIsWorking(t, ctx, fixture, tempDir)
153+
136154
// check `elastic-agent status` returns successfully
137155
require.Eventuallyf(t, func() bool {
138156
// This will return errors until it connects to the agent,
@@ -176,6 +194,25 @@ func TestOtelFileProcessing(t *testing.T) {
176194
require.True(t, err == nil || err == context.Canceled || err == context.DeadlineExceeded, "Retrieved unexpected error: %s", err.Error())
177195
}
178196

197+
func validateCommandIsWorking(t *testing.T, ctx context.Context, fixture *aTesting.Fixture, tempDir string) {
198+
cfgFilePath := filepath.Join(tempDir, "otel-valid.yml")
199+
require.NoError(t, os.WriteFile(cfgFilePath, []byte(fileProcessingConfig), 0600))
200+
201+
// check `elastic-agent otel validate` command works for otel config
202+
out, err := fixture.Exec(ctx, []string{"otel", "validate", "--config", cfgFilePath})
203+
require.NoError(t, err)
204+
require.Equal(t, 0, len(out)) // no error printed out
205+
206+
// check `elastic-agent otel validate` command works for invalid otel config
207+
cfgFilePath = filepath.Join(tempDir, "otel-invalid.yml")
208+
require.NoError(t, os.WriteFile(cfgFilePath, []byte(fileInvalidOtelConfig), 0600))
209+
210+
out, err = fixture.Exec(ctx, []string{"otel", "validate", "--config", cfgFilePath})
211+
require.Error(t, err)
212+
require.False(t, len(out) == 0)
213+
require.Contains(t, string(out), `service::pipelines::logs: references processor "nonexistingprocessor" which is not configured`)
214+
}
215+
179216
func TestOtelAPMIngestion(t *testing.T) {
180217
info := define.Require(t, define.Requirements{
181218
Group: Default,
@@ -261,12 +298,30 @@ func TestOtelAPMIngestion(t *testing.T) {
261298
fixtureWg.Done()
262299
}()
263300

301+
// wait for apm to start
264302
err = logWatcher.WaitForKeys(context.Background(),
265303
10*time.Minute,
266304
500*time.Millisecond,
267305
apmReadyLog,
268306
)
269307
require.NoError(t, err, "APM not initialized")
308+
309+
// wait for otel collector to start
310+
require.Eventuallyf(t, func() bool {
311+
// This will return errors until it connects to the agent,
312+
// they're mostly noise because until the agent starts running
313+
// we will get connection errors. If the test fails
314+
// the agent logs will be present in the error message
315+
// which should help to explain why the agent was not
316+
// healthy.
317+
err = fixture.IsHealthy(ctx)
318+
return err == nil
319+
},
320+
2*time.Minute, time.Second,
321+
"Elastic-Agent did not report healthy. Agent status error: \"%v\"",
322+
err,
323+
)
324+
270325
require.NoError(t, os.WriteFile(filepath.Join(tempDir, fileName), []byte(apmProcessingContent), 0600))
271326

272327
// check index

0 commit comments

Comments
 (0)