diff --git a/.chloggen/feature_solarwindsapmsettingsextension-concreteimpl.yaml b/.chloggen/feature_solarwindsapmsettingsextension-concreteimpl.yaml new file mode 100755 index 000000000000..cadf32556e5c --- /dev/null +++ b/.chloggen/feature_solarwindsapmsettingsextension-concreteimpl.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# 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. filelogreceiver) +component: solarwindsapmsettingsextension + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Added the first part of concrete implementation of solarwindsapmsettingsextension + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [27668] + +# (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: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/cmd/otelcontribcol/go.mod b/cmd/otelcontribcol/go.mod index fd4919af3935..7bdef22b4baa 100644 --- a/cmd/otelcontribcol/go.mod +++ b/cmd/otelcontribcol/go.mod @@ -692,6 +692,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/snowflakedb/gosnowflake v1.10.1-0.20240509141315-5570db2126fe // indirect github.com/soheilhy/cmux v0.1.5 // indirect + github.com/solarwindscloud/apm-proto v1.0.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/afero v1.11.0 // indirect diff --git a/cmd/otelcontribcol/go.sum b/cmd/otelcontribcol/go.sum index d6b0cf5547b8..3e531430e443 100644 --- a/cmd/otelcontribcol/go.sum +++ b/cmd/otelcontribcol/go.sum @@ -2222,6 +2222,8 @@ github.com/snowflakedb/gosnowflake v1.10.1-0.20240509141315-5570db2126fe/go.mod github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/solarwindscloud/apm-proto v1.0.3 h1:fmPwWNrM5kduAqvmH8mCD0E9MASK8m/mtPmw0yXGOBs= +github.com/solarwindscloud/apm-proto v1.0.3/go.mod h1:PIMzXc8HpB0ryT4Oci4pUz8F0m1X7Q/hVXkQE4jGv6Y= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= diff --git a/extension/solarwindsapmsettingsextension/README.md b/extension/solarwindsapmsettingsextension/README.md index 88f766bf985d..0af3f00168b1 100644 --- a/extension/solarwindsapmsettingsextension/README.md +++ b/extension/solarwindsapmsettingsextension/README.md @@ -23,11 +23,11 @@ extensions: solarwindsapmsettings: endpoint: "" key: ":" - interval: 1m + interval: 10s ``` ### endpoint (Required) -The APM collector endpoint which this extension calls `getSettings`. See [here](https://documentation.solarwinds.com/en/success_center/observability/content/system_requirements/endpoints.htm) for our APM collector endpoints. +The APM collector endpoint which this extension calls `getSettings`. See [here](https://documentation.solarwinds.com/en/success_center/observability/content/system_requirements/endpoints.htm) for our APM collector endpoints. The endpoint is in format `:`. ### key (Required) The service key in format `:` for `getSettings` from Solarwinds APM collector. See [here](https://documentation.solarwinds.com/en/success_center/observability/content/configure/configure-services.htm) for configuring a service key. @@ -35,4 +35,10 @@ The service key in format `:` for `getSettings` from Solarwinds APM ### interval (Optional) Periodic interval to get Solarwinds APM specific settings from Solarwinds APM collector. -Default: `1m` +Minimum value: `5s` + +Maximum value: `60s` + +Value that is outside the boundary will be bounded to either the minimum or maximum value. + +Default: `10s` diff --git a/extension/solarwindsapmsettingsextension/config.go b/extension/solarwindsapmsettingsextension/config.go index af41f9722c47..70011b4e65d8 100644 --- a/extension/solarwindsapmsettingsextension/config.go +++ b/extension/solarwindsapmsettingsextension/config.go @@ -4,38 +4,67 @@ package solarwindsapmsettingsextension // import "github.com/open-telemetry/opentelemetry-collector-contrib/extension/solarwindsapmsettingsextension" import ( - "errors" - "strconv" + "os" + "regexp" "strings" "time" + + "go.opentelemetry.io/collector/component" ) type Config struct { - Endpoint string `mapstructure:"endpoint"` - Key string `mapstructure:"key"` - Interval string `mapstructure:"interval"` + Endpoint string `mapstructure:"endpoint"` + Key string `mapstructure:"key"` + Interval time.Duration `mapstructure:"interval"` } -func (cfg *Config) Validate() error { - if len(cfg.Endpoint) == 0 { - return errors.New("endpoint must not be empty") - } - endpointArr := strings.Split(cfg.Endpoint, ":") - if len(endpointArr) != 2 { - return errors.New("endpoint should be in \":\" format") - } - if _, err := strconv.Atoi(endpointArr[1]); err != nil { - return errors.New("the portion of endpoint has to be an integer") +const ( + DefaultEndpoint = "apm.collector.na-01.cloud.solarwinds.com:443" + DefaultInterval = time.Duration(10) * time.Second + MinimumInterval = time.Duration(5) * time.Second + MaximumInterval = time.Duration(60) * time.Second +) + +func createDefaultConfig() component.Config { + return &Config{ + Endpoint: DefaultEndpoint, + Interval: DefaultInterval, } - if len(cfg.Key) == 0 { - return errors.New("key must not be empty") +} + +func (cfg *Config) Validate() error { + // Endpoint + matched, _ := regexp.MatchString(`apm.collector.[a-z]{2,3}-[0-9]{2}.[a-z\-]*.solarwinds.com:443`, cfg.Endpoint) + if !matched { + // Replaced by the default + cfg.Endpoint = DefaultEndpoint } + // Key keyArr := strings.Split(cfg.Key, ":") - if len(keyArr) != 2 { - return errors.New("key should be in \":\" format") + if len(keyArr) == 2 && len(keyArr[1]) == 0 { + /** + * Service name is empty. We are trying our best effort to resolve the service name + */ + serviceName := resolveServiceNameBestEffort() + if len(serviceName) > 0 { + cfg.Key = keyArr[0] + ":" + serviceName + } + } + // Interval + if cfg.Interval.Seconds() < MinimumInterval.Seconds() { + cfg.Interval = MinimumInterval } - if _, err := time.ParseDuration(cfg.Interval); err != nil { - return errors.New("interval has to be a duration string. Valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\"") + if cfg.Interval.Seconds() > MaximumInterval.Seconds() { + cfg.Interval = MaximumInterval } return nil } + +func resolveServiceNameBestEffort() string { + if otelServiceName, otelServiceNameDefined := os.LookupEnv("OTEL_SERVICE_NAME"); otelServiceNameDefined && len(otelServiceName) > 0 { + return otelServiceName + } else if awsLambdaFunctionName, awsLambdaFunctionNameDefined := os.LookupEnv("AWS_LAMBDA_FUNCTION_NAME"); awsLambdaFunctionNameDefined && len(awsLambdaFunctionName) > 0 { + return awsLambdaFunctionName + } + return "" +} diff --git a/extension/solarwindsapmsettingsextension/config_test.go b/extension/solarwindsapmsettingsextension/config_test.go index b0860aeacb1f..f87fc59c2353 100644 --- a/extension/solarwindsapmsettingsextension/config_test.go +++ b/extension/solarwindsapmsettingsextension/config_test.go @@ -4,63 +4,85 @@ package solarwindsapmsettingsextension import ( - "errors" + "os" + "path/filepath" "testing" + "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/confmap/confmaptest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/extension/solarwindsapmsettingsextension/internal/metadata" ) -func TestValidate(t *testing.T) { +func TestLoadConfig(t *testing.T) { + t.Parallel() + tests := []struct { - name string - cfg *Config - err error + id component.ID + expected component.Config }{ { - name: "nothing", - cfg: &Config{}, - err: errors.New("endpoint must not be empty"), + id: component.NewID(metadata.Type), + expected: NewFactory().CreateDefaultConfig(), }, { - name: "empty key", - cfg: &Config{ - Endpoint: "host:12345", + id: component.NewIDWithName(metadata.Type, "1"), + expected: &Config{ + Endpoint: "apm.collector.apj-01.cloud.solarwinds.com:443", + Key: "something:name", + Interval: time.Duration(10) * time.Second, }, - err: errors.New("key must not be empty"), }, { - name: "invalid endpoint", - cfg: &Config{ - Endpoint: "invalid", - Key: "token:name", + id: component.NewIDWithName(metadata.Type, "2"), + expected: &Config{ + Endpoint: "apm.collector.na-01.cloud.solarwinds.com:443", + Key: "something", + Interval: time.Duration(5) * time.Second, }, - err: errors.New("endpoint should be in \":\" format"), }, { - name: "invalid endpoint format but port is not an integer", - cfg: &Config{ - Endpoint: "host:abc", - Key: "token:name", + id: component.NewIDWithName(metadata.Type, "3"), + expected: &Config{ + Endpoint: "apm.collector.na-01.cloud.solarwinds.com:443", + Key: "something:name", + Interval: time.Duration(60) * time.Second, }, - err: errors.New("the portion of endpoint has to be an integer"), - }, - { - name: "invalid key", - cfg: &Config{ - Endpoint: "host:12345", - Key: "invalid", - }, - err: errors.New("key should be in \":\" format"), }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := tc.cfg.Validate() - if tc.err != nil { - require.EqualError(t, err, tc.err.Error()) - } else { - require.NoError(t, err) - } + for _, tt := range tests { + t.Run(tt.id.String(), func(t *testing.T) { + cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) + require.NoError(t, err) + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub(tt.id.String()) + require.NoError(t, err) + require.NoError(t, component.UnmarshalConfig(sub, cfg)) + assert.NoError(t, component.ValidateConfig(cfg)) + assert.Equal(t, tt.expected, cfg) }) } } + +func TestResolveServiceNameBestEffort(t *testing.T) { + // Without any environment variables + require.Empty(t, resolveServiceNameBestEffort()) + // With OTEL_SERVICE_NAME only + require.NoError(t, os.Setenv("OTEL_SERVICE_NAME", "otel_ser1")) + require.Equal(t, "otel_ser1", resolveServiceNameBestEffort()) + require.NoError(t, os.Unsetenv("OTEL_SERVICE_NAME")) + // With AWS_LAMBDA_FUNCTION_NAME only + require.NoError(t, os.Setenv("AWS_LAMBDA_FUNCTION_NAME", "lambda")) + require.Equal(t, "lambda", resolveServiceNameBestEffort()) + require.NoError(t, os.Unsetenv("AWS_LAMBDA_FUNCTION_NAME")) + // With both + require.NoError(t, os.Setenv("OTEL_SERVICE_NAME", "otel_ser1")) + require.NoError(t, os.Setenv("AWS_LAMBDA_FUNCTION_NAME", "lambda")) + require.Equal(t, "otel_ser1", resolveServiceNameBestEffort()) + require.NoError(t, os.Unsetenv("AWS_LAMBDA_FUNCTION_NAME")) + require.NoError(t, os.Unsetenv("OTEL_SERVICE_NAME")) +} diff --git a/extension/solarwindsapmsettingsextension/extension.go b/extension/solarwindsapmsettingsextension/extension.go index b3a2b3bb4507..60ba0c88df1a 100644 --- a/extension/solarwindsapmsettingsextension/extension.go +++ b/extension/solarwindsapmsettingsextension/extension.go @@ -5,16 +5,23 @@ package solarwindsapmsettingsextension // import "github.com/open-telemetry/open import ( "context" + "crypto/tls" + "time" + "github.com/solarwindscloud/apm-proto/go/collectorpb" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/extension" "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" ) type solarwindsapmSettingsExtension struct { logger *zap.Logger config *Config cancel context.CancelFunc + conn *grpc.ClientConn + client collectorpb.TraceCollectorClient } func newSolarwindsApmSettingsExtension(extensionCfg *Config, logger *zap.Logger) (extension.Extension, error) { @@ -26,12 +33,49 @@ func newSolarwindsApmSettingsExtension(extensionCfg *Config, logger *zap.Logger) } func (extension *solarwindsapmSettingsExtension) Start(_ context.Context, _ component.Host) error { - extension.logger.Debug("Starting up solarwinds apm settings extension") - _, extension.cancel = context.WithCancel(context.Background()) + extension.logger.Info("Starting up solarwinds apm settings extension") + ctx := context.Background() + ctx, extension.cancel = context.WithCancel(ctx) + var err error + extension.conn, err = grpc.Dial(extension.config.Endpoint, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) + if err != nil { + return err + } + extension.logger.Info("Dailed to endpoint", zap.String("endpoint", extension.config.Endpoint)) + extension.client = collectorpb.NewTraceCollectorClient(extension.conn) + + // initial refresh + refresh(extension) + + go func() { + ticker := time.NewTicker(extension.config.Interval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + refresh(extension) + case <-ctx.Done(): + extension.logger.Info("Received ctx.Done() from ticker") + return + } + } + }() + return nil } func (extension *solarwindsapmSettingsExtension) Shutdown(_ context.Context) error { - extension.logger.Debug("Shutting down solarwinds apm settings extension") + extension.logger.Info("Shutting down solarwinds apm settings extension") + if extension.cancel != nil { + extension.cancel() + } + if extension.conn != nil { + return extension.conn.Close() + } return nil } + +func refresh(extension *solarwindsapmSettingsExtension) { + // Concrete implementation will be available in later PR + extension.logger.Info("refresh task") +} diff --git a/extension/solarwindsapmsettingsextension/extension_test.go b/extension/solarwindsapmsettingsextension/extension_test.go index 8c6d177a1aa8..743f9b89eca7 100644 --- a/extension/solarwindsapmsettingsextension/extension_test.go +++ b/extension/solarwindsapmsettingsextension/extension_test.go @@ -6,6 +6,7 @@ package solarwindsapmsettingsextension import ( "context" "testing" + "time" "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/extension" @@ -13,33 +14,34 @@ import ( ) func TestCreateExtension(t *testing.T) { - conf := &Config{ - Endpoint: "apm-testcollector.click:443", - Key: "valid:unittest", - Interval: "1s", - } - ex := createAnExtension(conf, t) - require.NoError(t, ex.Shutdown(context.TODO())) -} + t.Parallel() -func TestCreateExtensionWrongEndpoint(t *testing.T) { - conf := &Config{ - Endpoint: "apm-testcollector.nothing:443", - Key: "valid:unittest", - Interval: "1s", + tests := []struct { + name string + cfg *Config + }{ + { + name: "default", + cfg: &Config{ + Endpoint: DefaultEndpoint, + Interval: DefaultInterval, + }, + }, + { + name: "anything", + cfg: &Config{ + Endpoint: "apm.collector.na-02.cloud.solarwinds.com:443", + Key: "something:name", + Interval: time.Duration(10) * time.Second, + }, + }, } - ex := createAnExtension(conf, t) - require.NoError(t, ex.Shutdown(context.TODO())) -} - -func TestCreateExtensionWrongKey(t *testing.T) { - conf := &Config{ - Endpoint: "apm-testcollector.click:443", - Key: "invalid", - Interval: "1s", + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ex := createAnExtension(tt.cfg, t) + require.NoError(t, ex.Shutdown(context.TODO())) + }) } - ex := createAnExtension(conf, t) - require.NoError(t, ex.Shutdown(context.TODO())) } // create extension diff --git a/extension/solarwindsapmsettingsextension/factory.go b/extension/solarwindsapmsettingsextension/factory.go index bb8aa2806501..371435c32249 100644 --- a/extension/solarwindsapmsettingsextension/factory.go +++ b/extension/solarwindsapmsettingsextension/factory.go @@ -12,16 +12,6 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/extension/solarwindsapmsettingsextension/internal/metadata" ) -const ( - DefaultInterval = "1m" -) - -func createDefaultConfig() component.Config { - return &Config{ - Interval: DefaultInterval, - } -} - func createExtension(_ context.Context, settings extension.CreateSettings, cfg component.Config) (extension.Extension, error) { return newSolarwindsApmSettingsExtension(cfg.(*Config), settings.Logger) } diff --git a/extension/solarwindsapmsettingsextension/factory_test.go b/extension/solarwindsapmsettingsextension/factory_test.go index e5db55e6bc1a..48f7ee1e35f8 100644 --- a/extension/solarwindsapmsettingsextension/factory_test.go +++ b/extension/solarwindsapmsettingsextension/factory_test.go @@ -17,7 +17,7 @@ func TestCreateDefaultConfig(t *testing.T) { assert.NoError(t, componenttest.CheckConfigStruct(cfg)) ocfg, ok := factory.CreateDefaultConfig().(*Config) assert.True(t, ok) - assert.Empty(t, ocfg.Endpoint, "There is no default endpoint") + assert.Equal(t, ocfg.Endpoint, DefaultEndpoint, "Wrong default endpoint") assert.Empty(t, ocfg.Key, "There is no default key") assert.Equal(t, ocfg.Interval, DefaultInterval, "Wrong default interval") } diff --git a/extension/solarwindsapmsettingsextension/go.mod b/extension/solarwindsapmsettingsextension/go.mod index a282f6efeeee..4f6a773456c3 100644 --- a/extension/solarwindsapmsettingsextension/go.mod +++ b/extension/solarwindsapmsettingsextension/go.mod @@ -3,6 +3,7 @@ module github.com/open-telemetry/opentelemetry-collector-contrib/extension/solar go 1.21.0 require ( + github.com/solarwindscloud/apm-proto v1.0.3 github.com/stretchr/testify v1.9.0 go.opentelemetry.io/collector/component v0.100.1-0.20240509190532-c555005fcc80 go.opentelemetry.io/collector/confmap v0.100.1-0.20240509190532-c555005fcc80 @@ -11,6 +12,7 @@ require ( go.opentelemetry.io/otel/trace v1.26.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.63.2 ) require ( @@ -43,7 +45,6 @@ require ( golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/grpc v1.63.2 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/extension/solarwindsapmsettingsextension/go.sum b/extension/solarwindsapmsettingsextension/go.sum index a51c8c25085e..333819e9dee0 100644 --- a/extension/solarwindsapmsettingsextension/go.sum +++ b/extension/solarwindsapmsettingsextension/go.sum @@ -45,6 +45,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/solarwindscloud/apm-proto v1.0.3 h1:fmPwWNrM5kduAqvmH8mCD0E9MASK8m/mtPmw0yXGOBs= +github.com/solarwindscloud/apm-proto v1.0.3/go.mod h1:PIMzXc8HpB0ryT4Oci4pUz8F0m1X7Q/hVXkQE4jGv6Y= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/extension/solarwindsapmsettingsextension/metadata.yaml b/extension/solarwindsapmsettingsextension/metadata.yaml index 694ffa7014bc..fdd3ca43401a 100644 --- a/extension/solarwindsapmsettingsextension/metadata.yaml +++ b/extension/solarwindsapmsettingsextension/metadata.yaml @@ -8,3 +8,8 @@ status: distributions: [] codeowners: active: [jerrytfleung, cheempz] + +tests: + config: + endpoint: "apm.collector.na-01.cloud.solarwinds.com:443" + diff --git a/extension/solarwindsapmsettingsextension/testdata/config.yaml b/extension/solarwindsapmsettingsextension/testdata/config.yaml new file mode 100644 index 000000000000..4fb5cf5c0417 --- /dev/null +++ b/extension/solarwindsapmsettingsextension/testdata/config.yaml @@ -0,0 +1,13 @@ +solarwindsapmsettings: +solarwindsapmsettings/1: + endpoint: "apm.collector.apj-01.cloud.solarwinds.com:443" + key: "something:name" + interval: 10s +solarwindsapmsettings/2: + endpoint: "apm.collector.eu-01.cloud.solarwinds.com" + key: "something" + interval: "4s" +solarwindsapmsettings/3: + endpoint: "" + key: "something:name" + interval: 61s