Skip to content

Commit c2765b6

Browse files
authored
[processor/awsentity] Add configurable entity transform option (#1682)
1 parent b9b5e9f commit c2765b6

File tree

8 files changed

+489
-7
lines changed

8 files changed

+489
-7
lines changed

internal/entity/entity.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package entity
5+
6+
// KeyPair represents a key-value pair for entity attributes
7+
type KeyPair struct {
8+
Key string `mapstructure:"key"`
9+
Value string `mapstructure:"value"`
10+
}
11+
12+
// Transform contains configuration for overriding entity attributes
13+
type Transform struct {
14+
KeyAttributes []KeyPair `mapstructure:"key_attributes"`
15+
Attributes []KeyPair `mapstructure:"attributes"`
16+
}

plugins/processors/awsentity/config.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
package awsentity
55

66
import (
7+
"errors"
8+
79
"go.opentelemetry.io/collector/component"
10+
11+
"github.com/aws/amazon-cloudwatch-agent/internal/entity"
12+
"github.com/aws/amazon-cloudwatch-agent/plugins/processors/awsentity/entityattributes"
813
)
914

1015
type Config struct {
@@ -23,11 +28,34 @@ type Config struct {
2328
// EntityType determines the type of entity processing done for
2429
// telemetry. Possible values are Service and Resource
2530
EntityType string `mapstructure:"entity_type,omitempty"`
31+
// TransformEntity contains configuration for overriding entity attributes
32+
TransformEntity *entity.Transform `mapstructure:"transform_entity,omitempty"`
2633
}
2734

2835
// Verify Config implements Processor interface.
2936
var _ component.Config = (*Config)(nil)
3037

3138
func (cfg *Config) Validate() error {
39+
if cfg.TransformEntity != nil {
40+
// Validate key attributes
41+
for _, keyAttr := range cfg.TransformEntity.KeyAttributes {
42+
if !entityattributes.IsAllowedKeyAttribute(keyAttr.Key) {
43+
return errors.New("Invalid key attribute name for entity: " + keyAttr.Key)
44+
}
45+
if keyAttr.Value == "" {
46+
return errors.New("empty value for entity key attribute")
47+
}
48+
}
49+
50+
// Validate regular attributes
51+
for _, attr := range cfg.TransformEntity.Attributes {
52+
if !entityattributes.IsAllowedAttribute(attr.Key) {
53+
return errors.New("Invalid attribute name for entity: " + attr.Key)
54+
}
55+
if attr.Value == "" {
56+
return errors.New("empty value for entity attribute")
57+
}
58+
}
59+
}
3260
return nil
3361
}

plugins/processors/awsentity/config_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88

99
"github.com/stretchr/testify/assert"
1010
"go.opentelemetry.io/collector/confmap"
11+
12+
"github.com/aws/amazon-cloudwatch-agent/internal/entity"
13+
"github.com/aws/amazon-cloudwatch-agent/plugins/processors/awsentity/entityattributes"
1114
)
1215

1316
func TestUnmarshalDefaultConfig(t *testing.T) {
@@ -16,3 +19,126 @@ func TestUnmarshalDefaultConfig(t *testing.T) {
1619
assert.NoError(t, confmap.New().Unmarshal(cfg))
1720
assert.Equal(t, factory.CreateDefaultConfig(), cfg)
1821
}
22+
23+
func TestUnmarshalConfig(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
conf *confmap.Conf
27+
expected *Config
28+
expectError bool
29+
}{
30+
{
31+
name: "TestValidEntityTransform",
32+
conf: confmap.NewFromStringMap(map[string]interface{}{
33+
"entity_type": entityattributes.Service,
34+
"platform": "ec2",
35+
"transform_entity": map[string]interface{}{
36+
"key_attributes": []interface{}{
37+
map[string]interface{}{
38+
"key": entityattributes.ServiceName,
39+
"value": "config-service-name",
40+
},
41+
map[string]interface{}{
42+
"key": entityattributes.DeploymentEnvironment,
43+
"value": "config-environment-name",
44+
},
45+
},
46+
"attributes": []interface{}{
47+
map[string]interface{}{
48+
"key": entityattributes.ServiceNameSource,
49+
"value": "UserConfiguration",
50+
},
51+
},
52+
},
53+
}),
54+
expected: &Config{
55+
EntityType: entityattributes.Service,
56+
Platform: "ec2",
57+
TransformEntity: &entity.Transform{
58+
KeyAttributes: []entity.KeyPair{
59+
{
60+
Key: entityattributes.ServiceName,
61+
Value: "config-service-name",
62+
},
63+
{
64+
Key: entityattributes.DeploymentEnvironment,
65+
Value: "config-environment-name",
66+
},
67+
},
68+
Attributes: []entity.KeyPair{
69+
{
70+
Key: entityattributes.ServiceNameSource,
71+
Value: "UserConfiguration",
72+
},
73+
},
74+
},
75+
},
76+
expectError: false,
77+
},
78+
{
79+
name: "TestInvalidEntityTransform",
80+
conf: confmap.NewFromStringMap(map[string]interface{}{
81+
"entity_type": entityattributes.Service,
82+
"platform": "ec2",
83+
"transform_entity": map[string]interface{}{
84+
"key_attributes": []interface{}{
85+
map[string]interface{}{
86+
"key": "InvalidKey",
87+
"value": "some-value",
88+
},
89+
},
90+
},
91+
}),
92+
expectError: true,
93+
},
94+
{
95+
name: "TestEmptyEntityTransform",
96+
conf: confmap.NewFromStringMap(map[string]interface{}{
97+
"entity_type": entityattributes.Service,
98+
"platform": "ec2",
99+
}),
100+
expected: &Config{
101+
EntityType: entityattributes.Service,
102+
Platform: "ec2",
103+
},
104+
expectError: false,
105+
},
106+
{
107+
name: "TestMissingRequiredFieldEntityTransform",
108+
conf: confmap.NewFromStringMap(map[string]interface{}{
109+
"transform_entity": map[string]interface{}{
110+
"key_attributes": []interface{}{
111+
map[string]interface{}{
112+
"key": entityattributes.ServiceName,
113+
"value": "",
114+
},
115+
},
116+
},
117+
}),
118+
expectError: true,
119+
},
120+
}
121+
122+
for _, tt := range tests {
123+
t.Run(tt.name, func(t *testing.T) {
124+
factory := NewFactory()
125+
cfg := factory.CreateDefaultConfig()
126+
127+
err := tt.conf.Unmarshal(cfg)
128+
129+
assert.NoError(t, err)
130+
131+
// Validate the configuration
132+
err = cfg.(*Config).Validate()
133+
if tt.expectError {
134+
assert.Error(t, err)
135+
} else {
136+
assert.NoError(t, err)
137+
}
138+
139+
if err == nil {
140+
assert.Equal(t, tt.expected, cfg)
141+
}
142+
})
143+
}
144+
}

plugins/processors/awsentity/entityattributes/entityattributes.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,52 @@ var attributeEntityToShortNameMap = map[string]string{
8989
AttributeEntityServiceNameSource: ServiceNameSource,
9090
}
9191

92+
// shortNameToEntityMap is the reverse mapping of keyAttributeEntityToShortNameMap
93+
var keyAttributeEntityToLongNameMap = map[string]string{
94+
EntityType: AttributeEntityType,
95+
ResourceType: AttributeEntityResourceType,
96+
Identifier: AttributeEntityIdentifier,
97+
AwsAccountId: AttributeEntityAwsAccountId,
98+
ServiceName: AttributeEntityServiceName,
99+
DeploymentEnvironment: AttributeEntityDeploymentEnvironment,
100+
}
101+
102+
// shortNameToAttributeMap is the reverse mapping of attributeEntityToShortNameMap
103+
var attributeEntityToLongNameMap = map[string]string{
104+
NamespaceField: AttributeEntityNamespace,
105+
Workload: AttributeEntityWorkload,
106+
Node: AttributeEntityNode,
107+
Platform: AttributeEntityPlatformType,
108+
InstanceID: AttributeEntityInstanceID,
109+
AutoscalingGroup: AttributeEntityAutoScalingGroup,
110+
ServiceNameSource: AttributeEntityServiceNameSource,
111+
}
112+
113+
// GetFullAttributeName returns the full attribute name for a given short name
114+
func GetFullAttributeName(shortName string) (string, bool) {
115+
// First check key attributes
116+
if fullName, ok := keyAttributeEntityToLongNameMap[shortName]; ok {
117+
return fullName, true
118+
}
119+
// Then check regular attributes
120+
if fullName, ok := attributeEntityToLongNameMap[shortName]; ok {
121+
return fullName, true
122+
}
123+
return "", false
124+
}
125+
126+
// IsAllowedKeyAttribute checks if the given key is an allowed entity key attribute name
127+
func IsAllowedKeyAttribute(key string) bool {
128+
_, exists := keyAttributeEntityToLongNameMap[key]
129+
return exists
130+
}
131+
132+
// IsAllowedAttribute checks if the given key is an allowed attribute name
133+
func IsAllowedAttribute(key string) bool {
134+
_, exists := attributeEntityToLongNameMap[key]
135+
return exists
136+
}
137+
92138
func CreateCloudWatchEntityFromAttributes(resourceAttributes pcommon.Map) cloudwatch.Entity {
93139
keyAttributesMap := map[string]*string{}
94140
attributeMap := map[string]*string{}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package entitytransformer
5+
6+
import (
7+
"go.opentelemetry.io/collector/pdata/pcommon"
8+
"go.uber.org/zap"
9+
10+
"github.com/aws/amazon-cloudwatch-agent/internal/entity"
11+
"github.com/aws/amazon-cloudwatch-agent/plugins/processors/awsentity/entityattributes"
12+
)
13+
14+
type EntityTransformer struct {
15+
transform *entity.Transform
16+
logger *zap.Logger
17+
}
18+
19+
func NewEntityTransformer(transform *entity.Transform, logger *zap.Logger) *EntityTransformer {
20+
return &EntityTransformer{
21+
transform: transform,
22+
logger: logger,
23+
}
24+
}
25+
26+
func (p *EntityTransformer) ApplyTransforms(resourceAttrs pcommon.Map) {
27+
if p.transform == nil {
28+
return
29+
}
30+
31+
// Apply key attributes
32+
for _, keyAttr := range p.transform.KeyAttributes {
33+
if fullName, ok := entityattributes.GetFullAttributeName(keyAttr.Key); ok {
34+
resourceAttrs.PutStr(fullName, keyAttr.Value)
35+
} else {
36+
p.logger.Debug("Unrecognized key attribute", zap.String("key", keyAttr.Key))
37+
}
38+
}
39+
40+
// Apply additional attributes
41+
for _, attr := range p.transform.Attributes {
42+
if fullName, ok := entityattributes.GetFullAttributeName(attr.Key); ok {
43+
resourceAttrs.PutStr(fullName, attr.Value)
44+
} else {
45+
p.logger.Debug("Unrecognized attribute", zap.String("key", attr.Key))
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)