Skip to content

Commit

Permalink
[extension/solarwindsapmsettingsextension] Added part one of the conc…
Browse files Browse the repository at this point in the history
…rete implementation of solarwindsapmsettingsextension (open-telemetry#30788)

**Description:** <Describe what has changed.>
- Adding part one of the concrete implementation of
solarwindsapmsettingsextension
- Adding `github.com/solarwindscloud/apm-proto` dependency in go.mod
- Changed the datatype of interval configuration from `string` to
`time.Duration`
- Moved config validation logic from config to extension so as to print
warning instead of shutting down the collector

**Link to tracking Issue:** <Issue number if applicable>

[27668](open-telemetry#27668)

**Testing:** <Describe what testing was performed and which tests were
added.>
- Added testdata to load config.yaml in config_test
- Added parallel test in extension test

**Documentation:** <Describe the documentation added.>
- Updated README about the change of the datatype of `interval` and the
default value
  • Loading branch information
jerrytfleung authored May 22, 2024
1 parent c1f7ba2 commit 6c2fc71
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 101 deletions.
27 changes: 27 additions & 0 deletions .chloggen/feature_solarwindsapmsettingsextension-concreteimpl.yaml
Original file line number Diff line number Diff line change
@@ -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: []
1 change: 1 addition & 0 deletions cmd/otelcontribcol/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions cmd/otelcontribcol/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions extension/solarwindsapmsettingsextension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,22 @@ extensions:
solarwindsapmsettings:
endpoint: "<endpoint>"
key: "<token>:<name>"
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 `<host>:<port>`.

### key (Required)
The service key in format `<token>:<name>` 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.

### 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`
71 changes: 50 additions & 21 deletions extension/solarwindsapmsettingsextension/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 \"<host>:<port>\" format")
}
if _, err := strconv.Atoi(endpointArr[1]); err != nil {
return errors.New("the <port> 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 \"<token>:<service_name>\" 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 ""
}
98 changes: 60 additions & 38 deletions extension/solarwindsapmsettingsextension/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 \"<host>:<port>\" 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 <port> 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 \"<token>:<service_name>\" 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"))
}
50 changes: 47 additions & 3 deletions extension/solarwindsapmsettingsextension/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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")
}
Loading

0 comments on commit 6c2fc71

Please sign in to comment.