Skip to content

Commit 0704bcb

Browse files
authored
Add optional usage metadata (#1904)
1 parent 95fbd16 commit 0704bcb

File tree

18 files changed

+462
-75
lines changed

18 files changed

+462
-75
lines changed

extension/agenthealth/config.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,28 @@
44
package agenthealth
55

66
import (
7+
"fmt"
8+
79
"go.opentelemetry.io/collector/component"
810

911
"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/stats/agent"
12+
"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/metadata"
1013
)
1114

1215
type Config struct {
13-
IsUsageDataEnabled bool `mapstructure:"is_usage_data_enabled"`
14-
Stats *agent.StatsConfig `mapstructure:"stats,omitempty"`
15-
IsStatusCodeEnabled bool `mapstructure:"is_status_code_enabled,omitempty"`
16+
IsUsageDataEnabled bool `mapstructure:"is_usage_data_enabled"`
17+
Stats *agent.StatsConfig `mapstructure:"stats,omitempty"`
18+
IsStatusCodeEnabled bool `mapstructure:"is_status_code_enabled,omitempty"`
19+
UsageMetadata []metadata.Metadata `mapstructure:"usage_metadata,omitempty"`
1620
}
1721

1822
var _ component.Config = (*Config)(nil)
23+
24+
func (c *Config) Validate() error {
25+
for _, m := range c.UsageMetadata {
26+
if !metadata.IsSupported(m) {
27+
return fmt.Errorf("usage metadata %q is not supported", m)
28+
}
29+
}
30+
return nil
31+
}

extension/agenthealth/config_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import (
1010
"github.com/stretchr/testify/assert"
1111
"github.com/stretchr/testify/require"
1212
"go.opentelemetry.io/collector/component"
13+
"go.opentelemetry.io/collector/confmap"
1314
"go.opentelemetry.io/collector/confmap/confmaptest"
1415
"go.opentelemetry.io/collector/confmap/xconfmap"
1516

1617
"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/handler/stats/agent"
18+
"github.com/aws/amazon-cloudwatch-agent/extension/agenthealth/metadata"
1719
)
1820

1921
func TestLoadConfig(t *testing.T) {
@@ -33,6 +35,10 @@ func TestLoadConfig(t *testing.T) {
3335
id: component.NewIDWithName(TypeStr, "2"),
3436
want: &Config{IsUsageDataEnabled: true, Stats: &agent.StatsConfig{Operations: []string{"ListBuckets"}}},
3537
},
38+
{
39+
id: component.NewIDWithName(TypeStr, "3"),
40+
want: &Config{IsUsageDataEnabled: true, UsageMetadata: []metadata.Metadata{"obs_jvm", "obs_tomcat"}},
41+
},
3642
}
3743
for _, testCase := range testCases {
3844
conf, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml"))
@@ -46,3 +52,53 @@ func TestLoadConfig(t *testing.T) {
4652
assert.Equal(t, testCase.want, cfg)
4753
}
4854
}
55+
56+
func TestValidateConfig(t *testing.T) {
57+
testCases := map[string]struct {
58+
cfg component.Config
59+
wantErr bool
60+
}{
61+
"WithEmptyConfig": {
62+
cfg: &Config{},
63+
},
64+
"WithUsageMetadata/Unsupported": {
65+
cfg: &Config{
66+
IsUsageDataEnabled: true,
67+
UsageMetadata: []metadata.Metadata{
68+
"unsupported_metadata",
69+
},
70+
},
71+
wantErr: true,
72+
},
73+
"WithUsageMetadata/Mixed": {
74+
cfg: &Config{
75+
IsUsageDataEnabled: true,
76+
UsageMetadata: []metadata.Metadata{
77+
"obs_jvm",
78+
"unsupported_metadata",
79+
},
80+
},
81+
wantErr: true,
82+
},
83+
"WithUsageMetadata/Supported": {
84+
cfg: &Config{
85+
IsUsageDataEnabled: true,
86+
UsageMetadata: []metadata.Metadata{
87+
"obs_jvm",
88+
},
89+
},
90+
wantErr: false,
91+
},
92+
}
93+
for name, testCase := range testCases {
94+
t.Run(name, func(t *testing.T) {
95+
assert.NoError(t, confmap.New().Unmarshal(testCase.cfg))
96+
err := testCase.cfg.(*Config).Validate()
97+
if testCase.wantErr {
98+
assert.Error(t, err)
99+
} else {
100+
assert.NoError(t, err)
101+
}
102+
})
103+
}
104+
}

extension/agenthealth/extension.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func (ah *agentHealth) Handlers() ([]awsmiddleware.RequestHandler, []awsmiddlewa
3131
return requestHandlers, responseHandlers
3232
}
3333

34+
useragent.Get().AddFeatureFlags(ah.cfg.UsageMetadata...)
3435
statusCodeEnabled := ah.cfg.IsStatusCodeEnabled
3536

3637
var statsResponseHandlers []awsmiddleware.ResponseHandler

extension/agenthealth/handler/useragent/useragent.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
typeInputs = "inputs"
3939
typeProcessors = "processors"
4040
typeOutputs = "outputs"
41+
typeFeature = "feature"
4142
)
4243

4344
var (
@@ -48,8 +49,10 @@ var (
4849
type UserAgent interface {
4950
SetComponents(otelCfg *otelcol.Config, telegrafCfg *telegraf.Config)
5051
SetContainerInsightsFlag()
52+
AddFeatureFlags(features ...string)
5153
Header(isUsageDataEnabled bool) string
5254
Listen(listener func())
55+
Reset()
5356
}
5457

5558
type userAgent struct {
@@ -63,15 +66,19 @@ type userAgent struct {
6366
inputs collections.Set[string]
6467
processors collections.Set[string]
6568
outputs collections.Set[string]
69+
feature collections.Set[string]
6670

6771
inputsStr atomic.String
6872
processorsStr atomic.String
6973
outputsStr atomic.String
74+
featureStr atomic.String
7075
}
7176

7277
var _ UserAgent = (*userAgent)(nil)
7378

7479
func (ua *userAgent) SetComponents(otelCfg *otelcol.Config, telegrafCfg *telegraf.Config) {
80+
ua.dataLock.Lock()
81+
defer ua.dataLock.Unlock()
7582
for _, input := range telegrafCfg.Inputs {
7683
ua.inputs.Add(input.Config.Name)
7784
}
@@ -145,6 +152,37 @@ func (ua *userAgent) SetContainerInsightsFlag() {
145152
}
146153
}
147154

155+
func (ua *userAgent) AddFeatureFlags(features ...string) {
156+
ua.dataLock.Lock()
157+
defer ua.dataLock.Unlock()
158+
featureCount := len(ua.feature)
159+
for _, feature := range features {
160+
if feature != "" {
161+
ua.feature.Add(feature)
162+
}
163+
}
164+
if len(ua.feature) > featureCount {
165+
ua.featureStr.Store(componentsStr(typeFeature, ua.feature))
166+
ua.notify()
167+
}
168+
}
169+
170+
// Reset allows tests to reset the user agent.
171+
func (ua *userAgent) Reset() {
172+
ua.dataLock.Lock()
173+
defer ua.dataLock.Unlock()
174+
ua.inputs = collections.NewSet[string]()
175+
ua.processors = collections.NewSet[string]()
176+
ua.outputs = collections.NewSet[string]()
177+
ua.feature = collections.NewSet[string]()
178+
179+
ua.inputsStr.Store("")
180+
ua.processorsStr.Store("")
181+
ua.outputsStr.Store("")
182+
ua.featureStr.Store("")
183+
ua.notify()
184+
}
185+
148186
func (ua *userAgent) Listen(listener func()) {
149187
ua.listenerLock.Lock()
150188
defer ua.listenerLock.Unlock()
@@ -180,6 +218,10 @@ func (ua *userAgent) Header(isUsageDataEnabled bool) string {
180218
if outputs != "" {
181219
components = append(components, outputs)
182220
}
221+
feature := ua.featureStr.Load()
222+
if feature != "" {
223+
components = append(components, feature)
224+
}
183225

184226
return strings.TrimSpace(fmt.Sprintf("%s ID/%s %s", version.Full(), ua.id, strings.Join(components, separator)))
185227
}
@@ -204,6 +246,7 @@ func newUserAgent() *userAgent {
204246
inputs: collections.NewSet[string](),
205247
processors: collections.NewSet[string](),
206248
outputs: collections.NewSet[string](),
249+
feature: collections.NewSet[string](),
207250
}
208251
}
209252

extension/agenthealth/handler/useragent/useragent_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package useragent
55

66
import (
7+
"fmt"
78
"sync"
89
"testing"
910

@@ -208,6 +209,67 @@ func TestJmx(t *testing.T) {
208209
assert.Equal(t, "outputs:(nop)", ua.outputsStr.Load())
209210
}
210211

212+
func TestAddFeatureFlags(t *testing.T) {
213+
ua := newUserAgent()
214+
215+
ua.AddFeatureFlags("feature1")
216+
assert.Len(t, ua.feature, 1)
217+
assert.Equal(t, "feature:(feature1)", ua.featureStr.Load())
218+
219+
ua.AddFeatureFlags("feature1", "feature2", "feature3")
220+
assert.Len(t, ua.feature, 3)
221+
assert.Equal(t, "feature:(feature1 feature2 feature3)", ua.featureStr.Load())
222+
223+
ua.AddFeatureFlags("")
224+
assert.Len(t, ua.feature, 3)
225+
assert.Equal(t, "feature:(feature1 feature2 feature3)", ua.featureStr.Load())
226+
assert.Contains(t, ua.Header(true), "feature:(feature1 feature2 feature3)")
227+
}
228+
229+
func TestAddFeatureFlags_Concurrent(t *testing.T) {
230+
ua := newUserAgent()
231+
var wg sync.WaitGroup
232+
for i := 0; i < 50; i++ {
233+
wg.Add(1)
234+
go func(i int) {
235+
defer wg.Done()
236+
ua.AddFeatureFlags(fmt.Sprintf("feature%d", i))
237+
}(i)
238+
}
239+
wg.Wait()
240+
assert.Len(t, ua.feature, 50)
241+
}
242+
243+
func TestReset(t *testing.T) {
244+
ua := newUserAgent()
245+
246+
ua.SetComponents(&otelcol.Config{}, &telegraf.Config{})
247+
ua.SetContainerInsightsFlag()
248+
ua.AddFeatureFlags("test")
249+
250+
assert.Len(t, ua.inputs, 1)
251+
assert.Len(t, ua.processors, 0)
252+
assert.Len(t, ua.outputs, 1)
253+
assert.Len(t, ua.feature, 1)
254+
255+
assert.Equal(t, "inputs:(run_as_user)", ua.inputsStr.Load())
256+
assert.Equal(t, "", ua.processorsStr.Load())
257+
assert.Equal(t, "outputs:(container_insights)", ua.outputsStr.Load())
258+
assert.Equal(t, "feature:(test)", ua.featureStr.Load())
259+
260+
ua.Reset()
261+
262+
assert.Len(t, ua.inputs, 0)
263+
assert.Len(t, ua.processors, 0)
264+
assert.Len(t, ua.outputs, 0)
265+
assert.Len(t, ua.feature, 0)
266+
267+
assert.Equal(t, "", ua.inputsStr.Load())
268+
assert.Equal(t, "", ua.processorsStr.Load())
269+
assert.Equal(t, "", ua.outputsStr.Load())
270+
assert.Equal(t, "", ua.featureStr.Load())
271+
}
272+
211273
func TestSingleton(t *testing.T) {
212274
assert.Equal(t, Get().(*userAgent).id, Get().(*userAgent).id)
213275
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package metadata
5+
6+
import (
7+
"strings"
8+
9+
"github.com/aws/amazon-cloudwatch-agent/internal/util/collections"
10+
)
11+
12+
type Metadata = string
13+
14+
const (
15+
KeyObservabilitySolutions = "ObservabilitySolution"
16+
ValueEC2Health = "ec2_health"
17+
ValueJVM = "jvm"
18+
ValueTomcat = "tomcat"
19+
ValueKafkaBroker = "kafka_broker"
20+
ValueNVIDIA = "nvidia_gpu"
21+
22+
shortKeyObservabilitySolutions = "obs"
23+
separator = "_"
24+
)
25+
26+
var (
27+
supportedMetadata = collections.NewSet(
28+
Build(KeyObservabilitySolutions, ValueEC2Health),
29+
Build(KeyObservabilitySolutions, ValueJVM),
30+
Build(KeyObservabilitySolutions, ValueTomcat),
31+
Build(KeyObservabilitySolutions, ValueKafkaBroker),
32+
Build(KeyObservabilitySolutions, ValueNVIDIA),
33+
)
34+
shortKeyMapping = map[string]string{
35+
strings.ToLower(KeyObservabilitySolutions): shortKeyObservabilitySolutions,
36+
}
37+
)
38+
39+
func IsSupported(m Metadata) bool {
40+
return supportedMetadata.Contains(m)
41+
}
42+
43+
// Build finds any short key mappings and then adds them to the value.
44+
func Build(key, value string) Metadata {
45+
key = strings.ToLower(key)
46+
if shortKey, ok := shortKeyMapping[key]; ok {
47+
key = shortKey
48+
}
49+
return key + separator + strings.ToLower(value)
50+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package metadata
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestBuild(t *testing.T) {
13+
testCases := []struct {
14+
key string
15+
value string
16+
want string
17+
}{
18+
{key: "ObservabilitySolution", value: "ec2_health", want: "obs_ec2_health"},
19+
{key: "ObservabilitySolution", value: "JVM", want: "obs_jvm"},
20+
{key: "OBSERVABILITYSOLUTION", value: "TOMCAT", want: "obs_tomcat"},
21+
{key: "observabilitysolution", value: "kafka_broker", want: "obs_kafka_broker"},
22+
{key: "ObservabilitySolution", value: "NVIDIA_GPU", want: "obs_nvidia_gpu"},
23+
{key: "unsupported", value: "Value", want: "unsupported_value"},
24+
}
25+
for _, testCase := range testCases {
26+
assert.Equal(t, testCase.want, Build(testCase.key, testCase.value))
27+
}
28+
}
29+
30+
func TestIsSupported(t *testing.T) {
31+
testCases := []struct {
32+
input string
33+
want bool
34+
}{
35+
{input: "obs_jvm", want: true},
36+
{input: "obs_tomcat", want: true},
37+
{input: "obs_kafka_broker", want: true},
38+
{input: "obs_nvidia_gpu", want: true},
39+
{input: "obs_ec2_health", want: true},
40+
{input: "unsupported_value", want: false},
41+
}
42+
for _, testCase := range testCases {
43+
assert.Equal(t, testCase.want, IsSupported(testCase.input))
44+
}
45+
}

extension/agenthealth/testdata/config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ agenthealth/2:
66
stats:
77
operations:
88
- 'ListBuckets'
9+
agenthealth/3:
10+
is_usage_data_enabled: true
11+
usage_metadata:
12+
- 'obs_jvm'
13+
- 'obs_tomcat'

0 commit comments

Comments
 (0)