From c356157565b34163cfe6ce75e1353021f4eef34a Mon Sep 17 00:00:00 2001 From: Brandon Johnson Date: Tue, 16 Apr 2024 09:11:29 -0400 Subject: [PATCH] [extension/opamp] Add config options to specify extra non-identifying attributes (#32153) **Description:** * Adds a new `agent_description.non_identifying_attributes` config option to allow setting user-defined non-identifying attributes **Link to tracking Issue:** Closes #32107 **Testing:** Added unit tests Manually tested against an OpAMP server **Documentation:** Added new parameter to extension docs --- ...amp-extension-user-defined-attributes.yaml | 13 +++ cmd/otelcontribcol/go.mod | 2 +- cmd/otelcontribcol/go.sum | 4 +- extension/opampextension/README.md | 2 + extension/opampextension/config.go | 9 ++ extension/opampextension/go.mod | 1 + extension/opampextension/go.sum | 2 + extension/opampextension/opamp_agent.go | 25 +++- extension/opampextension/opamp_agent_test.go | 107 ++++++++++++++++-- 9 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 .chloggen/feat_opamp-extension-user-defined-attributes.yaml diff --git a/.chloggen/feat_opamp-extension-user-defined-attributes.yaml b/.chloggen/feat_opamp-extension-user-defined-attributes.yaml new file mode 100644 index 0000000000000..473bfb6507bca --- /dev/null +++ b/.chloggen/feat_opamp-extension-user-defined-attributes.yaml @@ -0,0 +1,13 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: opampextension + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Added a new `agent_description.non_identifying_attributes` config option to allow setting user-defined non-identifying attributes + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [32107] diff --git a/cmd/otelcontribcol/go.mod b/cmd/otelcontribcol/go.mod index ca24c7632cd90..1e73a5544d01a 100644 --- a/cmd/otelcontribcol/go.mod +++ b/cmd/otelcontribcol/go.mod @@ -694,7 +694,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect diff --git a/cmd/otelcontribcol/go.sum b/cmd/otelcontribcol/go.sum index f49c5ec067aae..da0f4906e6017 100644 --- a/cmd/otelcontribcol/go.sum +++ b/cmd/otelcontribcol/go.sum @@ -1765,8 +1765,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/extension/opampextension/README.md b/extension/opampextension/README.md index 51ea597afc9fa..20264a951cfa2 100644 --- a/extension/opampextension/README.md +++ b/extension/opampextension/README.md @@ -30,6 +30,8 @@ The following settings are optional: instance UID remains constant across process restarts. - `capabilities`: Keys with boolean true/false values that enable a particular OpAMP capability. - `reports_effective_config`: Whether to enable the OpAMP ReportsEffectiveConfig capability. Default is `true`. +- `agent_description`: Setting that modifies the agent description reported to the OpAMP server. + - `non_identifying_attributes`: A map of key value pairs that will be added to the [non-identifying attributes](https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#agentdescriptionnon_identifying_attributes) reported to the OpAMP server. If an attribute collides with the default non-identifying attributes that are automatically added, the ones specified here take precedence. ### Example diff --git a/extension/opampextension/config.go b/extension/opampextension/config.go index 7756b1e1164a1..877b5f62c874e 100644 --- a/extension/opampextension/config.go +++ b/extension/opampextension/config.go @@ -26,6 +26,15 @@ type Config struct { // Capabilities contains options to enable a particular OpAMP capability Capabilities Capabilities `mapstructure:"capabilities"` + + // Agent descriptions contains options to modify the AgentDescription message + AgentDescription AgentDescription `mapstructure:"agent_description"` +} + +type AgentDescription struct { + // NonIdentifyingAttributes are a map of key-value pairs that may be specified to provide + // extra information about the agent to the OpAMP server. + NonIdentifyingAttributes map[string]string `mapstructure:"non_identifying_attributes"` } type Capabilities struct { diff --git a/extension/opampextension/go.mod b/extension/opampextension/go.mod index 2b8f5f73c6fde..aa7da0255183a 100644 --- a/extension/opampextension/go.mod +++ b/extension/opampextension/go.mod @@ -18,6 +18,7 @@ require ( go.opentelemetry.io/otel/trace v1.25.0 go.uber.org/goleak v1.3.0 go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/extension/opampextension/go.sum b/extension/opampextension/go.sum index b84c09973d8f0..739cb64d65383 100644 --- a/extension/opampextension/go.sum +++ b/extension/opampextension/go.sum @@ -97,6 +97,8 @@ 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/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= 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= diff --git a/extension/opampextension/opamp_agent.go b/extension/opampextension/opamp_agent.go index 42096711120f8..b5c14e9005b57 100644 --- a/extension/opampextension/opamp_agent.go +++ b/extension/opampextension/opamp_agent.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "runtime" + "sort" "strings" "sync" @@ -21,6 +22,7 @@ import ( "go.opentelemetry.io/collector/pdata/pcommon" semconv "go.opentelemetry.io/collector/semconv/v1.18.0" "go.uber.org/zap" + "golang.org/x/exp/maps" "gopkg.in/yaml.v3" ) @@ -194,10 +196,25 @@ func (o *opampAgent) createAgentDescription() error { stringKeyValue(semconv.AttributeServiceVersion, o.agentVersion), } - nonIdent := []*protobufs.KeyValue{ - stringKeyValue(semconv.AttributeOSType, runtime.GOOS), - stringKeyValue(semconv.AttributeHostArch, runtime.GOARCH), - stringKeyValue(semconv.AttributeHostName, hostname), + // Initially construct using a map to properly deduplicate any keys that + // are both automatically determined and defined in the config + nonIdentifyingAttributeMap := map[string]string{} + nonIdentifyingAttributeMap[semconv.AttributeOSType] = runtime.GOOS + nonIdentifyingAttributeMap[semconv.AttributeHostArch] = runtime.GOARCH + nonIdentifyingAttributeMap[semconv.AttributeHostName] = hostname + + for k, v := range o.cfg.AgentDescription.NonIdentifyingAttributes { + nonIdentifyingAttributeMap[k] = v + } + + // Sort the non identifying attributes to give them a stable order for tests + keys := maps.Keys(nonIdentifyingAttributeMap) + sort.Strings(keys) + + nonIdent := make([]*protobufs.KeyValue, 0, len(nonIdentifyingAttributeMap)) + for _, k := range keys { + v := nonIdentifyingAttributeMap[k] + nonIdent = append(nonIdent, stringKeyValue(k, v)) } o.agentDescription = &protobufs.AgentDescription{ diff --git a/extension/opampextension/opamp_agent_test.go b/extension/opampextension/opamp_agent_test.go index bc6c26735ac01..652826639a983 100644 --- a/extension/opampextension/opamp_agent_test.go +++ b/extension/opampextension/opamp_agent_test.go @@ -7,10 +7,13 @@ import ( "context" "os" "path/filepath" + "runtime" "testing" "github.com/oklog/ulid/v2" + "github.com/open-telemetry/opamp-go/protobufs" "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/confmap/confmaptest" @@ -47,15 +50,101 @@ func TestNewOpampAgentAttributes(t *testing.T) { } func TestCreateAgentDescription(t *testing.T) { - cfg := createDefaultConfig() - set := extensiontest.NewNopCreateSettings() - o, err := newOpampAgent(cfg.(*Config), set.Logger, set.BuildInfo, set.Resource) - assert.NoError(t, err) - - assert.Nil(t, o.agentDescription) - err = o.createAgentDescription() - assert.NoError(t, err) - assert.NotNil(t, o.agentDescription) + hostname, err := os.Hostname() + require.NoError(t, err) + + serviceName := "otelcol-distrot" + serviceVersion := "distro.0" + serviceInstanceUUID := "f8999bc1-4c9b-4619-9bae-7f009d2411ec" + serviceInstanceULID := "7RK6DW2K4V8RCSQBKZ02EJ84FC" + + testCases := []struct { + name string + cfg func(*Config) + + expected *protobufs.AgentDescription + }{ + { + name: "No extra attributes", + cfg: func(_ *Config) {}, + expected: &protobufs.AgentDescription{ + IdentifyingAttributes: []*protobufs.KeyValue{ + stringKeyValue(semconv.AttributeServiceInstanceID, serviceInstanceULID), + stringKeyValue(semconv.AttributeServiceName, serviceName), + stringKeyValue(semconv.AttributeServiceVersion, serviceVersion), + }, + NonIdentifyingAttributes: []*protobufs.KeyValue{ + stringKeyValue(semconv.AttributeHostArch, runtime.GOARCH), + stringKeyValue(semconv.AttributeHostName, hostname), + stringKeyValue(semconv.AttributeOSType, runtime.GOOS), + }, + }, + }, + { + name: "Extra attributes specified", + cfg: func(c *Config) { + c.AgentDescription.NonIdentifyingAttributes = map[string]string{ + "env": "prod", + semconv.AttributeK8SPodName: "my-very-cool-pod", + } + }, + expected: &protobufs.AgentDescription{ + IdentifyingAttributes: []*protobufs.KeyValue{ + stringKeyValue(semconv.AttributeServiceInstanceID, serviceInstanceULID), + stringKeyValue(semconv.AttributeServiceName, serviceName), + stringKeyValue(semconv.AttributeServiceVersion, serviceVersion), + }, + NonIdentifyingAttributes: []*protobufs.KeyValue{ + stringKeyValue("env", "prod"), + stringKeyValue(semconv.AttributeHostArch, runtime.GOARCH), + stringKeyValue(semconv.AttributeHostName, hostname), + stringKeyValue(semconv.AttributeK8SPodName, "my-very-cool-pod"), + stringKeyValue(semconv.AttributeOSType, runtime.GOOS), + }, + }, + }, + { + name: "Extra attributes override", + cfg: func(c *Config) { + c.AgentDescription.NonIdentifyingAttributes = map[string]string{ + semconv.AttributeHostName: "override-host", + } + }, + expected: &protobufs.AgentDescription{ + IdentifyingAttributes: []*protobufs.KeyValue{ + stringKeyValue(semconv.AttributeServiceInstanceID, serviceInstanceULID), + stringKeyValue(semconv.AttributeServiceName, serviceName), + stringKeyValue(semconv.AttributeServiceVersion, serviceVersion), + }, + NonIdentifyingAttributes: []*protobufs.KeyValue{ + stringKeyValue(semconv.AttributeHostArch, runtime.GOARCH), + stringKeyValue(semconv.AttributeHostName, "override-host"), + stringKeyValue(semconv.AttributeOSType, runtime.GOOS), + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + cfg := createDefaultConfig().(*Config) + tc.cfg(cfg) + + set := extensiontest.NewNopCreateSettings() + set.Resource.Attributes().PutStr(semconv.AttributeServiceName, serviceName) + set.Resource.Attributes().PutStr(semconv.AttributeServiceVersion, serviceVersion) + set.Resource.Attributes().PutStr(semconv.AttributeServiceInstanceID, serviceInstanceUUID) + + o, err := newOpampAgent(cfg, set.Logger, set.BuildInfo, set.Resource) + require.NoError(t, err) + assert.Nil(t, o.agentDescription) + + err = o.createAgentDescription() + assert.NoError(t, err) + require.Equal(t, tc.expected, o.agentDescription) + }) + } } func TestUpdateAgentIdentity(t *testing.T) {