From f8de8ea7ef3c84733b22c6b9c2287a3ed4ef01e0 Mon Sep 17 00:00:00 2001 From: Matt Cotter Date: Thu, 29 May 2025 16:44:09 -0500 Subject: [PATCH 1/3] feat: support otelcol flags (config, set, feature-gates) in all sub commands --- go.mod | 2 +- internal/commands/config/config.go | 16 +-- internal/commands/config/config_test.go | 45 +++++++ .../config_printers.go => config/printers.go} | 54 +++++--- .../commands/config/test/agent-config.yaml | 22 ++++ .../commands/config/test/otel-config.yaml | 17 +++ internal/commands/config/test/output.yaml | 124 ++++++++++++++++++ internal/commands/diagnose/otelconfigcheck.go | 12 +- internal/commands/start/start.go | 87 +++--------- internal/connections/confighandler.go | 22 ++++ internal/root/root.go | 14 +- main_windows.go | 5 +- observecol/feature_gates.go | 58 ++++++++ observecol/flags.go | 24 ++++ observecol/go.mod | 16 ++- observecol/otelcollector.go | 74 +++++++---- 16 files changed, 447 insertions(+), 145 deletions(-) create mode 100644 internal/commands/config/config_test.go rename internal/commands/{util/config_printers.go => config/printers.go} (63%) create mode 100644 internal/commands/config/test/agent-config.yaml create mode 100644 internal/commands/config/test/otel-config.yaml create mode 100644 internal/commands/config/test/output.yaml create mode 100644 observecol/feature_gates.go create mode 100644 observecol/flags.go diff --git a/go.mod b/go.mod index a677d4ae4..c62e2a622 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/prometheus/common v0.62.0 github.com/shirou/gopsutil/v3 v3.24.5 github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/confmap v1.30.0 @@ -293,7 +294,6 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tidwall/gjson v1.14.2 // indirect diff --git a/internal/commands/config/config.go b/internal/commands/config/config.go index ed6cbb3c7..228ea3a17 100644 --- a/internal/commands/config/config.go +++ b/internal/commands/config/config.go @@ -6,9 +6,8 @@ package config import ( "context" "fmt" + "os" - "github.com/observeinc/observe-agent/internal/commands/start" - "github.com/observeinc/observe-agent/internal/commands/util" "github.com/observeinc/observe-agent/internal/commands/util/logger" "github.com/observeinc/observe-agent/internal/root" "github.com/spf13/cobra" @@ -33,19 +32,12 @@ bundled OTel configuration.`, } ctx := logger.WithCtx(context.Background(), logger.GetNop()) - configFilePaths, cleanup, err := start.SetupAndGetConfigFiles(ctx) - if cleanup != nil { - defer cleanup() - } - if err != nil { - return err - } if singleOtel { - return util.PrintShortOtelConfig(ctx, configFilePaths) + return PrintShortOtelConfig(ctx, os.Stdout) } else if detailedOtel { - return util.PrintFullOtelConfig(configFilePaths) + return PrintFullOtelConfig(ctx, os.Stdout) } - return util.PrintAllConfigsIndividually(configFilePaths) + return PrintAllConfigsIndividually(ctx, os.Stdout) }, } diff --git a/internal/commands/config/config_test.go b/internal/commands/config/config_test.go new file mode 100644 index 000000000..31521e89e --- /dev/null +++ b/internal/commands/config/config_test.go @@ -0,0 +1,45 @@ +/* +Copyright © 2024 NAME HERE +*/ +package config + +import ( + "bytes" + "context" + "os" + "path" + "path/filepath" + "runtime" + "testing" + + "github.com/observeinc/observe-agent/internal/commands/util/logger" + "github.com/observeinc/observe-agent/internal/root" + "github.com/observeinc/observe-agent/observecol" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +// TODO enable this test once the agent bundled configs don't depend on the host filesystem having the templates. +func XTest_RenderOtelConfig(t *testing.T) { + // Get current path + _, filename, _, ok := runtime.Caller(0) + assert.True(t, ok) + curPath := path.Dir(filename) + + // Set config flags + flags := pflag.NewFlagSet("test", pflag.ContinueOnError) + observecol.AddConfigFlags(flags) + flags.Parse([]string{"--config", filepath.Join(curPath, "test/otel-config.yaml")}) + viper.Reset() + root.CfgFile = filepath.Join(curPath, "test/agent-config.yaml") + root.InitConfig() + + // Run the test + ctx := logger.WithCtx(context.Background(), logger.GetNop()) + var output bytes.Buffer + PrintShortOtelConfig(ctx, &output) + expected, err := os.ReadFile(filepath.Join(curPath, "test/output.yaml")) + assert.NoError(t, err) + assert.Equal(t, string(expected), output.String()) +} diff --git a/internal/commands/util/config_printers.go b/internal/commands/config/printers.go similarity index 63% rename from internal/commands/util/config_printers.go rename to internal/commands/config/printers.go index 2b78d2ed1..6a67a8a53 100644 --- a/internal/commands/util/config_printers.go +++ b/internal/commands/config/printers.go @@ -1,13 +1,15 @@ -package util +package config import ( "context" "fmt" + "io" "os" "strings" "github.com/go-viper/mapstructure/v2" "github.com/observeinc/observe-agent/internal/config" + "github.com/observeinc/observe-agent/internal/connections" "github.com/observeinc/observe-agent/observecol" "github.com/spf13/viper" "go.opentelemetry.io/collector/confmap" @@ -15,11 +17,19 @@ import ( "gopkg.in/yaml.v3" ) -func PrintAllConfigsIndividually(configFilePaths []string) error { +func PrintAllConfigsIndividually(ctx context.Context, w io.Writer) error { + configFilePaths, cleanup, err := connections.SetupAndGetConfigFiles(ctx) + if cleanup != nil { + defer cleanup() + } + if err != nil { + return err + } + printConfig := func(comment string, data []byte) { - fmt.Printf("# ======== %s\n", comment) - fmt.Println(strings.Trim(string(data), "\n\t ")) - fmt.Println("---") + fmt.Fprintf(w, "# ======== %s\n", comment) + fmt.Fprintln(w, strings.Trim(string(data), "\n\t ")) + fmt.Fprintln(w, "---") } agentConfig, err := config.AgentConfigFromViper(viper.GetViper()) @@ -52,12 +62,18 @@ func PrintAllConfigsIndividually(configFilePaths []string) error { return nil } -func PrintShortOtelConfig(ctx context.Context, configFilePaths []string) error { - if len(configFilePaths) == 0 { +func PrintShortOtelConfig(ctx context.Context, w io.Writer) error { + settings, cleanup, err := observecol.GetOtelCollectorSettings(ctx) + if cleanup != nil { + defer cleanup() + } + if err != nil { + return err + } + if len(settings.ConfigProviderSettings.ResolverSettings.URIs) == 0 { return nil } - settings := observecol.ConfigProviderSettings(configFilePaths) - resolver, err := confmap.NewResolver(settings.ResolverSettings) + resolver, err := confmap.NewResolver(settings.ConfigProviderSettings.ResolverSettings) if err != nil { return fmt.Errorf("failed to create new resolver: %w", err) } @@ -69,20 +85,26 @@ func PrintShortOtelConfig(ctx context.Context, configFilePaths []string) error { if err != nil { return fmt.Errorf("error while marshaling to YAML: %w", err) } - fmt.Printf("%s\n", b) + fmt.Fprintf(w, "%s\n", b) return nil } -func PrintFullOtelConfig(configFilePaths []string) error { - if len(configFilePaths) == 0 { +func PrintFullOtelConfig(ctx context.Context, w io.Writer) error { + settings, cleanup, err := observecol.GetOtelCollectorSettings(ctx) + if cleanup != nil { + defer cleanup() + } + if err != nil { + return err + } + if len(settings.ConfigProviderSettings.ResolverSettings.URIs) == 0 { return nil } - colSettings := observecol.GenerateCollectorSettingsWithConfigFiles(configFilePaths) - factories, err := colSettings.Factories() + factories, err := settings.Factories() if err != nil { return fmt.Errorf("failed to create component factory maps: %w", err) } - provider, err := otelcol.NewConfigProvider(colSettings.ConfigProviderSettings) + provider, err := otelcol.NewConfigProvider(settings.ConfigProviderSettings) if err != nil { return fmt.Errorf("failed to create config provider: %w", err) } @@ -99,6 +121,6 @@ func PrintFullOtelConfig(configFilePaths []string) error { if err != nil { return fmt.Errorf("failed to marshall config to yaml: %w", err) } - fmt.Printf("%s\n", cfgYaml) + fmt.Fprintf(w, "%s\n", cfgYaml) return nil } diff --git a/internal/commands/config/test/agent-config.yaml b/internal/commands/config/test/agent-config.yaml new file mode 100644 index 000000000..d207a5851 --- /dev/null +++ b/internal/commands/config/test/agent-config.yaml @@ -0,0 +1,22 @@ +# Target Observe collection url +observe_url: https://test.collect.observeinc.com/ + +# Observe data token +token: 12345678901234567890:abcdefghijklmnopqrstuvwxyzABCDEF + +debug: false + +forwarding: + enabled: false + +health_check: + enabled: false + +internal_telemetry: + enabled: false + +host_monitoring: + enabled: false + +self_monitoring: + enabled: false diff --git a/internal/commands/config/test/otel-config.yaml b/internal/commands/config/test/otel-config.yaml new file mode 100644 index 000000000..db670d8cc --- /dev/null +++ b/internal/commands/config/test/otel-config.yaml @@ -0,0 +1,17 @@ +receivers: + filelog/test: + include: ["./test.log"] + +service: + pipelines: + logs/test: + receivers: + - filelog/test + processors: + - memory_limiter + - transform/truncate + - resourcedetection + - resourcedetection/cloud + - batch + exporters: + - otlphttp/observe diff --git a/internal/commands/config/test/output.yaml b/internal/commands/config/test/output.yaml new file mode 100644 index 000000000..1a1a0de98 --- /dev/null +++ b/internal/commands/config/test/output.yaml @@ -0,0 +1,124 @@ +connectors: + count: null +exporters: + debug: null + nop: null + otlphttp/observe: + compression: zstd + endpoint: https://test.collect.observeinc.com/v2/otel + headers: + authorization: Bearer 12345678901234567890:abcdefghijklmnopqrstuvwxyzABCDEF + x-observe-target-package: Host Explorer + retry_on_failure: + enabled: true + sending_queue: + num_consumers: 4 + queue_size: 100 + prometheusremotewrite/observe: + endpoint: https://test.collect.observeinc.com/v1/prometheus + headers: + authorization: Bearer 12345678901234567890:abcdefghijklmnopqrstuvwxyzABCDEF + x-observe-target-package: Host Explorer + max_batch_request_parallelism: 5 + remote_write_queue: + num_consumers: 5 + resource_to_telemetry_conversion: + enabled: true + send_metadata: true +extensions: + file_storage: + directory: /var/lib/observe-agent/filestorage +processors: + batch: + timeout: 5s + deltatocumulative: null + filter/count: + error_mode: ignore + metrics: + metric: + - IsMatch(name, ".*") + memory_limiter: + check_interval: 1s + limit_percentage: 80 + spike_limit_percentage: 20 + resourcedetection: + detectors: + - env + - system + system: + hostname_sources: + - dns + - os + resource_attributes: + host.arch: + enabled: true + host.cpu.cache.l2.size: + enabled: true + host.cpu.family: + enabled: true + host.cpu.model.id: + enabled: true + host.cpu.model.name: + enabled: true + host.cpu.stepping: + enabled: true + host.cpu.vendor.id: + enabled: true + host.id: + enabled: false + host.name: + enabled: true + os.description: + enabled: true + os.type: + enabled: true + resourcedetection/cloud: + detectors: + - gcp + - ecs + - ec2 + - azure + override: false + timeout: 2s + transform/truncate: + log_statements: + - context: log + statements: + - truncate_all(attributes, 4095) + - truncate_all(resource.attributes, 4095) + trace_statements: + - context: span + statements: + - truncate_all(attributes, 4095) + - truncate_all(resource.attributes, 4095) +receivers: + filelog/test: + include: + - ./test.log + nop: null +service: + extensions: + - health_check + - file_storage + pipelines: + logs/test: + exporters: + - otlphttp/observe + processors: + - memory_limiter + - transform/truncate + - resourcedetection + - resourcedetection/cloud + - batch + receivers: + - filelog/test + metrics/count-nop-in: + exporters: + - count + receivers: + - nop + metrics/count-nop-out: + exporters: + - nop + receivers: + - count diff --git a/internal/commands/diagnose/otelconfigcheck.go b/internal/commands/diagnose/otelconfigcheck.go index 5816c6e06..e65f88d8b 100644 --- a/internal/commands/diagnose/otelconfigcheck.go +++ b/internal/commands/diagnose/otelconfigcheck.go @@ -4,11 +4,9 @@ import ( "context" "embed" - "github.com/observeinc/observe-agent/internal/commands/start" "github.com/observeinc/observe-agent/internal/commands/util/logger" "github.com/observeinc/observe-agent/observecol" "github.com/spf13/viper" - "go.opentelemetry.io/collector/otelcol" ) type OtelConfigTestResult struct { @@ -17,21 +15,17 @@ type OtelConfigTestResult struct { } func checkOtelConfig(_ *viper.Viper) (bool, any, error) { - configFilePaths, cleanup, err := start.SetupAndGetConfigFiles(logger.WithCtx(context.Background(), logger.GetNop())) + ctx := logger.WithCtx(context.Background(), logger.GetNop()) + col, cleanup, err := observecol.GetOtelCollector(ctx) if cleanup != nil { defer cleanup() } if err != nil { return false, nil, err } - colSettings := observecol.GenerateCollectorSettingsWithConfigFiles(configFilePaths) // These are the same checks as the `otelcol validate` command: // https://github.com/open-telemetry/opentelemetry-collector/blob/main/otelcol/command_validate.go - col, err := otelcol.NewCollector(*colSettings) - if err != nil { - return false, nil, err - } - err = col.DryRun(context.Background()) + err = col.DryRun(ctx) if err != nil { return false, OtelConfigTestResult{ Passed: false, diff --git a/internal/commands/start/start.go b/internal/commands/start/start.go index d51112f85..ebd06b7d1 100644 --- a/internal/commands/start/start.go +++ b/internal/commands/start/start.go @@ -5,91 +5,34 @@ package start import ( "context" - "os" - "strings" "github.com/observeinc/observe-agent/internal/commands/util/logger" - "github.com/observeinc/observe-agent/internal/connections" "github.com/observeinc/observe-agent/internal/root" "github.com/observeinc/observe-agent/observecol" "github.com/spf13/cobra" ) -func SetupAndGetConfigFiles(ctx context.Context) ([]string, func(), error) { - // Set Env Vars from config - err := connections.SetEnvVars() - if err != nil { - return nil, nil, err - } - // Set up our temp dir annd temp config files - tmpDir, err := os.MkdirTemp("", connections.TempFilesFolder) - if err != nil { - return nil, nil, err - } - cleanup := func() { - os.RemoveAll(tmpDir) - } - configFilePaths, err := connections.GetAllOtelConfigFilePaths(ctx, tmpDir) - if err != nil { - cleanup() - return nil, nil, err - } - return configFilePaths, cleanup, nil -} - func DefaultLoggerCtx() context.Context { return logger.WithCtx(context.Background(), logger.Get()) } func MakeStartCommand() *cobra.Command { - // Create the start command from the otel collector command - settings := observecol.GenerateCollectorSettings() - otleCmd := observecol.GetOtelCollectorCommand(settings) - otleCmd.Use = "start" - otleCmd.Short = "Start the Observe agent process." - otleCmd.Long = `The Observe agent is based on the OpenTelemetry Collector. + otleCmd := &cobra.Command{ + Use: "start", + Short: "Start the Observe agent process.", + Long: `The Observe agent is based on the OpenTelemetry Collector. This command reads in the local config and env vars and starts the -collector on the current host.` - // Drop the sub commands - otleCmd.ResetCommands() - - // Modify the run function so we can pass in our packaged config files. - originalRunE := otleCmd.RunE - otleCmd.RunE = func(cmd *cobra.Command, args []string) error { - observeAgentConfigs, cleanup, err := SetupAndGetConfigFiles(DefaultLoggerCtx()) - if cleanup != nil { - defer cleanup() - } - if err != nil { - return err - } - configFlag := otleCmd.Flags().Lookup("config") - // The otelcol config flag has a lot of functionality that we want to utilize, but the flag isn't exposed to - // the library's callers in the code. Our workaround is to use the original otelcol command as our `start` - // command and add our bundled config files to the config flag. - // - // However, we want any overrides that users provide via `--config` to take precedence, so we need to add our - // config values _before_ any current values. Since we can only append to the end of the flag, we add our - // bundled configs, then *re-add* the original config values. - originalConfigStr := configFlag.Value.String() - // This string is formatted as "[path1, path2, ...]" - // see: https://github.com/open-telemetry/opentelemetry-collector/blob/v0.118.0/otelcol/flags.go#L28-L30 - // Trim the surrounding brackets, then split on ", " - var originalConfigs []string - if len(originalConfigStr) > 2 { - originalConfigStr = originalConfigStr[1 : len(originalConfigStr)-1] - originalConfigs = strings.Split(originalConfigStr, ", ") - } - for _, path := range observeAgentConfigs { - configFlag.Value.Set(path) - } - for _, path := range originalConfigs { - configFlag.Value.Set(path) - } - // Set PRWE enable multiple workers feature gate. - enableMultipleWritersFlag := otleCmd.Flags().Lookup("feature-gates") - enableMultipleWritersFlag.Value.Set("+exporter.prometheusremotewritexporter.EnableMultipleWorkers") - return originalRunE(cmd, args) +collector on the current host.`, + RunE: func(cmd *cobra.Command, args []string) error { + col, cleanup, err := observecol.GetOtelCollector(DefaultLoggerCtx()) + if cleanup != nil { + defer cleanup() + } + if err != nil { + return err + } + return col.Run(cmd.Context()) + }, } return otleCmd } diff --git a/internal/connections/confighandler.go b/internal/connections/confighandler.go index 5fbb655b2..32f0a4800 100644 --- a/internal/connections/confighandler.go +++ b/internal/connections/confighandler.go @@ -19,6 +19,28 @@ const ( OTEL_OVERRIDE_YAML_KEY = "otel_config_overrides" ) +func SetupAndGetConfigFiles(ctx context.Context) ([]string, func(), error) { + // Set Env Vars from config + err := SetEnvVars() + if err != nil { + return nil, nil, err + } + // Set up our temp dir annd temp config files + tmpDir, err := os.MkdirTemp("", TempFilesFolder) + if err != nil { + return nil, nil, err + } + cleanup := func() { + os.RemoveAll(tmpDir) + } + configFilePaths, err := GetAllOtelConfigFilePaths(ctx, tmpDir) + if err != nil { + cleanup() + return nil, nil, err + } + return configFilePaths, cleanup, nil +} + func GetAllOtelConfigFilePaths(ctx context.Context, tmpDir string) ([]string, error) { configFilePaths := []string{} // Get additional config paths based on connection configs diff --git a/internal/root/root.go b/internal/root/root.go index c52589166..ee16693cd 100644 --- a/internal/root/root.go +++ b/internal/root/root.go @@ -4,11 +4,15 @@ Copyright © 2024 NAME HERE package root import ( + "context" "fmt" "os" + "github.com/observeinc/observe-agent/build" + "github.com/observeinc/observe-agent/internal/commands/util/logger" "github.com/observeinc/observe-agent/internal/config" "github.com/observeinc/observe-agent/internal/connections" + "github.com/observeinc/observe-agent/observecol" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -21,6 +25,7 @@ var RootCmd = &cobra.Command{ Short: "Observe distribution of OTEL Collector", Long: `Observe distribution of OTEL Collector along with CLI utils to help with setup and maintenance. To start the agent, run: observe-agent start`, + Version: build.Version, } // Execute adds all child commands to the root command and sets flags appropriately. @@ -35,11 +40,15 @@ func Execute() { func init() { cobra.OnInitialize(InitConfig) - RootCmd.PersistentFlags().StringVar(&CfgFile, "observe-config", "", "observe-agent config file path") + flags := RootCmd.PersistentFlags() + flags.StringVar(&CfgFile, "observe-config", "", "observe-agent config file path") + observecol.AddConfigFlags(flags) + observecol.AddFeatureGateFlag(flags) } // InitConfig reads in config file and ENV variables if set. func InitConfig() { + ctx := logger.WithCtx(context.Background(), logger.Get()) // Some keys in OTEL component configs use "." as part of the key but viper ends up parsing that into // a subobject since the default key delimiter is "." which causes config validation to fail. // We set it to "::" here to prevent that behavior. This call modifies the global viper instance. @@ -67,4 +76,7 @@ func InitConfig() { fmt.Fprintln(os.Stderr, "error reading config file:", err) } } + + // Apply feature gates + observecol.ApplyFeatureGates(ctx) } diff --git a/main_windows.go b/main_windows.go index 5b946f00e..990cfdebf 100644 --- a/main_windows.go +++ b/main_windows.go @@ -3,12 +3,12 @@ package main import ( + "context" "errors" "fmt" "log" "os" - "github.com/observeinc/observe-agent/internal/commands/start" "github.com/observeinc/observe-agent/internal/root" "github.com/observeinc/observe-agent/observecol" "go.opentelemetry.io/collector/otelcol" @@ -31,14 +31,13 @@ func run() error { root.InitConfig() // Get the collector settings along with our bundled config files. - configFilePaths, cleanup, err := start.SetupAndGetConfigFiles(start.DefaultLoggerCtx()) + colSettings, cleanup, err := observecol.GetOtelCollectorSettings(context.Background()) if cleanup != nil { defer cleanup() } if err != nil { return err } - colSettings := observecol.GenerateCollectorSettingsWithConfigFiles(configFilePaths) if err := svc.Run("", otelcol.NewSvcHandler(*colSettings)); err != nil { if errors.Is(err, windows.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) { diff --git a/observecol/feature_gates.go b/observecol/feature_gates.go new file mode 100644 index 000000000..43bda44c6 --- /dev/null +++ b/observecol/feature_gates.go @@ -0,0 +1,58 @@ +package observecol + +import ( + "context" + "errors" + + "github.com/observeinc/observe-agent/internal/commands/util/logger" + "github.com/spf13/pflag" + "go.opentelemetry.io/collector/featuregate" + "go.uber.org/zap" +) + +// Flag name and description copied directly from otel collector +const ( + featureGatesFlag = "feature-gates" + featureGatesFlagDescription = "Comma-delimited list of feature gate identifiers. Prefix with '-' to disable the feature. '+' or no prefix will enable the feature." +) + +var featureGates []string + +var internalFeatureFlagDefaults = map[string]bool{ + "exporter.prometheusremotewritexporter.EnableMultipleWorkers": true, +} + +func AddFeatureGateFlag(flags *pflag.FlagSet) { + flags.StringSliceVar(&featureGates, featureGatesFlag, []string{}, featureGatesFlagDescription) +} + +func ApplyFeatureGates(ctx context.Context) error { + flags := make(map[string]bool) + for _, f := range featureGates { + if f[0] == '-' { + flags[f[1:]] = false + } else if f[0] == '+' { + flags[f[1:]] = true + } else { + flags[f] = true + } + } + + // Apply internal defaults only if the user did not specify a value in the flag. + for id, enabled := range internalFeatureFlagDefaults { + if _, ok := flags[id]; !ok { + flags[id] = enabled + } + } + + var errs error + for id, enabled := range flags { + err := featuregate.GlobalRegistry().Set(id, enabled) + if err != nil { + errs = errors.Join(errs, err) + } else { + logger.FromCtx(ctx).Debug("feature gate set", zap.String("id", id), zap.Bool("enabled", enabled)) + } + } + return errs +} diff --git a/observecol/flags.go b/observecol/flags.go new file mode 100644 index 000000000..6cbe3a013 --- /dev/null +++ b/observecol/flags.go @@ -0,0 +1,24 @@ +package observecol + +import ( + "github.com/spf13/pflag" +) + +// Flag name and description copied directly from otel collector +const ( + configFlag = "config" + configFlagDescription = "OpenTelemetry config file(s), note that only a single location can be set per flag entry e.g. " + + "`--config=file:/path/to/first --config=file:path/to/second`." + setFlag = "set" + setFlagDescription = "Set arbitrary OpenTelemetry component config properties. The component has to be defined in " + + "the bundled or provided OpenTelemetry config files and this flag has a higher precedence. " + + "Array config properties are overridden and maps are joined. Example --set=processors.batch.timeout=2s" +) + +var otelConfigs []string +var otelSets []string + +func AddConfigFlags(flags *pflag.FlagSet) { + flags.StringSliceVar(&otelConfigs, configFlag, []string{}, configFlagDescription) + flags.StringSliceVar(&otelSets, setFlag, []string{}, setFlagDescription) +} diff --git a/observecol/go.mod b/observecol/go.mod index a13c1cbb2..323ad393c 100644 --- a/observecol/go.mod +++ b/observecol/go.mod @@ -50,7 +50,7 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/receiver/tcplogreceiver v0.124.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/udplogreceiver v0.124.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/windowseventlogreceiver v0.124.0 - github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 go.opentelemetry.io/collector/component v1.30.0 go.opentelemetry.io/collector/confmap v1.30.0 go.opentelemetry.io/collector/confmap/provider/envprovider v1.30.0 @@ -67,6 +67,7 @@ require ( go.opentelemetry.io/collector/exporter/otlphttpexporter v0.124.0 go.opentelemetry.io/collector/extension v1.30.0 go.opentelemetry.io/collector/extension/zpagesextension v0.124.0 + go.opentelemetry.io/collector/featuregate v1.30.0 go.opentelemetry.io/collector/otelcol v0.124.0 go.opentelemetry.io/collector/processor v1.30.0 go.opentelemetry.io/collector/processor/batchprocessor v0.124.0 @@ -74,6 +75,7 @@ require ( go.opentelemetry.io/collector/receiver v1.30.0 go.opentelemetry.io/collector/receiver/nopreceiver v0.124.0 go.opentelemetry.io/collector/receiver/otlpreceiver v0.124.0 + go.uber.org/zap v1.27.0 ) require ( @@ -231,6 +233,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mcuadros/go-defaults v1.2.0 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect github.com/miekg/dns v1.1.63 // indirect @@ -278,6 +281,7 @@ require ( github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/ovh/go-ovh v1.7.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect @@ -299,12 +303,18 @@ require ( github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/relvacode/iso8601 v1.6.0 // indirect github.com/rs/cors v1.11.1 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 // indirect github.com/shirou/gopsutil/v4 v4.25.3 // indirect github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/viper v1.20.0-alpha.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/tidwall/gjson v1.14.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect @@ -355,7 +365,6 @@ require ( go.opentelemetry.io/collector/extension/extensioncapabilities v0.124.0 // indirect go.opentelemetry.io/collector/extension/extensiontest v0.124.0 // indirect go.opentelemetry.io/collector/extension/xextension v0.124.0 // indirect - go.opentelemetry.io/collector/featuregate v1.30.0 // indirect go.opentelemetry.io/collector/filter v0.124.0 // indirect go.opentelemetry.io/collector/internal/fanoutconsumer v0.124.0 // indirect go.opentelemetry.io/collector/internal/memorylimiter v0.124.0 // indirect @@ -408,7 +417,6 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect diff --git a/observecol/otelcollector.go b/observecol/otelcollector.go index ffc5b53a4..3f95edd5f 100644 --- a/observecol/otelcollector.go +++ b/observecol/otelcollector.go @@ -1,9 +1,13 @@ package observecol import ( + "context" + "fmt" + "strings" + "github.com/observeinc/observe-agent/build" + "github.com/observeinc/observe-agent/internal/connections" - "github.com/spf13/cobra" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/confmap" "go.opentelemetry.io/collector/confmap/provider/envprovider" @@ -14,42 +18,58 @@ import ( "go.opentelemetry.io/collector/otelcol" ) -func ConfigProviderSettings(URIs []string) otelcol.ConfigProviderSettings { - return otelcol.ConfigProviderSettings{ - ResolverSettings: confmap.ResolverSettings{ - URIs: URIs, - ProviderFactories: []confmap.ProviderFactory{ - fileprovider.NewFactory(), - envprovider.NewFactory(), - yamlprovider.NewFactory(), - httpprovider.NewFactory(), - httpsprovider.NewFactory(), - }, - }, - } -} - -func GenerateCollectorSettings() *otelcol.CollectorSettings { +func generateCollectorSettings(URIs []string) *otelcol.CollectorSettings { buildInfo := component.BuildInfo{ Command: "observe-agent", Description: "Observe Distribution of Opentelemetry Collector", Version: build.Version, } set := &otelcol.CollectorSettings{ - BuildInfo: buildInfo, - Factories: components, - ConfigProviderSettings: ConfigProviderSettings([]string{}), + BuildInfo: buildInfo, + Factories: components, + ConfigProviderSettings: otelcol.ConfigProviderSettings{ + ResolverSettings: confmap.ResolverSettings{ + URIs: URIs, + DefaultScheme: "file", + ProviderFactories: []confmap.ProviderFactory{ + fileprovider.NewFactory(), + envprovider.NewFactory(), + yamlprovider.NewFactory(), + httpprovider.NewFactory(), + httpsprovider.NewFactory(), + }, + }, + }, } return set } -func GenerateCollectorSettingsWithConfigFiles(configFiles []string) *otelcol.CollectorSettings { - set := GenerateCollectorSettings() - set.ConfigProviderSettings.ResolverSettings.URIs = configFiles - return set +func GetOtelCollectorSettings(ctx context.Context) (*otelcol.CollectorSettings, func(), error) { + observeAgentConfigs, cleanup, err := connections.SetupAndGetConfigFiles(ctx) + if err != nil { + return nil, cleanup, err + } + URIs := append(observeAgentConfigs, otelConfigs...) + // This loop is copied directly from the otelcol `set` flag handling. + for _, s := range otelSets { + idx := strings.Index(s, "=") + if idx == -1 { + return nil, cleanup, fmt.Errorf("Value provided to --set flag is missing equal sign: %s", s) + } + URIs = append(URIs, "yaml:"+strings.TrimSpace(strings.ReplaceAll(s[:idx], ".", "::"))+": "+strings.TrimSpace(s[idx+1:])) + } + return generateCollectorSettings(URIs), cleanup, nil } -func GetOtelCollectorCommand(otelconfig *otelcol.CollectorSettings) *cobra.Command { - cmd := otelcol.NewCommand(*otelconfig) - return cmd +func GetOtelCollector(ctx context.Context) (*otelcol.Collector, func(), error) { + settings, cleanup, err := GetOtelCollectorSettings(ctx) + if err != nil { + return nil, cleanup, err + } + + col, err := otelcol.NewCollector(*settings) + if err != nil { + return nil, cleanup, err + } + return col, cleanup, nil } From 0ecb86e40f362458851b4d86fd850c1c8c5c0066 Mon Sep 17 00:00:00 2001 From: Matt Cotter Date: Tue, 10 Jun 2025 10:53:23 -0500 Subject: [PATCH 2/3] add option to set config dir, enable unit test --- internal/commands/config/config_test.go | 13 ++++++++++--- internal/connections/connections.go | 7 +++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/internal/commands/config/config_test.go b/internal/commands/config/config_test.go index 31521e89e..923c728d0 100644 --- a/internal/commands/config/config_test.go +++ b/internal/commands/config/config_test.go @@ -10,9 +10,11 @@ import ( "path" "path/filepath" "runtime" + "strings" "testing" "github.com/observeinc/observe-agent/internal/commands/util/logger" + "github.com/observeinc/observe-agent/internal/connections" "github.com/observeinc/observe-agent/internal/root" "github.com/observeinc/observe-agent/observecol" "github.com/spf13/pflag" @@ -20,13 +22,18 @@ import ( "github.com/stretchr/testify/assert" ) -// TODO enable this test once the agent bundled configs don't depend on the host filesystem having the templates. -func XTest_RenderOtelConfig(t *testing.T) { +// TODO rework this test to handle our snapshot tests as go unit tests. +func Test_RenderOtelConfig(t *testing.T) { // Get current path _, filename, _, ok := runtime.Caller(0) assert.True(t, ok) curPath := path.Dir(filename) + // Set the template base dir for all connections + for _, conn := range connections.AllConnectionTypes { + conn.ApplyOptions(connections.WithConfigFolderPath(filepath.Join(curPath, "../../../packaging/macos/connections"))) + } + // Set config flags flags := pflag.NewFlagSet("test", pflag.ContinueOnError) observecol.AddConfigFlags(flags) @@ -41,5 +48,5 @@ func XTest_RenderOtelConfig(t *testing.T) { PrintShortOtelConfig(ctx, &output) expected, err := os.ReadFile(filepath.Join(curPath, "test/output.yaml")) assert.NoError(t, err) - assert.Equal(t, string(expected), output.String()) + assert.Equal(t, strings.TrimSpace(string(expected)), strings.TrimSpace(output.String())) } diff --git a/internal/connections/connections.go b/internal/connections/connections.go index 14be2f747..e338b18e7 100644 --- a/internal/connections/connections.go +++ b/internal/connections/connections.go @@ -108,3 +108,10 @@ func WithConfigFolderPath(configFolderPath string) ConnectionTypeOption { c.configFolderPath = configFolderPath } } + +func (c *ConnectionType) ApplyOptions(opts ...ConnectionTypeOption) *ConnectionType { + for _, opt := range opts { + opt(c) + } + return c +} From 307fb476b0159319adae8fbb15e1c1ceb1397877 Mon Sep 17 00:00:00 2001 From: Matt Cotter Date: Tue, 10 Jun 2025 11:33:37 -0500 Subject: [PATCH 3/3] add back SilenceUsage: true to avoid printing start usage when a runtime error happens in the otel collector --- internal/commands/start/start.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/commands/start/start.go b/internal/commands/start/start.go index ebd06b7d1..45d3e8a69 100644 --- a/internal/commands/start/start.go +++ b/internal/commands/start/start.go @@ -23,6 +23,7 @@ func MakeStartCommand() *cobra.Command { Long: `The Observe agent is based on the OpenTelemetry Collector. This command reads in the local config and env vars and starts the collector on the current host.`, + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { col, cleanup, err := observecol.GetOtelCollector(DefaultLoggerCtx()) if cleanup != nil {