diff --git a/CHANGELOG.md b/CHANGELOG.md index ee54498..7d4763e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v0.113.1 +Adds [SolarWinds Extension](./extension/solarwindsextension). The [SolarWinds Exporter](./exporter/solarwindsexporter) is now dependent on the extension. + ## v0.113.0 Initial version of SolarWinds OpenTelemetry Collector. The collector provides all available components (receivers, processors, exporters, connectors, providers) diff --git a/Makefile b/Makefile index d9c71d6..42fa8e2 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ include Makefile.Common ALL_SRC := $(shell find . \( -name "*.go" -o -name "*.sh" \) \ + -not -path '*generated*' \ -type f | sort) .PHONY: ci-check-licenses diff --git a/Makefile.Common b/Makefile.Common index 5576cd6..7da6b61 100644 --- a/Makefile.Common +++ b/Makefile.Common @@ -23,3 +23,12 @@ $(TOOLS_BIN_DIR): $(TOOLS_BIN_NAMES): $(TOOLS_BIN_DIR) $(TOOLS_MOD_DIR)/go.mod cd $(TOOLS_MOD_DIR) && $(GOCMD) build -o $@ -trimpath $(filter %/$(notdir $@),$(TOOLS_PKG_NAMES)) + +.PHONY: test +test: + go test ./... + +.PHONY: generate +generate: + go generate ./... + diff --git a/README.md b/README.md index e3fc7b6..bbdece3 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,13 @@ The SolarWinds OpenTelemetry collector contains following components: - full set of [opentelemetry-collector processors](https://github.com/open-telemetry/opentelemetry-collector/tree/v0.113.0/processor) - full set of [opentelemetry-collector-contrib processors](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.113.0/processor) - exporters + - [`solarwindsexporter`](./exporter/solarwindsexporter) - [`otlpexporter`](https://github.com/open-telemetry/opentelemetry-collector/tree/v0.113.0/exporter/otlpexporter) - [`fileexporter`](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.113.0/exporter/fileexporter) - - [`solarwindsexporter`](./exporter/solarwindsexporter) - [`debugexporter`](https://github.com/open-telemetry/opentelemetry-collector/tree/v0.113.0/exporter/debugexporter) - [`nopexporter`](https://github.com/open-telemetry/opentelemetry-collector/tree/v0.113.0/exporter/nopexporter) - extensions + - [`solarwindsextension`](./extension/solarwindsextension) - full set of [opentelemetry-collector extensions](https://github.com/open-telemetry/opentelemetry-collector/tree/v0.113.0/extension) - full set of [opentelemetry-collector-contrib extensions](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/v0.113.0/extension) - connectors diff --git a/build/docker/Dockerfile b/build/docker/Dockerfile index c0f6dbd..af89a10 100644 --- a/build/docker/Dockerfile +++ b/build/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/golang:1.22.7-bookworm AS base +FROM docker.io/library/golang:1.23.4-bookworm AS base COPY /LICENSE /LICENSE COPY ./ /src @@ -8,7 +8,7 @@ FROM base AS builder RUN cd /src/cmd/solarwinds-otel-collector && CGO_ENABLED=0 GOEXPERIMENT=boringcrypto go build -trimpath -o /src/bin/solarwinds-otel-collector "-ldflags=-s -w" FROM builder AS tests -WORKDIR src +WORKDIR /src # run tests for go modules of all maintained components # image build is stopped if test failure is detected RUN find . -name go.mod -not -path "./cmd/solarwinds-otel-collector/*" -execdir go test ./... \; | ( ! grep FAIL ) diff --git a/cmd/solarwinds-otel-collector/components.go b/cmd/solarwinds-otel-collector/components.go index fed480e..2293a17 100644 --- a/cmd/solarwinds-otel-collector/components.go +++ b/cmd/solarwinds-otel-collector/components.go @@ -17,6 +17,7 @@ package main import ( "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/fileexporter" "github.com/solarwinds/solarwinds-otel-collector/exporter/solarwindsexporter" + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension" "go.opentelemetry.io/collector/exporter/debugexporter" "go.opentelemetry.io/collector/exporter/nopexporter" "go.opentelemetry.io/collector/exporter/otlpexporter" @@ -219,6 +220,7 @@ func components() (otelcol.Factories, error) { sigv4authextension.NewFactory(), solarwindsapmsettingsextension.NewFactory(), sumologicextension.NewFactory(), + solarwindsextension.NewFactory(), ) if err != nil { diff --git a/cmd/solarwinds-otel-collector/go.mod b/cmd/solarwinds-otel-collector/go.mod index c23d4bc..8bb517e 100644 --- a/cmd/solarwinds-otel-collector/go.mod +++ b/cmd/solarwinds-otel-collector/go.mod @@ -1,8 +1,6 @@ module github.com/solarwinds/solarwinds-otel-collector -go 1.22.7 - -toolchain go1.22.9 +go 1.23.4 require ( github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/aesprovider v0.113.0 @@ -161,8 +159,9 @@ require ( github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zipkinreceiver v0.113.0 github.com/open-telemetry/opentelemetry-collector-contrib/receiver/zookeeperreceiver v0.113.0 github.com/solarwinds/solarwinds-otel-collector/exporter/solarwindsexporter v0.113.0 + github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension v0.113.0 go.opentelemetry.io/collector/component v0.113.0 - go.opentelemetry.io/collector/confmap v1.19.0 + go.opentelemetry.io/collector/confmap v1.21.0 go.opentelemetry.io/collector/confmap/provider/envprovider v1.19.0 go.opentelemetry.io/collector/confmap/provider/fileprovider v1.19.0 go.opentelemetry.io/collector/confmap/provider/httpprovider v1.19.0 @@ -712,3 +711,7 @@ require ( ) replace github.com/solarwinds/solarwinds-otel-collector/exporter/solarwindsexporter => ../../exporter/solarwindsexporter + +replace github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension => ../../extension/solarwindsextension + +replace github.com/solarwinds/solarwinds-otel-collector/pkg/testutil => ../../pkg/testutil diff --git a/cmd/solarwinds-otel-collector/go.sum b/cmd/solarwinds-otel-collector/go.sum index 2f68302..d73c7c2 100644 --- a/cmd/solarwinds-otel-collector/go.sum +++ b/cmd/solarwinds-otel-collector/go.sum @@ -2542,8 +2542,8 @@ go.opentelemetry.io/collector/config/configtls v1.19.0 h1:GQ/cF1hgNqHVBq2oSSrOFX go.opentelemetry.io/collector/config/configtls v1.19.0/go.mod h1:1hyqnYB3JqEUlk1ME/s9HYz4oCRcxQCRxsJitFFT/cA= go.opentelemetry.io/collector/config/internal v0.114.0 h1:uWSDWTJb8T6xRjKD9/XmEARakXnxgYVYKUeId78hErc= go.opentelemetry.io/collector/config/internal v0.114.0/go.mod h1:yC7E4h1Uj0SubxcFImh6OvBHFTjMh99+A5PuyIgDWqc= -go.opentelemetry.io/collector/confmap v1.19.0 h1:TQ0lZpAKqgsE0EKk+u4JA+uBbPYeFRmWP3GH43w40CY= -go.opentelemetry.io/collector/confmap v1.19.0/go.mod h1:GgNu1ElPGmLn9govqIfjaopvdspw4PJ9KeDtWC4E2Q4= +go.opentelemetry.io/collector/confmap v1.21.0 h1:1tIcx2/Suwg8VhuPmQw87ba0ludPmumpFCFRZZa6RXA= +go.opentelemetry.io/collector/confmap v1.21.0/go.mod h1:Rrhs+MWoaP6AswZp+ReQ2VO9dfOfcUjdjiSHBsG+nec= go.opentelemetry.io/collector/confmap/provider/envprovider v1.19.0 h1:f8O/I5pVRN86Gx5mHekNx92S6fGdOS4VcooRJKWe6Bs= go.opentelemetry.io/collector/confmap/provider/envprovider v1.19.0/go.mod h1:AiaW5YW1LD0/WlZuc8eZuZPBH6PA9QqsiAYRX1iC6T0= go.opentelemetry.io/collector/confmap/provider/fileprovider v1.19.0 h1:TYwyk4ea3U+5MYcEjrzZAaonBcLlabQu8CZeB7ekAYY= diff --git a/cmd/solarwinds-otel-collector/main.go b/cmd/solarwinds-otel-collector/main.go index 2dc4d1c..3919c58 100644 --- a/cmd/solarwinds-otel-collector/main.go +++ b/cmd/solarwinds-otel-collector/main.go @@ -16,6 +16,8 @@ package main import ( + "log" + "github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/aesprovider" "github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/s3provider" "github.com/open-telemetry/opentelemetry-collector-contrib/confmap/provider/secretsmanagerprovider" @@ -27,7 +29,6 @@ import ( "go.opentelemetry.io/collector/confmap/provider/httpsprovider" "go.opentelemetry.io/collector/confmap/provider/yamlprovider" "go.opentelemetry.io/collector/otelcol" - "log" ) func main() { diff --git a/exporter/solarwindsexporter/Makefile b/exporter/solarwindsexporter/Makefile index 60d73c6..ded7a36 100644 --- a/exporter/solarwindsexporter/Makefile +++ b/exporter/solarwindsexporter/Makefile @@ -1,7 +1 @@ -.PHONY: test -test: - go test ./... - -.PHONY: generate -generate: - go generate ./... +include ../../Makefile.Common diff --git a/exporter/solarwindsexporter/README.md b/exporter/solarwindsexporter/README.md index 553db01..b671e88 100644 --- a/exporter/solarwindsexporter/README.md +++ b/exporter/solarwindsexporter/README.md @@ -13,16 +13,16 @@ SolarWinds Exporter is a convenience wrapper around [OTLP gRPC Exporter](https:/ ## Getting Started -You just need to include the SolarWinds Exporter in your exporter definitions and provide the following minimal configuration: +You just need to include the SolarWinds Exporter in your exporter definitions and no additional configuration is needed. It always needs to be used together with the [Solarwinds Extension](../../extension/solarwindsextension). ```yaml exporters: solarwinds: - token: "YOUR-INGESTION-TOKEN" +extensions: + solarwinds: + token: "TOKEN" data_center: "na-01" ``` -- `token` (mandatory) - You can generate your token in your SolarWinds Observability SaaS account under _Settings / API Tokens / Create API Token_. The type is "Ingestion". You can find the complete documentation [here](https://documentation.solarwinds.com/en/success_center/observability/content/settings/api-tokens.htm). -- `data_center` (mandatory) - Data center is the region you picked during the sign-up process. You can easily see in URLs after logging in to SolarWinds Observability SaaS - it's either `na-01`, `na-02` or `eu-01`. Please refer to the [documentation](https://documentation.solarwinds.com/en/success_center/observability/content/system_requirements/endpoints.htm#Find) for details. ## Full configuration @@ -30,8 +30,7 @@ exporters: ```yaml exporters: solarwinds: - token: "YOUR-INGESTION-TOKEN" # No default (mandatory field). - data_center: "na-01" # No default (mandatory field). + extension: "solarwinds" timeout: "10s" sending_queue: enabled: true @@ -44,14 +43,43 @@ exporters: multiplier: 1.5 max_interval: "30s" max_elapsed_time: "300s" +extensions: + solarwinds: + token: "TOKEN" + data_center: "na-01" ``` -- `timeout` (optional) - Timeout for each attempt to send data to the SaaS service. A timeout of zero disables the timeout. The **default** is `5s`. +> [!TIP] +> You can omit `extension` from the Solarwinds Exporter configuration above if there's only a single instance of the Solarwinds Extension. + +- `extension` (optional) - This name identifies an instance of the [Solarwinds Extension](../../extension/solarwindsextension) to be used by this exporter to obtain its configuration. + If there is only a single instance of the extension, the configuration value is optional. The format mimics the identifier as it occurs in the collector configuration - + `type/name`, e.g `solarwinds` or `solarwinds/1` for multiple instances of the extension. You would use multiple instances for publishing your telemetry to + multiple **SolarWinds Observability SaaS** organizations. +- `timeout` (optional) - Timeout for each attempt to send data to the SaaS service. A timeout of zero disables the timeout. The **default** is `10s`. - `retry_on_failure` (optional) - These options configure the retry behavior. Please refer to the [Exporter Helper documentation](https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/exporterhelper/README.md). - `sending_queue` (optional) - These are the options to set queuing in the exporter. A full descriptions can be similarly found in [Exporter Helper documentation](https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/exporterhelper/README.md). > [!NOTE] > The format of all durations above follow the [time.ParseDuration](https://pkg.go.dev/time#ParseDuration) format of "Duration" strings. +### Example with Multiple Solarwinds Extensions +```yaml +exporters: + solarwinds: + extension: "solarwinds/1" +extensions: + solarwinds/1: + token: YOUR-INGESTION-TOKEN1" + data_center: "na-01" + solarwinds/2: + token: YOUR-INGESTION-TOKEN2" + data_center: "na-02" +``` +> [!WARNING] +> The `extension` configuration value cannot be omitted in the example above. +> There are multiple instances of the Solarwinds Extension and you need to +> configure which instance to use to obtain configuration for the exporter. + ## Development - **Tests** can be executed with `make test`. - After changes to `metadata.yaml` generated files need to be re-generated with `make generate`. The [mdatagen](http://go.opentelemetry.io/collector/cmd/mdatagen) tool has to be in the `PATH`. diff --git a/exporter/solarwindsexporter/config.go b/exporter/solarwindsexporter/config.go index 3ca634f..be26c97 100644 --- a/exporter/solarwindsexporter/config.go +++ b/exporter/solarwindsexporter/config.go @@ -15,9 +15,7 @@ package solarwindsexporter import ( - "errors" "fmt" - "strings" "time" "go.opentelemetry.io/collector/component" @@ -29,37 +27,11 @@ import ( "go.opentelemetry.io/collector/exporter/otlpexporter" ) -// dataCenterToURLMapping maps a data center ID to -// to its corresponding OTLP endpoint URL. -var dataCenterToURLMapping = map[string]string{ - "na-01": "otel.collector.na-01.cloud.solarwinds.com:443", - "na-02": "otel.collector.na-02.cloud.solarwinds.com:443", - "eu-01": "otel.collector.eu-01.cloud.solarwinds.com:443", -} - -// lookupDataCenterURL returns the OTLP endpoint URL -// for a `dc` data center ID. Matching is case-insensitive. -// It fails with an error if `dc` doesn't identify a data center. -func lookupDataCenterURL(dc string) (string, error) { - dcLowercase := strings.ToLower(dc) - - url, ok := dataCenterToURLMapping[dcLowercase] - if !ok { - return "", fmt.Errorf("unknown data center ID: %s", dc) - } - - return url, nil -} - // Config represents a Solarwinds Exporter configuration. type Config struct { - // DataCenter ID (e.g. na-01). - DataCenter string `mapstructure:"data_center"` - // EndpointURLOverride sets OTLP endpoint directly. - // Warning: Intended for testing use only, use `DataCenter` instead. - EndpointURLOverride string `mapstructure:"endpoint_url_override"` - // IngestionToken is your secret generated SWO ingestion token. - IngestionToken configopaque.String `mapstructure:"token"` + // Extension identifies a Solarwinds Extension to + // use for obtaining connection credentials in this exporter. + Extension string `mapstructure:"extension"` // BackoffSettings configures retry behavior of the exporter. // See [configretry.BackOffConfig] documentation. BackoffSettings configretry.BackOffConfig `mapstructure:"retry_on_failure"` @@ -68,12 +40,33 @@ type Config struct { QueueSettings exporterhelper.QueueConfig `mapstructure:"sending_queue"` // Timeout configures timeout in the underlying OTLP exporter. Timeout exporterhelper.TimeoutConfig `mapstructure:"timeout,squash"` + + // ingestionToken stores the token provided by the Solarwinds Extension. + ingestionToken configopaque.String `mapstructure:"-"` + // endpointURL stores the URL provided by the Solarwinds Extension. + endpointURL string `mapstructure:"-"` + // insecure stores the option to disable TLS provided + // by the Solarwinds Extension. + insecure bool `mapstructure:"-"` } -// NewDefaultConfig creates a new default configuration. +// extensionAsComponent tries to parse `extension` value of the form 'type/name' +// or 'type' from the configuration to [component.ID]. If the `extension value is empty, +// it returns `nil` with a `nil` error. // -// Warning: it doesn't define mandatory `Token` and `DataCenter` -// fields that need to be explicitly provided. +// It uses [component.ID.UnmarshalText] and behaves accordingly. +func (cfg *Config) extensionAsComponent() (*component.ID, error) { + if cfg.Extension == "" { + return nil, nil + } + + parsedID := &component.ID{} + err := parsedID.UnmarshalText([]byte(cfg.Extension)) + + return parsedID, err +} + +// NewDefaultConfig creates a new default configuration. func NewDefaultConfig() component.Config { // Using a higher default than OTLP Exporter does (5s) // based on previous experience with unnecessary timeouts. @@ -90,16 +83,14 @@ func NewDefaultConfig() component.Config { // Validate checks the configuration for its validity. func (cfg *Config) Validate() error { - if cfg.DataCenter == "" && cfg.EndpointURLOverride == "" { - return errors.New("invalid configuration: data center must be provided") - } - - if _, err := lookupDataCenterURL(cfg.DataCenter); err != nil { - return fmt.Errorf("invalid data center ID: %w", err) - } - - if cfg.IngestionToken == "" { - return errors.New("invalid configuration: token must be set") + if len(cfg.Extension) != 0 { + _, err := cfg.extensionAsComponent() + if err != nil { + return fmt.Errorf( + "invalid configuration: %q is not a correct value for 'extension'", + cfg.Extension, + ) + } } return nil @@ -111,15 +102,8 @@ func (cfg *Config) OTLPConfig() (*otlpexporter.Config, error) { return nil, err } - // Use overridden URL if provided. - endpointURL := cfg.EndpointURLOverride - if endpointURL == "" { - // Error doesn't need to be checked, it's been validated above. - endpointURL, _ = lookupDataCenterURL(cfg.DataCenter) - } - // Headers - set bearer auth. - bearer := fmt.Sprintf("Bearer %s", string(cfg.IngestionToken)) + bearer := fmt.Sprintf("Bearer %s", string(cfg.ingestionToken)) headers := map[string]configopaque.String{ "Authorization": configopaque.String(bearer), } @@ -130,7 +114,7 @@ func (cfg *Config) OTLPConfig() (*otlpexporter.Config, error) { Keepalive: configgrpc.NewDefaultKeepaliveClientConfig(), BalancerName: configgrpc.BalancerName(), Headers: headers, - Endpoint: endpointURL, + Endpoint: cfg.endpointURL, } otlpConfig := &otlpexporter.Config{ @@ -140,6 +124,9 @@ func (cfg *Config) OTLPConfig() (*otlpexporter.Config, error) { ClientConfig: clientCfg, } + // Disable TLS for testing. + otlpConfig.ClientConfig.TLSSetting.Insecure = cfg.insecure + if err := otlpConfig.Validate(); err != nil { return nil, err } diff --git a/exporter/solarwindsexporter/config_test.go b/exporter/solarwindsexporter/config_test.go index 37f7d9e..c923dcd 100644 --- a/exporter/solarwindsexporter/config_test.go +++ b/exporter/solarwindsexporter/config_test.go @@ -15,35 +15,20 @@ package solarwindsexporter import ( - "fmt" - "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/collector/config/configopaque" "go.opentelemetry.io/collector/config/configretry" - "go.opentelemetry.io/collector/confmap" - "go.opentelemetry.io/collector/confmap/confmaptest" "go.opentelemetry.io/collector/exporter/exporterhelper" -) - -// loadConfigTestdata is a helper function to load a testdata -// file by its name. -func loadConfigTestdata(t *testing.T, name string) *confmap.Conf { - t.Helper() - - filename := fmt.Sprintf("%s.yaml", name) - cm, err := confmaptest.LoadConf(filepath.Join("testdata", filename)) - require.NoError(t, err) - return cm -} + "github.com/solarwinds/solarwinds-otel-collector/pkg/testutil" +) // TestConfigUnmarshalFull tries to parse a configuration file // with all values provided and verifies the configuration. func TestConfigUnmarshalFull(t *testing.T) { - cfgFile := loadConfigTestdata(t, "full") + cfgFile := testutil.LoadConfigTestdata(t, "full") // Parse configuration. factory := NewFactory() @@ -52,9 +37,7 @@ func TestConfigUnmarshalFull(t *testing.T) { // Verify the values. assert.Equal(t, &Config{ - DataCenter: "na-01", - EndpointURLOverride: "127.0.0.1:1234", - IngestionToken: "TOKEN", + Extension: "solarwinds/1", BackoffSettings: configretry.BackOffConfig{ Enabled: false, InitialInterval: 15000000000, @@ -78,7 +61,7 @@ func TestConfigUnmarshalFull(t *testing.T) { // file containing only the mandatory values successfully // validates. func TestConfigValidateOK(t *testing.T) { - cfgFile := loadConfigTestdata(t, "minimal") + cfgFile := testutil.LoadConfigTestdata(t, "minimal") // Parse configuration. factory := NewFactory() @@ -89,107 +72,32 @@ func TestConfigValidateOK(t *testing.T) { assert.NoError(t, cfg.(*Config).Validate()) } -// TestConfigValidateMissingToken verifies that -// the validation of a configuration file with -// the token missing fails as expected. -func TestConfigValidateMissingToken(t *testing.T) { - cfgFile := loadConfigTestdata(t, "missing_token") - - // Parse configuration. - factory := NewFactory() - cfg := factory.CreateDefaultConfig() - require.NoError(t, cfgFile.Unmarshal(&cfg)) - - assert.ErrorContains( - t, - cfg.(*Config).Validate(), - "invalid configuration: token must be set", - ) -} - -// TestConfigValidateMissingDataCenter verifies that -// the validation of a configuration file with -// the dataCenter ID missing fails as expected. -func TestConfigValidateMissingDataCenter(t *testing.T) { - cfgFile := loadConfigTestdata(t, "missing_dc") +// TestConfigValidateNOK. +func TestConfigValidateNOK(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "invalid") // Parse configuration. factory := NewFactory() cfg := factory.CreateDefaultConfig() require.NoError(t, cfgFile.Unmarshal(&cfg)) + // Validation should fail with an error. assert.ErrorContains( t, cfg.(*Config).Validate(), - "invalid configuration: data center must be provided", + "invalid configuration", ) } -// TestConfigValidateDataCenters verifies mappings -// for data centers (the mapping is case-insensitive). -func TestConfigValidateDataCenters(t *testing.T) { - type test struct { - dataCenter string - url string - ok bool - } - - tests := []test{ - {dataCenter: "na-01", url: "otel.collector.na-01.cloud.solarwinds.com:443", ok: true}, - {dataCenter: "na-02", url: "otel.collector.na-02.cloud.solarwinds.com:443", ok: true}, - {dataCenter: "eu-01", url: "otel.collector.eu-01.cloud.solarwinds.com:443", ok: true}, - {dataCenter: "NA-01", url: "otel.collector.na-01.cloud.solarwinds.com:443", ok: true}, - {dataCenter: "apj-01", url: "", ok: false}, - } - - for _, tc := range tests { - // Try to find a dataCenter URL for its ID. - url, err := lookupDataCenterURL(tc.dataCenter) - - if tc.ok { // A URL should be returned. - require.NoError(t, err) - assert.Equal(t, tc.url, url) - } else { // It should fail. - assert.ErrorContains(t, err, "unknown data center ID") - } - } -} - // TestConfigTokenRedacted checks that the configuration // type doesn't leak its secret token unless it is accessed explicitly. func TestConfigTokenRedacted(t *testing.T) { cfg := &Config{ - DataCenter: "eu-01", - IngestionToken: "SECRET", + ingestionToken: "SECRET", } // This is the only way of accessing the actual token. - require.Equal(t, "SECRET", string(cfg.IngestionToken)) + require.Equal(t, "SECRET", string(cfg.ingestionToken)) // It is redacted when printed. - assert.Equal(t, "[REDACTED]", cfg.IngestionToken.String()) -} - -// TestConfigOTLPWithOverride converts exporter configuration to -// OTLP gRPC Exporter configuration and verifies that overridden -// endpoint and token propagate correctly. -func TestConfigOTLPWithOverride(t *testing.T) { - cfgFile := loadConfigTestdata(t, "url_override") - - // Parse configuration. - factory := NewFactory() - cfg := factory.CreateDefaultConfig() - require.NoError(t, cfgFile.Unmarshal(&cfg)) - - // Convert it to the OTLP Exporter configuration. - otlpCfg, err := cfg.(*Config).OTLPConfig() - require.NoError(t, err) - - // Verify that both the token and overridden URL were propagated - // to the OTLP configuration. - assert.Equal(t, "127.0.0.1:1234", otlpCfg.Endpoint) - assert.Equal( - t, - map[string]configopaque.String{"Authorization": "Bearer YOUR-INGESTION-TOKEN"}, - otlpCfg.Headers, - ) + assert.Equal(t, "[REDACTED]", cfg.ingestionToken.String()) } diff --git a/exporter/solarwindsexporter/factory.go b/exporter/solarwindsexporter/factory.go index 0cae0e3..b2c4c50 100644 --- a/exporter/solarwindsexporter/factory.go +++ b/exporter/solarwindsexporter/factory.go @@ -45,7 +45,10 @@ func createMetricsExporter( return nil, fmt.Errorf("unexpected config type: %T", cfg) } - metricsExporter := newExporter(ctx, exporterCfg, settings, metricsExporterType) + metricsExporter, err := newExporter(exporterCfg, settings, metricsExporterType) + if err != nil { + return nil, fmt.Errorf("failed to create exporter: %w", err) + } return exporterhelper.NewMetrics( ctx, @@ -70,7 +73,11 @@ func createLogsExporter( return nil, fmt.Errorf("unexpected config type: %T", cfg) } - logsExporter := newExporter(ctx, exporterCfg, settings, logsExporterType) + logsExporter, err := newExporter(exporterCfg, settings, logsExporterType) + if err != nil { + return nil, err + } + return exporterhelper.NewLogs( ctx, settings, @@ -92,7 +99,10 @@ func createTracesExporter(ctx context.Context, return nil, fmt.Errorf("unexpected config type: %T", cfg) } - tracesExporter := newExporter(ctx, exporterCfg, settings, tracesExporterType) + tracesExporter, err := newExporter(exporterCfg, settings, tracesExporterType) + if err != nil { + return nil, err + } return exporterhelper.NewTraces( ctx, diff --git a/exporter/solarwindsexporter/generated_component_test.go b/exporter/solarwindsexporter/generated_component_test.go index 1fa055f..70cb777 100644 --- a/exporter/solarwindsexporter/generated_component_test.go +++ b/exporter/solarwindsexporter/generated_component_test.go @@ -1,17 +1,3 @@ -// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. -// -// 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. - // Code generated by mdatagen. DO NOT EDIT. package solarwindsexporter @@ -85,46 +71,6 @@ func TestComponentLifecycle(t *testing.T) { err = c.Shutdown(context.Background()) require.NoError(t, err) }) - t.Run(tt.name+"-lifecycle", func(t *testing.T) { - c, err := tt.createFn(context.Background(), exportertest.NewNopSettings(), cfg) - require.NoError(t, err) - host := componenttest.NewNopHost() - err = c.Start(context.Background(), host) - require.NoError(t, err) - require.NotPanics(t, func() { - switch tt.name { - case "logs": - e, ok := c.(exporter.Logs) - require.True(t, ok) - logs := generateLifecycleTestLogs() - if !e.Capabilities().MutatesData { - logs.MarkReadOnly() - } - err = e.ConsumeLogs(context.Background(), logs) - case "metrics": - e, ok := c.(exporter.Metrics) - require.True(t, ok) - metrics := generateLifecycleTestMetrics() - if !e.Capabilities().MutatesData { - metrics.MarkReadOnly() - } - err = e.ConsumeMetrics(context.Background(), metrics) - case "traces": - e, ok := c.(exporter.Traces) - require.True(t, ok) - traces := generateLifecycleTestTraces() - if !e.Capabilities().MutatesData { - traces.MarkReadOnly() - } - err = e.ConsumeTraces(context.Background(), traces) - } - }) - - require.NoError(t, err) - - err = c.Shutdown(context.Background()) - require.NoError(t, err) - }) } } diff --git a/exporter/solarwindsexporter/generated_package_test.go b/exporter/solarwindsexporter/generated_package_test.go index 9d890da..8bb93fb 100644 --- a/exporter/solarwindsexporter/generated_package_test.go +++ b/exporter/solarwindsexporter/generated_package_test.go @@ -1,17 +1,3 @@ -// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. -// -// 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. - // Code generated by mdatagen. DO NOT EDIT. package solarwindsexporter diff --git a/exporter/solarwindsexporter/go.mod b/exporter/solarwindsexporter/go.mod index cd802b2..1c9e4a7 100644 --- a/exporter/solarwindsexporter/go.mod +++ b/exporter/solarwindsexporter/go.mod @@ -1,15 +1,17 @@ module github.com/solarwinds/solarwinds-otel-collector/exporter/solarwindsexporter -go 1.22.7 +go 1.23.4 require ( + github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension v0.113.0 + github.com/solarwinds/solarwinds-otel-collector/pkg/testutil v0.0.0-00010101000000-000000000000 github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/component v0.113.0 go.opentelemetry.io/collector/config/configgrpc v0.113.0 go.opentelemetry.io/collector/config/configopaque v1.19.0 go.opentelemetry.io/collector/config/configretry v1.19.0 go.opentelemetry.io/collector/config/configtls v1.19.0 - go.opentelemetry.io/collector/confmap v1.19.0 + go.opentelemetry.io/collector/confmap v1.21.0 go.opentelemetry.io/collector/exporter v0.113.0 go.opentelemetry.io/collector/exporter/exportertest v0.113.0 go.opentelemetry.io/collector/exporter/otlpexporter v0.113.0 @@ -76,3 +78,7 @@ require ( google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension => ../../extension/solarwindsextension + +replace github.com/solarwinds/solarwinds-otel-collector/pkg/testutil => ../../pkg/testutil diff --git a/exporter/solarwindsexporter/go.sum b/exporter/solarwindsexporter/go.sum index 5c8bbed..21684c7 100644 --- a/exporter/solarwindsexporter/go.sum +++ b/exporter/solarwindsexporter/go.sum @@ -52,8 +52,8 @@ github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mL github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= @@ -84,8 +84,8 @@ go.opentelemetry.io/collector/config/configtls v1.19.0 h1:GQ/cF1hgNqHVBq2oSSrOFX go.opentelemetry.io/collector/config/configtls v1.19.0/go.mod h1:1hyqnYB3JqEUlk1ME/s9HYz4oCRcxQCRxsJitFFT/cA= go.opentelemetry.io/collector/config/internal v0.114.0 h1:uWSDWTJb8T6xRjKD9/XmEARakXnxgYVYKUeId78hErc= go.opentelemetry.io/collector/config/internal v0.114.0/go.mod h1:yC7E4h1Uj0SubxcFImh6OvBHFTjMh99+A5PuyIgDWqc= -go.opentelemetry.io/collector/confmap v1.19.0 h1:TQ0lZpAKqgsE0EKk+u4JA+uBbPYeFRmWP3GH43w40CY= -go.opentelemetry.io/collector/confmap v1.19.0/go.mod h1:GgNu1ElPGmLn9govqIfjaopvdspw4PJ9KeDtWC4E2Q4= +go.opentelemetry.io/collector/confmap v1.21.0 h1:1tIcx2/Suwg8VhuPmQw87ba0ludPmumpFCFRZZa6RXA= +go.opentelemetry.io/collector/confmap v1.21.0/go.mod h1:Rrhs+MWoaP6AswZp+ReQ2VO9dfOfcUjdjiSHBsG+nec= go.opentelemetry.io/collector/consumer v0.113.0 h1:KJSiK5vSIY9dgPxwKfQ3gOgKtQsqc+7IB7mGhUAL5c8= go.opentelemetry.io/collector/consumer v0.113.0/go.mod h1:zHMlXYFaJlZoLCBR6UwWoyXZ/adcO1u2ydqUal3VmYU= go.opentelemetry.io/collector/consumer/consumererror v0.113.0 h1:Hd2N7n9RKbnKRaVrdw6fPBoQko5zZIgCxwVxkL6SAIE= diff --git a/exporter/solarwindsexporter/internal/metadata/generated_status.go b/exporter/solarwindsexporter/internal/metadata/generated_status.go index 30617e7..2147cab 100644 --- a/exporter/solarwindsexporter/internal/metadata/generated_status.go +++ b/exporter/solarwindsexporter/internal/metadata/generated_status.go @@ -1,17 +1,3 @@ -// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. -// -// 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. - // Code generated by mdatagen. DO NOT EDIT. package metadata diff --git a/exporter/solarwindsexporter/metadata.yaml b/exporter/solarwindsexporter/metadata.yaml index fa53e04..5b420e7 100644 --- a/exporter/solarwindsexporter/metadata.yaml +++ b/exporter/solarwindsexporter/metadata.yaml @@ -7,6 +7,6 @@ status: development: [traces, metrics, logs] tests: - config: - data_center: "na-01" - token: "THIS-IS-A-TEST" + # skipped because the exporter requires solarwindsextension to run + skip_lifecycle: true + diff --git a/exporter/solarwindsexporter/solarwinds_exporter.go b/exporter/solarwindsexporter/solarwinds_exporter.go index 7f9a744..a0a986f 100644 --- a/exporter/solarwindsexporter/solarwinds_exporter.go +++ b/exporter/solarwindsexporter/solarwinds_exporter.go @@ -16,8 +16,11 @@ package solarwindsexporter import ( "context" + "errors" "fmt" + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension" + "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/exporter" "go.opentelemetry.io/collector/exporter/otlpexporter" @@ -34,58 +37,96 @@ const ( tracesExporterType ) +var ( + ErrSwiExtensionNotFound = errors.New("solarwinds extension not found") +) + type solarwindsExporter struct { exporterType config *Config - settings component.TelemetrySettings + settings exporter.Settings metrics exporter.Metrics logs exporter.Logs traces exporter.Traces } func newExporter( - ctx context.Context, cfg *Config, settings exporter.Settings, typ exporterType, -) *solarwindsExporter { +) (*solarwindsExporter, error) { if err := cfg.Validate(); err != nil { - panic(err) + return nil, fmt.Errorf("validation of configuration failed: %w", err) } swiExporter := &solarwindsExporter{ - config: cfg, - settings: settings.TelemetrySettings, - } - if err := swiExporter.initExporterType(ctx, settings, typ); err != nil { - panic(err) + exporterType: typ, + config: cfg, + settings: settings, } - return swiExporter + return swiExporter, nil } -func (e *solarwindsExporter) initExporterType( +func (swiExporter *solarwindsExporter) initExporterType( ctx context.Context, settings exporter.Settings, + host component.Host, typ exporterType, ) error { - e.exporterType = typ + swiExporter.exporterType = typ + extensionID, err := swiExporter.config.extensionAsComponent() + if err != nil { + return fmt.Errorf("failed parsing extension id: %w", err) + } + + // Only allow the type of the [solarwindsextension]. + if extensionID != nil && + extensionID.Type() != solarwindsextension.NewFactory().Type() { + return fmt.Errorf("unexpected extension type: %s", extensionID.Type()) + } + + swiExtension := findExtension(host.GetExtensions(), extensionID) + if swiExtension == nil { + if extensionID != nil { + return fmt.Errorf("solarwinds extension %q not found", extensionID) + } + return ErrSwiExtensionNotFound + } + + endpointCfg := swiExtension.GetEndpointConfig() + + // Get token from the extensions. + token := endpointCfg.Token() + swiExporter.config.ingestionToken = token + + // Get URL from the extension. + url, err := endpointCfg.Url() + if err != nil { + return fmt.Errorf("URL configuration not available: %w", err) + } + swiExporter.config.endpointURL = url + + // Get TLS settings for testing. + insecure := endpointCfg.Insecure() + swiExporter.config.insecure = insecure + otlpExporter := otlpexporter.NewFactory() - otlpCfg, err := e.config.OTLPConfig() + otlpCfg, err := swiExporter.config.OTLPConfig() if err != nil { return err } switch typ { case metricsExporterType: - e.metrics, err = otlpExporter.CreateMetrics(ctx, settings, otlpCfg) + swiExporter.metrics, err = otlpExporter.CreateMetrics(ctx, settings, otlpCfg) return err case logsExporterType: - e.logs, err = otlpExporter.CreateLogs(ctx, settings, otlpCfg) + swiExporter.logs, err = otlpExporter.CreateLogs(ctx, settings, otlpCfg) return err case tracesExporterType: - e.traces, err = otlpExporter.CreateTraces(ctx, settings, otlpCfg) + swiExporter.traces, err = otlpExporter.CreateTraces(ctx, settings, otlpCfg) return err default: return fmt.Errorf("unknown exporter type: %v", typ) @@ -93,52 +134,98 @@ func (e *solarwindsExporter) initExporterType( } -func (e *solarwindsExporter) start(ctx context.Context, host component.Host) error { - switch e.exporterType { +// findExtension returns a found Solarwinds Extension or nil +// if not found. Respecting these rules: +// - If the name is provided and it's found, return it. +// - If no name is provided and there's only one Solarwinds Extension, +// return it. +// - Otherwise, return nil. +func findExtension( + extensions map[component.ID]component.Component, + cfgExtensionID *component.ID, +) *solarwindsextension.SolarwindsExtension { + foundExtensions := make([]*solarwindsextension.SolarwindsExtension, 0) + + for foundExtensionID, ext := range extensions { + if swiExtension, ok := ext.(*solarwindsextension.SolarwindsExtension); ok { + // If configured extension ID is found, return it. + if cfgExtensionID != nil && *cfgExtensionID == foundExtensionID { + return swiExtension + } + + // Otherwise, store it to the slice. + foundExtensions = append(foundExtensions, swiExtension) + } + } + + // If no extension name configured and there is only one + // found matching the type, return it. + if len(foundExtensions) == 1 && cfgExtensionID == nil { + return foundExtensions[0] + } + + return nil +} + +func (swiExporter *solarwindsExporter) start(ctx context.Context, host component.Host) error { + if err := swiExporter.initExporterType(ctx, swiExporter.settings, host, swiExporter.exporterType); err != nil { + return fmt.Errorf("failed to initialiaze exporter: %w", err) + } + + switch swiExporter.exporterType { case metricsExporterType: - return e.metrics.Start(ctx, host) + return swiExporter.metrics.Start(ctx, host) case logsExporterType: - return e.logs.Start(ctx, host) + return swiExporter.logs.Start(ctx, host) case tracesExporterType: - return e.traces.Start(ctx, host) + return swiExporter.traces.Start(ctx, host) default: - return fmt.Errorf("unknown exporter type: %v", e.exporterType) + return fmt.Errorf("unknown exporter type: %v", swiExporter.exporterType) } } -func (e *solarwindsExporter) shutdown(ctx context.Context) error { - switch e.exporterType { +func (swiExporter *solarwindsExporter) shutdown(ctx context.Context) error { + switch swiExporter.exporterType { case metricsExporterType: - return e.metrics.Shutdown(ctx) + if swiExporter.metrics == nil { + return nil + } + return swiExporter.metrics.Shutdown(ctx) case logsExporterType: - return e.logs.Shutdown(ctx) + if swiExporter.logs == nil { + return nil + } + return swiExporter.logs.Shutdown(ctx) case tracesExporterType: - return e.traces.Shutdown(ctx) + if swiExporter.traces == nil { + return nil + } + return swiExporter.traces.Shutdown(ctx) default: - return fmt.Errorf("unknown exporter type: %v", e.exporterType) + return fmt.Errorf("unknown exporter type: %v", swiExporter.exporterType) } } -func (e *solarwindsExporter) pushMetrics(ctx context.Context, metrics pmetric.Metrics) error { +func (swiExporter *solarwindsExporter) pushMetrics(ctx context.Context, metrics pmetric.Metrics) error { if metrics.MetricCount() == 0 { return nil } - return e.metrics.ConsumeMetrics(ctx, metrics) + return swiExporter.metrics.ConsumeMetrics(ctx, metrics) } -func (e *solarwindsExporter) pushLogs(ctx context.Context, logs plog.Logs) error { +func (swiExporter *solarwindsExporter) pushLogs(ctx context.Context, logs plog.Logs) error { if logs.LogRecordCount() == 0 { return nil } - return e.logs.ConsumeLogs(ctx, logs) + return swiExporter.logs.ConsumeLogs(ctx, logs) } -func (e *solarwindsExporter) pushTraces(ctx context.Context, traces ptrace.Traces) error { +func (swiExporter *solarwindsExporter) pushTraces(ctx context.Context, traces ptrace.Traces) error { if traces.SpanCount() == 0 { return nil } - return e.traces.ConsumeTraces(ctx, traces) + return swiExporter.traces.ConsumeTraces(ctx, traces) } diff --git a/exporter/solarwindsexporter/testdata/full.yaml b/exporter/solarwindsexporter/testdata/full.yaml index 78428fe..8925e4c 100644 --- a/exporter/solarwindsexporter/testdata/full.yaml +++ b/exporter/solarwindsexporter/testdata/full.yaml @@ -1,6 +1,4 @@ -token: "TOKEN" -data_center: "na-01" -endpoint_url_override: "127.0.0.1:1234" +extension: "solarwinds/1" timeout: "20s" sending_queue: enabled: true @@ -13,4 +11,3 @@ retry_on_failure: multiplier: 2.4 max_interval: "40s" max_elapsed_time: "400s" - diff --git a/exporter/solarwindsexporter/testdata/invalid.yaml b/exporter/solarwindsexporter/testdata/invalid.yaml new file mode 100644 index 0000000..abb2afa --- /dev/null +++ b/exporter/solarwindsexporter/testdata/invalid.yaml @@ -0,0 +1 @@ +extension: "/" diff --git a/exporter/solarwindsexporter/testdata/minimal.yaml b/exporter/solarwindsexporter/testdata/minimal.yaml index 762db58..8b13789 100644 --- a/exporter/solarwindsexporter/testdata/minimal.yaml +++ b/exporter/solarwindsexporter/testdata/minimal.yaml @@ -1,2 +1 @@ -token: "YOUR-INGESTION-TOKEN" -data_center: "na-01" + diff --git a/exporter/solarwindsexporter/testdata/missing_token.yaml b/exporter/solarwindsexporter/testdata/missing_token.yaml deleted file mode 100644 index 46d8215..0000000 --- a/exporter/solarwindsexporter/testdata/missing_token.yaml +++ /dev/null @@ -1 +0,0 @@ -data_center: "na-01" diff --git a/extension/solarwindsextension/Makefile b/extension/solarwindsextension/Makefile new file mode 100644 index 0000000..84677bc --- /dev/null +++ b/extension/solarwindsextension/Makefile @@ -0,0 +1,2 @@ +include ../../Makefile.Common + diff --git a/extension/solarwindsextension/README.md b/extension/solarwindsextension/README.md new file mode 100644 index 0000000..9b84188 --- /dev/null +++ b/extension/solarwindsextension/README.md @@ -0,0 +1,42 @@ +# Solarwinds Extension + + +| Status | | +| ------------- |-----------| +| Stability | [development] | +| Distributions | [] | +| Issues | [![Open issues](https://img.shields.io/github/issues-search/solarwinds/solarwinds-otel-collector?query=is%3Aissue%20is%3Aopen%20label%3Aextension%2Fsolarwinds%20&label=open&color=orange&logo=opentelemetry)](https://github.com/solarwinds/solarwinds-otel-collector/issues?q=is%3Aopen+is%3Aissue+label%3Aextension%2Fsolarwinds) [![Closed issues](https://img.shields.io/github/issues-search/solarwinds/solarwinds-otel-collector?query=is%3Aissue%20is%3Aclosed%20label%3Aextension%2Fsolarwinds%20&label=closed&color=blue&logo=opentelemetry)](https://github.com/solarwinds/solarwinds-otel-collector/issues?q=is%3Aclosed+is%3Aissue+label%3Aextension%2Fsolarwinds) | + +[development]: https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/component-stability.md#development + + +The SolarWinds Extension offers capabilities related to the SolarWinds Observability SaaS platform and is required for the [SolarWinds Exporter](../../exporter/solarwindsexporter) to function. + +It provides these features: + +- Endpoint configuration for the Solarwinds Exporters +- Heartbeat signal + - It's a standard metric: `sw.otecol.uptime` + - The value is a time from the start of the collector in seconds + - This signal is necessary for SolarWinds Observability SaaS to detect the collector when installed and to determine if it's still alive. + - It also contains some additional information as resource attributes for SolarWinds Observability SaaS: + - Collector name: `sw.collector.name` + +## Getting Started + +You just need to include the SolarWinds Extension in your extension definitions and provide the following configuration: + +```yaml +extensions: + solarwinds: + token: "YOUR-INGESTION-TOKEN" + data_center: "na-01" + collector_name: "Collector Display Name" +``` +- `token` (mandatory) - You can generate your token in your SolarWinds Observability SaaS account under _Settings / API Tokens / Create API Token_. The type is "Ingestion". You can find the complete documentation [here](https://documentation.solarwinds.com/en/success_center/observability/content/settings/api-tokens.htm). +- `data_center` (mandatory) - Data center is the region you picked during the sign-up process. You can easily see in URLs after logging in to SolarWinds Observability SaaS - it's either `na-01`, `na-02` or `eu-01`. Please refer to the [documentation](https://documentation.solarwinds.com/en/success_center/observability/content/system_requirements/endpoints.htm#Find) for details. +- `collector_name` (mandatory) - The collector name passed in the heartbeat metric (as `sw.collector.name` resource attribute) to identify the collector. Doesn't have to be unique. + +## Development +- **Tests** can be executed with `make test`. +- After changes to `metadata.yaml` generated files need to be re-generated with `make generate`. The [mdatagen](http://go.opentelemetry.io/collector/cmd/mdatagen) tool has to be in the `PATH`. diff --git a/extension/solarwindsextension/config_test.go b/extension/solarwindsextension/config_test.go new file mode 100644 index 0000000..93da7b1 --- /dev/null +++ b/extension/solarwindsextension/config_test.go @@ -0,0 +1,172 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 solarwindsextension + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/config/configopaque" + + "github.com/solarwinds/solarwinds-otel-collector/pkg/testutil" + + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension/internal" +) + +// TestConfigUnmarshalFull tries to parse a configuration file +// with all values provided and verifies the configuration. +func TestConfigUnmarshalFull(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "full") + + // Parse configuration. + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + require.NoError(t, cfgFile.Unmarshal(&cfg)) + + // Verify the values. + assert.Equal(t, &internal.Config{ + DataCenter: "na-01", + EndpointURLOverride: "127.0.0.1:1234", + IngestionToken: "TOKEN", + CollectorName: "test-collector", + Insecure: true, + }, cfg) +} + +// TestConfigValidateOK verifies that a configuration +// file containing only the mandatory values successfully +// validates. +func TestConfigValidateOK(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "minimal") + + // Parse configuration. + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + require.NoError(t, cfgFile.Unmarshal(&cfg)) + + // Try to validate it. + assert.NoError(t, cfg.(*internal.Config).Validate()) +} + +// TestConfigValidateMissingToken verifies that +// the validation of a configuration file with +// the token missing fails as expected. +func TestConfigValidateMissingToken(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "missing_token") + + // Parse configuration. + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + require.NoError(t, cfgFile.Unmarshal(&cfg)) + + assert.ErrorContains( + t, + cfg.(*internal.Config).Validate(), + "'token' must be set", + ) +} + +// TestConfigValidateMissingDataCenter verifies that +// the validation of a configuration file with +// the data center ID missing fails as expected. +func TestConfigValidateMissingDataCenter(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "missing_dc") + + // Parse configuration. + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + require.NoError(t, cfgFile.Unmarshal(&cfg)) + + assert.ErrorContains( + t, + cfg.(*internal.Config).Validate(), + "'data_center' must be set", + ) +} + +// TestConfigValidateMissingDataCenter verifies that +// the validation of a configuration file with +// the collector name missing fails as expected. +func TestConfigValidateMissingCollectorName(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "missing_collector_name") + + // Parse configuration. + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + require.NoError(t, cfgFile.Unmarshal(&cfg)) + + assert.ErrorContains( + t, + cfg.(*internal.Config).Validate(), + "'collector_name' must be set", + ) +} + +// TestConfigValidateInsecureInProd tests that 'insecure' +// cannot be enabled for a production endpoint. +func TestConfigValidateInsecureInProd(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "insecure_in_prod") + + // Parse configuration. + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + require.NoError(t, cfgFile.Unmarshal(&cfg)) + + assert.ErrorContains( + t, + cfg.(*internal.Config).Validate(), + "invalid configuration: 'insecure'", + ) +} + +// TestConfigTokenRedacted checks that the configuration +// type doesn't leak its secret token unless it is accessed explicitly. +func TestConfigTokenRedacted(t *testing.T) { + cfg := &internal.Config{ + DataCenter: "eu-01", + IngestionToken: "SECRET", + } + // This is the only way of accessing the actual token. + require.Equal(t, "SECRET", string(cfg.IngestionToken)) + + // It is redacted when printed. + assert.Equal(t, "[REDACTED]", cfg.IngestionToken.String()) +} + +// TestConfigOTLPWithOverride converts configuration to +// OTLP gRPC Exporter configuration and verifies that overridden +// endpoint and token propagate correctly. +func TestConfigOTLPWithOverride(t *testing.T) { + cfgFile := testutil.LoadConfigTestdata(t, "url_override") + + // Parse configuration. + factory := NewFactory() + cfg := factory.CreateDefaultConfig() + require.NoError(t, cfgFile.Unmarshal(&cfg)) + + // Convert it to the OTLP Exporter configuration. + otlpCfg, err := cfg.(*internal.Config).OTLPConfig() + require.NoError(t, err) + + // Verify that both the token and overridden URL were propagated + // to the OTLP configuration. + assert.Equal(t, "127.0.0.1:1234", otlpCfg.Endpoint) + assert.Equal( + t, + map[string]configopaque.String{"Authorization": "Bearer YOUR-INGESTION-TOKEN"}, + otlpCfg.Headers, + ) +} diff --git a/extension/solarwindsextension/doc.go b/extension/solarwindsextension/doc.go new file mode 100644 index 0000000..3bcd9d8 --- /dev/null +++ b/extension/solarwindsextension/doc.go @@ -0,0 +1,16 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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. + +//go:generate mdatagen metadata.yaml +package solarwindsextension diff --git a/extension/solarwindsextension/endpoint_config.go b/extension/solarwindsextension/endpoint_config.go new file mode 100644 index 0000000..c120c01 --- /dev/null +++ b/extension/solarwindsextension/endpoint_config.go @@ -0,0 +1,47 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 solarwindsextension + +import ( + "go.opentelemetry.io/collector/config/configopaque" + + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension/internal" +) + +type EndpointConfig interface { + Url() (string, error) + Token() configopaque.String + Insecure() bool +} + +type endpointConfig struct{ cfg *internal.Config } + +var _ EndpointConfig = (*endpointConfig)(nil) + +func newEndpointConfig(cfg *internal.Config) *endpointConfig { + return &endpointConfig{cfg: cfg} +} + +func (c *endpointConfig) Url() (string, error) { + return c.cfg.EndpointUrl() +} + +func (c *endpointConfig) Token() configopaque.String { + return c.cfg.IngestionToken +} + +func (c *endpointConfig) Insecure() bool { + return c.cfg.Insecure +} diff --git a/extension/solarwindsextension/extension.go b/extension/solarwindsextension/extension.go new file mode 100644 index 0000000..41045d4 --- /dev/null +++ b/extension/solarwindsextension/extension.go @@ -0,0 +1,62 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 solarwindsextension + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension" + "go.uber.org/zap" + + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension/internal" +) + +type SolarwindsExtension struct { + logger *zap.Logger + config *internal.Config + heartbeat *internal.Heartbeat +} + +func newExtension(ctx context.Context, set extension.Settings, cfg *internal.Config) (*SolarwindsExtension, error) { + set.Logger.Info("Creating Solarwinds Extension") + set.Logger.Info("Config", zap.Any("config", cfg)) + + e := &SolarwindsExtension{ + logger: set.Logger, + config: cfg, + } + var err error + e.heartbeat, err = internal.NewHeartbeat(ctx, set, cfg) + if err != nil { + return nil, err + } + + return e, nil +} + +func (e *SolarwindsExtension) GetEndpointConfig() EndpointConfig { return newEndpointConfig(e.config) } + +func (e *SolarwindsExtension) Start(ctx context.Context, host component.Host) error { + e.logger.Info("Starting Solarwinds Extension") + return e.heartbeat.Start(ctx, host) +} + +func (e *SolarwindsExtension) Shutdown(ctx context.Context) error { + e.logger.Info("Shutting down Solarwinds Extension") + // Everything must be shut down, regardless of the failure. + return e.heartbeat.Shutdown(ctx) + +} diff --git a/extension/solarwindsextension/factory.go b/extension/solarwindsextension/factory.go new file mode 100644 index 0000000..c0f40e5 --- /dev/null +++ b/extension/solarwindsextension/factory.go @@ -0,0 +1,42 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 solarwindsextension + +import ( + "context" + "fmt" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension" + + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension/internal" + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension/internal/metadata" +) + +func NewFactory() extension.Factory { + return extension.NewFactory( + metadata.Type, + internal.NewDefaultConfig, + createExtension, + metadata.ExtensionStability) +} + +func createExtension(ctx context.Context, set extension.Settings, cfg component.Config) (extension.Extension, error) { + extCfg, ok := cfg.(*internal.Config) + if !ok { + return nil, fmt.Errorf("unexpected config type: %T", cfg) + } + return newExtension(ctx, set, extCfg) +} diff --git a/extension/solarwindsextension/generated_component_test.go b/extension/solarwindsextension/generated_component_test.go new file mode 100644 index 0000000..f37af2b --- /dev/null +++ b/extension/solarwindsextension/generated_component_test.go @@ -0,0 +1,49 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package solarwindsextension + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/confmap/confmaptest" + "go.opentelemetry.io/collector/extension/extensiontest" +) + +func TestComponentFactoryType(t *testing.T) { + require.Equal(t, "solarwinds", NewFactory().Type().String()) +} + +func TestComponentConfigStruct(t *testing.T) { + require.NoError(t, componenttest.CheckConfigStruct(NewFactory().CreateDefaultConfig())) +} + +func TestComponentLifecycle(t *testing.T) { + factory := NewFactory() + + cm, err := confmaptest.LoadConf("metadata.yaml") + require.NoError(t, err) + cfg := factory.CreateDefaultConfig() + sub, err := cm.Sub("tests::config") + require.NoError(t, err) + require.NoError(t, sub.Unmarshal(&cfg)) + t.Run("shutdown", func(t *testing.T) { + e, err := factory.Create(context.Background(), extensiontest.NewNopSettings(), cfg) + require.NoError(t, err) + err = e.Shutdown(context.Background()) + require.NoError(t, err) + }) + t.Run("lifecycle", func(t *testing.T) { + firstExt, err := factory.Create(context.Background(), extensiontest.NewNopSettings(), cfg) + require.NoError(t, err) + require.NoError(t, firstExt.Start(context.Background(), componenttest.NewNopHost())) + require.NoError(t, firstExt.Shutdown(context.Background())) + + secondExt, err := factory.Create(context.Background(), extensiontest.NewNopSettings(), cfg) + require.NoError(t, err) + require.NoError(t, secondExt.Start(context.Background(), componenttest.NewNopHost())) + require.NoError(t, secondExt.Shutdown(context.Background())) + }) +} diff --git a/extension/solarwindsextension/generated_package_test.go b/extension/solarwindsextension/generated_package_test.go new file mode 100644 index 0000000..61bd5ee --- /dev/null +++ b/extension/solarwindsextension/generated_package_test.go @@ -0,0 +1,12 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package solarwindsextension + +import ( + "go.uber.org/goleak" + "testing" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} diff --git a/extension/solarwindsextension/go.mod b/extension/solarwindsextension/go.mod new file mode 100644 index 0000000..8602072 --- /dev/null +++ b/extension/solarwindsextension/go.mod @@ -0,0 +1,77 @@ +module github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension + +go 1.23.4 + +require ( + github.com/solarwinds/solarwinds-otel-collector/pkg/testutil v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/collector/component v0.113.0 + go.opentelemetry.io/collector/config/configgrpc v0.113.0 + go.opentelemetry.io/collector/config/configopaque v1.19.0 + go.opentelemetry.io/collector/config/configtls v1.19.0 + go.opentelemetry.io/collector/confmap v1.19.0 + go.opentelemetry.io/collector/exporter v0.113.0 + go.opentelemetry.io/collector/exporter/otlpexporter v0.113.0 + go.opentelemetry.io/collector/extension v0.113.0 + go.opentelemetry.io/collector/pdata v1.19.0 + go.uber.org/goleak v1.3.0 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.2 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/go-grpc-compression v1.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect + go.opentelemetry.io/collector/client v1.19.0 // indirect + go.opentelemetry.io/collector/config/configauth v0.113.0 // indirect + go.opentelemetry.io/collector/config/configcompression v1.19.0 // indirect + go.opentelemetry.io/collector/config/confignet v1.19.0 // indirect + go.opentelemetry.io/collector/config/configretry v1.19.0 // indirect + go.opentelemetry.io/collector/config/configtelemetry v0.113.0 // indirect + go.opentelemetry.io/collector/config/internal v0.113.0 // indirect + go.opentelemetry.io/collector/consumer v0.113.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror v0.113.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror/consumererrorprofiles v0.113.0 // indirect + go.opentelemetry.io/collector/consumer/consumerprofiles v0.113.0 // indirect + go.opentelemetry.io/collector/exporter/exporterhelper/exporterhelperprofiles v0.113.0 // indirect + go.opentelemetry.io/collector/exporter/exporterprofiles v0.113.0 // indirect + go.opentelemetry.io/collector/extension/auth v0.113.0 // indirect + go.opentelemetry.io/collector/extension/experimental/storage v0.113.0 // indirect + go.opentelemetry.io/collector/pdata/pprofile v0.113.0 // indirect + go.opentelemetry.io/collector/pipeline v0.113.0 // indirect + go.opentelemetry.io/collector/pipeline/pipelineprofiles v0.113.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +replace github.com/solarwinds/solarwinds-otel-collector/pkg/testutil => ../../pkg/testutil diff --git a/extension/solarwindsextension/go.sum b/extension/solarwindsextension/go.sum new file mode 100644 index 0000000..2ed1b3e --- /dev/null +++ b/extension/solarwindsextension/go.sum @@ -0,0 +1,188 @@ +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= +github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I= +github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/collector v0.113.0 h1:dBuo2/OKBhoMCR86W4fFJLXGQ0gJfKRmi65AZwFkU2I= +go.opentelemetry.io/collector v0.113.0/go.mod h1:XbjD4Yw9LunLo3IJu3ZZytNZ0drEVznxw1Z14Ujlw3s= +go.opentelemetry.io/collector/client v1.19.0 h1:TUal8WV1agTrZStgE7BJ8ZC0IHLGtrfgO9ogU9t1mv8= +go.opentelemetry.io/collector/client v1.19.0/go.mod h1:jgiXMEM6l8L2QEyf2I/M47Zd8+G7e4z+6H8q5SkHOlQ= +go.opentelemetry.io/collector/component v0.113.0 h1:/nx+RvZgxUEXP+YcTj69rEtuSEGkfaCyp/ad5zQGLjU= +go.opentelemetry.io/collector/component v0.113.0/go.mod h1:2T779hIGHU9i7xbXbV3q1/JnRw2FyzUYXW2vq47A6EU= +go.opentelemetry.io/collector/config/configauth v0.113.0 h1:CBz43fGpN41MwLdwe3mw/XVSIDvGRMT8aaaPuqKukTU= +go.opentelemetry.io/collector/config/configauth v0.113.0/go.mod h1:Q8SlxrIvL3FJO51hXa4n9ARvox04lK8mmpjf4b3UNAU= +go.opentelemetry.io/collector/config/configcompression v1.19.0 h1:bTSjTLhnPXX1NSFM6GzguEM/NBe8QUPsXHc9kMOAJzE= +go.opentelemetry.io/collector/config/configcompression v1.19.0/go.mod h1:pnxkFCLUZLKWzYJvfSwZnPrnm0twX14CYj2ADth5xiU= +go.opentelemetry.io/collector/config/configgrpc v0.113.0 h1:rNbRd033JlIeU+TH+3bEt4OwRlEwrktWdf6V+VUJUPk= +go.opentelemetry.io/collector/config/configgrpc v0.113.0/go.mod h1:InXxPUj1oxJ57Sl954d2tQxXTgVHhfppFYjMwGjQukg= +go.opentelemetry.io/collector/config/confignet v1.19.0 h1:gEDTd8zLx4pPpG5///XPRpbYUpvKsuQzDdM5IEULY9w= +go.opentelemetry.io/collector/config/confignet v1.19.0/go.mod h1:o3v4joAEjvLwntqexg5ixMqRrU1+Vst+jWuCUaBNgOg= +go.opentelemetry.io/collector/config/configopaque v1.19.0 h1:7uvntQeAAtqCaeiS2dDGrT1wLPhWvDlEsD3SliA/koQ= +go.opentelemetry.io/collector/config/configopaque v1.19.0/go.mod h1:6zlLIyOoRpJJ+0bEKrlZOZon3rOp5Jrz9fMdR4twOS4= +go.opentelemetry.io/collector/config/configretry v1.19.0 h1:DEg8PXpo4ahMYgMzZZUU2cPcDF4vqowZlvimJ/t9InY= +go.opentelemetry.io/collector/config/configretry v1.19.0/go.mod h1:KvQF5cfphq1rQm1dKR4eLDNQYw6iI2fY72NMZVa+0N0= +go.opentelemetry.io/collector/config/configtelemetry v0.113.0 h1:hweTRrVddnUeA3k7HzRY4oUR9lRdMa7of3mHNUS5YyA= +go.opentelemetry.io/collector/config/configtelemetry v0.113.0/go.mod h1:R0MBUxjSMVMIhljuDHWIygzzJWQyZHXXWIgQNxcFwhc= +go.opentelemetry.io/collector/config/configtls v1.19.0 h1:GQ/cF1hgNqHVBq2oSSrOFXxVCyMDyd5kq4R/RMEbL98= +go.opentelemetry.io/collector/config/configtls v1.19.0/go.mod h1:1hyqnYB3JqEUlk1ME/s9HYz4oCRcxQCRxsJitFFT/cA= +go.opentelemetry.io/collector/config/internal v0.113.0 h1:9RAzH8v7ItFT1npHpvP0SvUzBHcZDliCGRo9Spp6v7c= +go.opentelemetry.io/collector/config/internal v0.113.0/go.mod h1:yC7E4h1Uj0SubxcFImh6OvBHFTjMh99+A5PuyIgDWqc= +go.opentelemetry.io/collector/confmap v1.19.0 h1:TQ0lZpAKqgsE0EKk+u4JA+uBbPYeFRmWP3GH43w40CY= +go.opentelemetry.io/collector/confmap v1.19.0/go.mod h1:GgNu1ElPGmLn9govqIfjaopvdspw4PJ9KeDtWC4E2Q4= +go.opentelemetry.io/collector/consumer v0.113.0 h1:KJSiK5vSIY9dgPxwKfQ3gOgKtQsqc+7IB7mGhUAL5c8= +go.opentelemetry.io/collector/consumer v0.113.0/go.mod h1:zHMlXYFaJlZoLCBR6UwWoyXZ/adcO1u2ydqUal3VmYU= +go.opentelemetry.io/collector/consumer/consumererror v0.113.0 h1:Hd2N7n9RKbnKRaVrdw6fPBoQko5zZIgCxwVxkL6SAIE= +go.opentelemetry.io/collector/consumer/consumererror v0.113.0/go.mod h1:o0MAGFdzcr7LFTUQ6iivPPhbVmn2ZVIYm3FPXk2+JUo= +go.opentelemetry.io/collector/consumer/consumererror/consumererrorprofiles v0.113.0 h1:2kLIt+6dGmhCd48CWXh3IEon/uW4+c8y81IGCA/h8wE= +go.opentelemetry.io/collector/consumer/consumererror/consumererrorprofiles v0.113.0/go.mod h1:/eESy7Ifyf7G6r6WUpEOq2tnfjIJ2QNB2EvZcEu0aWA= +go.opentelemetry.io/collector/consumer/consumerprofiles v0.113.0 h1:RftAcQUY5UOfbEK4s16jnORqTx16y9+PxA1lQwt98cQ= +go.opentelemetry.io/collector/consumer/consumerprofiles v0.113.0/go.mod h1:ZuHrQ4pWguh6dw0DgTfcUtdY/T+cnOJJNP6LMbm5Y5A= +go.opentelemetry.io/collector/consumer/consumertest v0.113.0 h1:ua2AjNx3DUA8qElXNkggB4w3VDL/rBKBvryOQkhumH8= +go.opentelemetry.io/collector/consumer/consumertest v0.113.0/go.mod h1:vK8o4ZTZSiG3rVyqxZcCNmT/cvEfx34ig7V65L9+6Rg= +go.opentelemetry.io/collector/exporter v0.113.0 h1:lDZJ6xfuhyLsT/7lqLhIN/ftA6G+9fuYFtubPFvNDxo= +go.opentelemetry.io/collector/exporter v0.113.0/go.mod h1:0W4NBf5NjWYxR8oJodmOybgN4O0MLazdJwwHevirvXg= +go.opentelemetry.io/collector/exporter/exporterhelper/exporterhelperprofiles v0.113.0 h1:Auz2vZYReIlyDvJ162OCO8XcV7L2BIbFb5HJWxerc5A= +go.opentelemetry.io/collector/exporter/exporterhelper/exporterhelperprofiles v0.113.0/go.mod h1:JQuawcAfDuzNneDF5Ep1CZJ5snsLp6Bh1gZcHhja7yU= +go.opentelemetry.io/collector/exporter/exporterprofiles v0.113.0 h1:8bsk3wYYNr+WAM5nZkFjiLYSTH9MsY2tm7nUpMWt3qc= +go.opentelemetry.io/collector/exporter/exporterprofiles v0.113.0/go.mod h1:/HFWF846XePYL/qKDtcEAFgkiGSkLUTaC59A5F48axM= +go.opentelemetry.io/collector/exporter/exportertest v0.113.0 h1:U6cRxjJS7td8iNriUI2QfEdH+Yj60ytyvpmnmKTw0+8= +go.opentelemetry.io/collector/exporter/exportertest v0.113.0/go.mod h1:SRz5jGyAjtNiWwJ93B1+Ndk1p3oFtQsyLw52UGeyRwc= +go.opentelemetry.io/collector/exporter/otlpexporter v0.113.0 h1://7diunG5SohqaYfqvHzCtcfrY7y3WQj0vklFYgeNW4= +go.opentelemetry.io/collector/exporter/otlpexporter v0.113.0/go.mod h1:THF0eq4lA6dYOho53iKFCBOv91HEeISZyep5dXr+fBU= +go.opentelemetry.io/collector/extension v0.113.0 h1:Vp/YSL8ZCkJQrP1lf2Bm5yaTvcp6ROO3AnfuSL3GEXM= +go.opentelemetry.io/collector/extension v0.113.0/go.mod h1:Pwp0TNqdHeER4V1I6H6oCvrto/riiOAqs3737BWCnjw= +go.opentelemetry.io/collector/extension/auth v0.113.0 h1:4ggRy1vepOabUiCWfU+6M9P/ftXojMUNAvBpeLihYj8= +go.opentelemetry.io/collector/extension/auth v0.113.0/go.mod h1:VbvAm2YZAqePkWgwn0m0vBaq3aC49CxPVwHmrJ24aeQ= +go.opentelemetry.io/collector/extension/experimental/storage v0.113.0 h1:Qq4IaB6bMUrf/bWoPZ5ESWywCt+vDi8I/ChYejIEPcc= +go.opentelemetry.io/collector/extension/experimental/storage v0.113.0/go.mod h1:BRmo+A7f06u/rhyLauU/Vogk+QRN0y1j2VVVgMGWrfQ= +go.opentelemetry.io/collector/pdata v1.19.0 h1:jmnU5R8TOCbwRr4B8sjdRxM7L5WnEKlQWX1dtLYxIbE= +go.opentelemetry.io/collector/pdata v1.19.0/go.mod h1:Ox1YVLe87cZDB/TL30i4SUz1cA5s6AM6SpFMfY61ICs= +go.opentelemetry.io/collector/pdata/pprofile v0.113.0 h1:VRf4p0VhfuaR+Epy/nMIlu/9t39WU9CUgHVUvpuGxfU= +go.opentelemetry.io/collector/pdata/pprofile v0.113.0/go.mod h1:5aDejksdXh5PdJN/OhpzATGT3kbNL0RMmw2Q0Q6E/o0= +go.opentelemetry.io/collector/pdata/testdata v0.113.0 h1:vRfn85jicO2F4eOTgsWtzmU/K3E/uZUtM1HEefvvJD8= +go.opentelemetry.io/collector/pdata/testdata v0.113.0/go.mod h1:sR+6eR+YEJhYZu9StbqzeWcCmHpfBAgX/qjP82HY9Gw= +go.opentelemetry.io/collector/pipeline v0.113.0 h1:vSRzRe3717jV0btCNPhVkhg2lu0uFxcm2VO+vhad/eE= +go.opentelemetry.io/collector/pipeline v0.113.0/go.mod h1:4vOvjVsoYTHVGTbfFwqfnQOSV2K3RKUHofh3jNRc2Mg= +go.opentelemetry.io/collector/pipeline/pipelineprofiles v0.113.0 h1:PwQnErsLvEd1x6VIyjLmKQot9huKWqIfEz1kd+8aj4k= +go.opentelemetry.io/collector/pipeline/pipelineprofiles v0.113.0/go.mod h1:tChJYsCG3wc6JPT9aJO3y+32V14NhmCFZOh3k5ORGdQ= +go.opentelemetry.io/collector/receiver v0.113.0 h1:vraAbkPy8Pz9x5X39gV+j9t6x23PNsY2aJ6gQMugRbQ= +go.opentelemetry.io/collector/receiver v0.113.0/go.mod h1:IUa8/lNw8Qh4L5Q3jOeRWKW0ebQPoNcfhytxN5Puq2A= +go.opentelemetry.io/collector/receiver/receiverprofiles v0.113.0 h1:uVxuzjGe2t1sbwahSBowVHYnGzpzn8brmfn8z1UHvQg= +go.opentelemetry.io/collector/receiver/receiverprofiles v0.113.0/go.mod h1:khKDkzYJR2x2OPUqGSmoSncdINT9lUE5IThiHPDbqZk= +go.opentelemetry.io/collector/receiver/receivertest v0.113.0 h1:0vOvz3S4Q/KwcNCS9C7zPo0uxD6RSWktG88yGdxfV6g= +go.opentelemetry.io/collector/receiver/receivertest v0.113.0/go.mod h1:sRq5ctm5UE/0Ar562wnCVQ1zbAie/D127D1WbtbEuEc= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/extension/solarwindsextension/internal/config.go b/extension/solarwindsextension/internal/config.go new file mode 100644 index 0000000..82893aa --- /dev/null +++ b/extension/solarwindsextension/internal/config.go @@ -0,0 +1,156 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 internal + +import ( + "errors" + "fmt" + "maps" + "slices" + "strings" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/configgrpc" + "go.opentelemetry.io/collector/config/configopaque" + "go.opentelemetry.io/collector/config/configtls" + "go.opentelemetry.io/collector/exporter/otlpexporter" +) + +// Config represents a Solarwinds Extension configuration. +type Config struct { + // DataCenter ID (e.g. na-01). + DataCenter string `mapstructure:"data_center"` + // IngestionToken is your secret generated SolarWinds Observability SaaS ingestion token. + IngestionToken configopaque.String `mapstructure:"token"` + // CollectorName name of the collector passed in the heartbeat metric + CollectorName string `mapstructure:"collector_name"` + // Insecure disables TLS in the exporters. + + // ⚠️ Warning: For testing purpose only. + // EndpointURLOverride sets OTLP endpoint directly, it overrides the DataCenter configuration. + EndpointURLOverride string `mapstructure:"endpoint_url_override"` + // ⚠️ Warning: For testing purpose only. + // Insecure disables the TLS security. It can be used only together with EndpointURLOverride. + Insecure bool `mapstructure:"insecure"` +} + +var ( + ErrMissingDataCenter = errors.New("invalid configuration: 'data_center' must be set") + ErrMissingToken = errors.New("invalid configuration: 'token' must be set") + ErrMissingCollectorName = errors.New("invalid configuration: 'collector_name' must be set") + ErrInsecureInProd = errors.New("invalid configuration: 'insecure' is not allowed in production mode") +) + +// NewDefaultConfig creates a new default configuration. +// +// Warning: it doesn't define mandatory `Token` and `DataCenter` +// fields that need to be explicitly provided. +func NewDefaultConfig() component.Config { + return &Config{} +} + +// Validate checks the configuration for its validity. +func (cfg *Config) Validate() error { + if cfg.DataCenter == "" && cfg.EndpointURLOverride == "" { + return ErrMissingDataCenter + } + + if cfg.Insecure && cfg.EndpointURLOverride == "" { + return ErrInsecureInProd + } + + if _, err := cfg.EndpointUrl(); err != nil { + return fmt.Errorf("invalid 'data_center' value: %w", err) + } + + if cfg.IngestionToken == "" { + return ErrMissingToken + } + if cfg.CollectorName == "" { + return ErrMissingCollectorName + } + + return nil +} + +// OTLPConfig generates a full OTLP Exporter configuration from the configuration. +func (cfg *Config) OTLPConfig() (*otlpexporter.Config, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + + endpointURL, err := cfg.EndpointUrl() + if err != nil { + return nil, err + } + + // Headers - set bearer auth. + bearer := configopaque.String(fmt.Sprintf("Bearer %s", string(cfg.IngestionToken))) + headers := map[string]configopaque.String{ + "Authorization": bearer, + } + + // gRPC client configuration. + otlpConfig := &otlpexporter.Config{ + ClientConfig: configgrpc.ClientConfig{ + TLSSetting: configtls.NewDefaultClientConfig(), + Keepalive: configgrpc.NewDefaultKeepaliveClientConfig(), + BalancerName: configgrpc.BalancerName(), + Headers: headers, + Endpoint: endpointURL, + }, + } + + // Disable TLS for testing. + if cfg.Insecure { + otlpConfig.ClientConfig.TLSSetting.Insecure = true + } + + if err = otlpConfig.Validate(); err != nil { + return nil, err + } + + return otlpConfig, nil +} + +func (cfg *Config) EndpointUrl() (string, error) { + // Use overridden URL if provided. + if cfg.EndpointURLOverride != "" { + return cfg.EndpointURLOverride, nil + } + return lookupDataCenterURL(cfg.DataCenter) +} + +// dataCenterToURLMapping maps a data center ID to +// to its corresponding OTLP endpoint URL. +var dataCenterToURLMapping = map[string]string{ + "na-01": "otel.collector.na-01.cloud.solarwinds.com:4317", + "na-02": "otel.collector.na-02.cloud.solarwinds.com:4317", + "eu-01": "otel.collector.eu-01.cloud.solarwinds.com:4317", +} + +// lookupDataCenterURL returns the OTLP endpoint URL +// for a `dc` data center ID. Matching is case-insensitive. +// It fails with an error if `dc` doesn't identify a data center. +func lookupDataCenterURL(dc string) (string, error) { + dcLowercase := strings.ToLower(dc) + + url, ok := dataCenterToURLMapping[dcLowercase] + if !ok { + return "", fmt.Errorf("unknown data center ID: %s, valid IDs: %s", dc, slices.Collect(maps.Keys(dataCenterToURLMapping))) + } + + return url, nil +} diff --git a/extension/solarwindsextension/internal/datacell_test.go b/extension/solarwindsextension/internal/datacell_test.go new file mode 100644 index 0000000..9f56032 --- /dev/null +++ b/extension/solarwindsextension/internal/datacell_test.go @@ -0,0 +1,52 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestConfigValidateDataCenters verifies mappings +// for data centers (the mapping is case-insensitive). +func TestConfigValidateDataCenters(t *testing.T) { + type test struct { + dataCenter string + url string + ok bool + } + + tests := []test{ + {dataCenter: "na-01", url: "otel.collector.na-01.cloud.solarwinds.com:4317", ok: true}, + {dataCenter: "na-02", url: "otel.collector.na-02.cloud.solarwinds.com:4317", ok: true}, + {dataCenter: "eu-01", url: "otel.collector.eu-01.cloud.solarwinds.com:4317", ok: true}, + {dataCenter: "NA-01", url: "otel.collector.na-01.cloud.solarwinds.com:4317", ok: true}, + {dataCenter: "apj-01", url: "", ok: false}, + } + + for _, tc := range tests { + // Try to find a dataCenter URL for its ID. + url, err := lookupDataCenterURL(tc.dataCenter) + + if tc.ok { // A URL should be returned. + require.NoError(t, err) + assert.Equal(t, tc.url, url) + } else { // It should fail. + assert.ErrorContains(t, err, "unknown data center ID") + } + } +} diff --git a/extension/solarwindsextension/internal/exporter.go b/extension/solarwindsextension/internal/exporter.go new file mode 100644 index 0000000..bc97f8b --- /dev/null +++ b/extension/solarwindsextension/internal/exporter.go @@ -0,0 +1,69 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 internal + +import ( + "context" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/exporter" + "go.opentelemetry.io/collector/exporter/otlpexporter" + "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.uber.org/zap" +) + +type Exporter struct { + logger *zap.Logger + exporter exporter.Metrics +} + +func newExporter(ctx context.Context, set extension.Settings, cfg *Config) (*Exporter, error) { + set.Logger.Debug("Creating Exporter") + oCfg, err := cfg.OTLPConfig() + if err != nil { + return nil, err + } + expSet := toExporterSettings(set) + + exp := &Exporter{logger: set.Logger} + exp.exporter, err = otlpexporter.NewFactory().CreateMetrics(ctx, expSet, oCfg) + if err != nil { + return nil, err + } + return exp, nil +} + +func (e *Exporter) start(ctx context.Context, host component.Host) error { + e.logger.Debug("Starting exporter") + return e.exporter.Start(ctx, host) +} + +func (e *Exporter) shutdown(ctx context.Context) error { + e.logger.Debug("Shutting down exporter") + return e.exporter.Shutdown(ctx) +} + +func toExporterSettings(set extension.Settings) exporter.Settings { + return exporter.Settings{ + ID: set.ID, + TelemetrySettings: set.TelemetrySettings, + BuildInfo: set.BuildInfo, + } +} + +func (e *Exporter) push(ctx context.Context, md pmetric.Metrics) error { + return e.exporter.ConsumeMetrics(ctx, md) +} diff --git a/extension/solarwindsextension/internal/heartbeat.go b/extension/solarwindsextension/internal/heartbeat.go new file mode 100644 index 0000000..720ac29 --- /dev/null +++ b/extension/solarwindsextension/internal/heartbeat.go @@ -0,0 +1,159 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 internal + +import ( + "context" + "errors" + "sync" + "time" + + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/extension" + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.uber.org/zap" +) + +const ( + defaultHeartbeatInterval = 30 * time.Second +) + +type MetricsExporter interface { + start(context.Context, component.Host) error + shutdown(context.Context) error + push(context.Context, pmetric.Metrics) error +} + +type Heartbeat struct { + logger *zap.Logger + + cancel context.CancelFunc + startShutdownMtx sync.Mutex + + metric *UptimeMetric + exporter MetricsExporter + collectorName string + + beatInterval time.Duration +} + +var ErrAlreadyRunning = errors.New("heartbeat already running") + +func NewHeartbeat(ctx context.Context, set extension.Settings, cfg *Config) (*Heartbeat, error) { + set.Logger.Debug("Creating Heartbeat") + + exp, err := newExporter(ctx, set, cfg) + if err != nil { + return nil, err + } + + return newHeartbeatWithExporter(set, cfg, exp), nil +} + +func newHeartbeatWithExporter( + set extension.Settings, + cfg *Config, + exporter MetricsExporter, +) *Heartbeat { + return &Heartbeat{ + logger: set.Logger, + metric: newUptimeMetric(set.Logger), + collectorName: cfg.CollectorName, + exporter: exporter, + beatInterval: defaultHeartbeatInterval, + } +} + +func (h *Heartbeat) Start(ctx context.Context, host component.Host) error { + h.startShutdownMtx.Lock() + defer h.startShutdownMtx.Unlock() + + h.logger.Debug("Starting Heartbeat routine") + if h.cancel != nil { + return ErrAlreadyRunning + } + + err := h.exporter.start(ctx, host) + if err != nil { + return err + } + + var loopCtx context.Context + loopCtx, h.cancel = context.WithCancel(context.Background()) + go h.loop(loopCtx) + return nil +} + +func (h *Heartbeat) Shutdown(ctx context.Context) error { + h.startShutdownMtx.Lock() + defer h.startShutdownMtx.Unlock() + + h.logger.Debug("Stopping Heartbeat routine") + if h.cancel == nil { + // already stopped + return nil + } + h.cancel() + h.cancel = nil + return h.exporter.shutdown(ctx) +} + +func (h *Heartbeat) loop(ctx context.Context) { + tick := time.NewTicker(h.beatInterval) + defer tick.Stop() + + // Start beat + if err := h.generate(ctx); err != nil { + h.logger.Error("Generating heartbeat failed", zap.Error(err)) + } + + for { + select { + case <-tick.C: + if err := h.generate(ctx); err != nil { + h.logger.Error("Generating heartbeat failed", zap.Error(err)) + } + case <-ctx.Done(): + return + } + } + +} + +func (h *Heartbeat) generate(ctx context.Context) error { + h.logger.Debug("Generating heartbeat") + md := pmetric.NewMetrics() + + if err := h.metric.add(ctx, md); err != nil { + return err + } + + for i, rms := 0, md.ResourceMetrics(); i < rms.Len(); i++ { + rm := rms.At(i) + if err := h.decorateResourceAttributes(rm.Resource()); err != nil { + return err + } + } + + return h.exporter.push(ctx, md) +} + +func (h *Heartbeat) decorateResourceAttributes(resource pcommon.Resource) error { + if h.collectorName != "" { + resource.Attributes().PutStr("sw.otelcol.collector.name", h.collectorName) + } + return nil +} diff --git a/extension/solarwindsextension/internal/hearthbeat_test.go b/extension/solarwindsextension/internal/hearthbeat_test.go new file mode 100644 index 0000000..07603cf --- /dev/null +++ b/extension/solarwindsextension/internal/hearthbeat_test.go @@ -0,0 +1,94 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 internal + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/component/componenttest" + "go.opentelemetry.io/collector/extension/extensiontest" + "go.opentelemetry.io/collector/pdata/pmetric" +) + +type mockExporter struct { + pushed []pmetric.Metrics +} + +func newMockExporter() *mockExporter { + return &mockExporter{ + pushed: []pmetric.Metrics{}, + } +} + +func (m *mockExporter) start(_ context.Context, _ component.Host) error { + return nil +} + +func (m *mockExporter) shutdown(_ context.Context) error { + return nil +} + +func (m *mockExporter) push(_ context.Context, metrics pmetric.Metrics) error { + m.pushed = append(m.pushed, metrics) + + return nil +} + +// TestHeartbeatEmittingMetrics runs the `Hearthbeat` +// in isolation with a mocked exporter and inspects +// emitted metrics. +func TestHeartbeatEmittingMetrics(t *testing.T) { + const ( + testDuration = 1000 * time.Millisecond + beatInterval = 100 * time.Millisecond + expectedCount = int(testDuration / beatInterval) + ) + + mockExp := newMockExporter() + hb := newHeartbeatWithExporter( + extensiontest.NewNopSettings(), + &Config{}, + mockExp, + ) + // Adjust the heartbeat interval to shave off some time. + hb.beatInterval = beatInterval + + // Start the heartbeat. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + err := hb.Start(ctx, componenttest.NewNopHost()) + require.NoError(t, err) + + // Inspect exported metrics in a loop. + assert.Eventuallyf( + t, + func() bool { + return len(mockExp.pushed) == expectedCount + }, + testDuration+50*time.Millisecond, // Allow some leeway. + 10*time.Millisecond, + "expected %d metrics, got %d", + expectedCount, + len(mockExp.pushed), + ) + + err = hb.Shutdown(ctx) + require.NoError(t, err) +} diff --git a/extension/solarwindsextension/internal/metadata/generated_status.go b/extension/solarwindsextension/internal/metadata/generated_status.go new file mode 100644 index 0000000..d55c1a9 --- /dev/null +++ b/extension/solarwindsextension/internal/metadata/generated_status.go @@ -0,0 +1,16 @@ +// Code generated by mdatagen. DO NOT EDIT. + +package metadata + +import ( + "go.opentelemetry.io/collector/component" +) + +var ( + Type = component.MustNewType("solarwinds") + ScopeName = "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension" +) + +const ( + ExtensionStability = component.StabilityLevelDevelopment +) diff --git a/extension/solarwindsextension/internal/uptime_counter.go b/extension/solarwindsextension/internal/uptime_counter.go new file mode 100644 index 0000000..b551a4b --- /dev/null +++ b/extension/solarwindsextension/internal/uptime_counter.go @@ -0,0 +1,30 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 internal + +import "time" + +type uptimeCounter struct { + startTimeUnixNano int64 +} + +func newUptimeCounter() *uptimeCounter { + return &uptimeCounter{startTimeUnixNano: time.Now().UnixNano()} +} + +func (u *uptimeCounter) Get() float64 { + // Borrowed from Collector's processor uptime metric + return float64(time.Now().UnixNano()-u.startTimeUnixNano) / 1e9 +} diff --git a/extension/solarwindsextension/internal/uptime_metric.go b/extension/solarwindsextension/internal/uptime_metric.go new file mode 100644 index 0000000..3b9cba8 --- /dev/null +++ b/extension/solarwindsextension/internal/uptime_metric.go @@ -0,0 +1,50 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 internal + +import ( + "context" + "time" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/pmetric" + "go.uber.org/zap" + + "github.com/solarwinds/solarwinds-otel-collector/extension/solarwindsextension/internal/metadata" +) + +func newUptimeMetric(logger *zap.Logger) *UptimeMetric { + logger.Debug("Creating UptimeMetric") + return &UptimeMetric{logger: logger, uptime: newUptimeCounter()} +} + +type UptimeMetric struct { + logger *zap.Logger + uptime *uptimeCounter +} + +func (um *UptimeMetric) add(_ context.Context, md pmetric.Metrics) error { + um.logger.Debug("Adding uptime metric") + res := md.ResourceMetrics().AppendEmpty() + scopeMetrics := res.ScopeMetrics().AppendEmpty() + scopeMetrics.Scope().SetName(metadata.ScopeName) + scopeMetrics.Scope().SetVersion("0.0.1") + m := scopeMetrics.Metrics().AppendEmpty() + m.SetName("sw.otelcol.uptime") + dataPoint := m.SetEmptyGauge().DataPoints().AppendEmpty() + dataPoint.SetTimestamp(pcommon.NewTimestampFromTime(time.Now())) + dataPoint.SetDoubleValue(um.uptime.Get()) + return nil +} diff --git a/extension/solarwindsextension/metadata.yaml b/extension/solarwindsextension/metadata.yaml new file mode 100644 index 0000000..1d55116 --- /dev/null +++ b/extension/solarwindsextension/metadata.yaml @@ -0,0 +1,13 @@ +type: solarwinds +github_project: solarwinds/solarwinds-otel-collector + +status: + class: extension + stability: + development: [ extension ] + +tests: + config: + data_center: "na-01" + token: "THIS-IS-A-TEST" + collector_name: "test-name" diff --git a/extension/solarwindsextension/testdata/full.yaml b/extension/solarwindsextension/testdata/full.yaml new file mode 100644 index 0000000..b7bd8e3 --- /dev/null +++ b/extension/solarwindsextension/testdata/full.yaml @@ -0,0 +1,5 @@ +token: "TOKEN" +data_center: "na-01" +collector_name: "test-collector" +endpoint_url_override: "127.0.0.1:1234" +insecure: true diff --git a/extension/solarwindsextension/testdata/insecure_in_prod.yaml b/extension/solarwindsextension/testdata/insecure_in_prod.yaml new file mode 100644 index 0000000..0c0d906 --- /dev/null +++ b/extension/solarwindsextension/testdata/insecure_in_prod.yaml @@ -0,0 +1,4 @@ +token: "YOUR-INGESTION-TOKEN" +data_center: "na-02" +collector_name: "collector" +insecure: true diff --git a/extension/solarwindsextension/testdata/minimal.yaml b/extension/solarwindsextension/testdata/minimal.yaml new file mode 100644 index 0000000..e06a1eb --- /dev/null +++ b/extension/solarwindsextension/testdata/minimal.yaml @@ -0,0 +1,3 @@ +token: "YOUR-INGESTION-TOKEN" +data_center: "na-01" +collector_name: "mycoll" diff --git a/exporter/solarwindsexporter/testdata/missing_dc.yaml b/extension/solarwindsextension/testdata/missing_collector_name.yaml similarity index 58% rename from exporter/solarwindsexporter/testdata/missing_dc.yaml rename to extension/solarwindsextension/testdata/missing_collector_name.yaml index 8b4fe9e..762db58 100644 --- a/exporter/solarwindsexporter/testdata/missing_dc.yaml +++ b/extension/solarwindsextension/testdata/missing_collector_name.yaml @@ -1 +1,2 @@ token: "YOUR-INGESTION-TOKEN" +data_center: "na-01" diff --git a/extension/solarwindsextension/testdata/missing_dc.yaml b/extension/solarwindsextension/testdata/missing_dc.yaml new file mode 100644 index 0000000..31ac87a --- /dev/null +++ b/extension/solarwindsextension/testdata/missing_dc.yaml @@ -0,0 +1,2 @@ +token: "YOUR-INGESTION-TOKEN" +collector_name: "mycol" diff --git a/extension/solarwindsextension/testdata/missing_token.yaml b/extension/solarwindsextension/testdata/missing_token.yaml new file mode 100644 index 0000000..71cc90d --- /dev/null +++ b/extension/solarwindsextension/testdata/missing_token.yaml @@ -0,0 +1,2 @@ +data_center: "na-01" +collector_name: "mycol" diff --git a/exporter/solarwindsexporter/testdata/url_override.yaml b/extension/solarwindsextension/testdata/url_override.yaml similarity index 79% rename from exporter/solarwindsexporter/testdata/url_override.yaml rename to extension/solarwindsextension/testdata/url_override.yaml index ef329f6..15c6f70 100644 --- a/exporter/solarwindsexporter/testdata/url_override.yaml +++ b/extension/solarwindsextension/testdata/url_override.yaml @@ -1,4 +1,4 @@ token: "YOUR-INGESTION-TOKEN" data_center: "na-01" +collector_name: "mycol" endpoint_url_override: "127.0.0.1:1234" - diff --git a/pkg/testutil/config.go b/pkg/testutil/config.go new file mode 100644 index 0000000..48b0973 --- /dev/null +++ b/pkg/testutil/config.go @@ -0,0 +1,37 @@ +// Copyright 2024 SolarWinds Worldwide, LLC. All rights reserved. +// +// 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 testutil + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/confmap" + "go.opentelemetry.io/collector/confmap/confmaptest" +) + +// LoadConfigTestdata is a helper function to load a configuration +// testdata file by its name from the 'testdata' folder of the current package. +func LoadConfigTestdata(t *testing.T, name string) *confmap.Conf { + t.Helper() + + filename := fmt.Sprintf("%s.yaml", name) + cm, err := confmaptest.LoadConf(filepath.Join("./testdata", filename)) + require.NoError(t, err) + + return cm +} diff --git a/pkg/testutil/go.mod b/pkg/testutil/go.mod new file mode 100644 index 0000000..b262557 --- /dev/null +++ b/pkg/testutil/go.mod @@ -0,0 +1,22 @@ +module github.com/solarwinds/solarwinds-otel-collector/pkg/testutil + +go 1.23.4 + +require ( + github.com/stretchr/testify v1.10.0 + go.opentelemetry.io/collector/confmap v1.19.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.2 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/testutil/go.sum b/pkg/testutil/go.sum new file mode 100644 index 0000000..a863f67 --- /dev/null +++ b/pkg/testutil/go.sum @@ -0,0 +1,37 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= +github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/collector/confmap v1.19.0 h1:TQ0lZpAKqgsE0EKk+u4JA+uBbPYeFRmWP3GH43w40CY= +go.opentelemetry.io/collector/confmap v1.19.0/go.mod h1:GgNu1ElPGmLn9govqIfjaopvdspw4PJ9KeDtWC4E2Q4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=