Skip to content

Commit

Permalink
[exporter/awsemf] Add AppSignals metadata flag into user-agent in EMF…
Browse files Browse the repository at this point in the history
… exporter (#32998)

**Description:** 
Cherry-picking from downstream:
amazon-contributing#105

This PR adds AppSignals metadata flag in user-agent in EMF exporter, so
CWLogs backend can indicate EMF logs request type for the corresponding
actions

---------

Co-authored-by: Min Xia <[email protected]>
Co-authored-by: Jeffrey Chien <[email protected]>
Co-authored-by: Vastin <[email protected]>
  • Loading branch information
4 people authored Jun 26, 2024
1 parent 4623e7e commit 041812a
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 19 deletions.
27 changes: 27 additions & 0 deletions .chloggen/appsignals-flag-in-emfexporter-user-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
component: awsemfexporter

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: AWS EMF Exporter to add AppSignals metadata flag into the user-agent

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [32998]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
13 changes: 13 additions & 0 deletions exporter/awsemfexporter/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package awsemfexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsemfexporter"

import (
"strings"

"go.opentelemetry.io/collector/component"
"go.uber.org/zap"

Expand Down Expand Up @@ -134,7 +136,18 @@ func (config *Config) Validate() error {
}

return cwlogs.ValidateTagsInput(config.Tags)
}

func (config *Config) isAppSignalsEnabled() bool {
if config.LogGroupName == "" || config.Namespace == "" {
return false
}

if config.Namespace == appSignalsMetricNamespace && strings.HasPrefix(config.LogGroupName, appSignalsLogGroupNamePrefix) {
return true
}

return false
}

func newEMFSupportedUnits() map[string]any {
Expand Down
54 changes: 54 additions & 0 deletions exporter/awsemfexporter/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,57 @@ func TestNoDimensionRollupFeatureGate(t *testing.T) {
assert.Equal(t, cfg.(*Config).DimensionRollupOption, "NoDimensionRollup")
_ = featuregate.GlobalRegistry().Set("awsemf.nodimrollupdefault", false)
}

func TestIsAppSignalsEnabled(t *testing.T) {
tests := []struct {
name string
metricNameSpace string
logGroupName string
expectedResult bool
}{
{
"validAppSignalsEMF",
"AppSignals",
"/aws/appsignals/eks",
true,
},
{
"invalidAppSignalsLogsGroup",
"AppSignals",
"/nonaws/appsignals/eks",
false,
},
{
"invalidAppSignalsMetricNamespace",
"NonAppSignals",
"/aws/appsignals/eks",
false,
},
{
"invalidAppSignalsEMF",
"NonAppSignals",
"/nonaws/appsignals/eks",
false,
},
{
"defaultConfig",
"",
"",
false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
factory := NewFactory()
cfg := factory.CreateDefaultConfig().(*Config)
if len(tc.metricNameSpace) > 0 {
cfg.Namespace = tc.metricNameSpace
}
if len(tc.logGroupName) > 0 {
cfg.LogGroupName = tc.logGroupName
}

assert.Equal(t, cfg.isAppSignalsEnabled(), tc.expectedResult)
})
}
}
20 changes: 19 additions & 1 deletion exporter/awsemfexporter/emf_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const (
// OutputDestination Options
outputDestinationCloudWatch = "cloudwatch"
outputDestinationStdout = "stdout"

// AppSignals EMF config
appSignalsMetricNamespace = "AppSignals"
appSignalsLogGroupNamePrefix = "/aws/appsignals/"
)

type emfExporter struct {
Expand Down Expand Up @@ -55,8 +59,22 @@ func newEmfExporter(config *Config, set exporter.Settings) (*emfExporter, error)
return nil, err
}

var userAgentExtras []string
if config.isAppSignalsEnabled() {
userAgentExtras = append(userAgentExtras, "AppSignals")
}

// create CWLogs client with aws session config
svcStructuredLog := cwlogs.NewClient(set.Logger, awsConfig, set.BuildInfo, config.LogGroupName, config.LogRetention, config.Tags, session, metadata.Type.String())
svcStructuredLog := cwlogs.NewClient(set.Logger,
awsConfig,
set.BuildInfo,
config.LogGroupName,
config.LogRetention,
config.Tags,
session,
metadata.Type.String(),
cwlogs.WithUserAgentExtras(userAgentExtras...),
)
collectorIdentifier, err := uuid.NewRandom()

if err != nil {
Expand Down
48 changes: 36 additions & 12 deletions internal/aws/cwlogs/cwlog_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const (
errCodeThrottlingException = "ThrottlingException"
)

var (
containerInsightsRegexPattern = regexp.MustCompile(`^/aws/.*containerinsights/.*/(performance|prometheus)$`)
)

// Possible exceptions are combination of common errors (https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/CommonErrors.html)
// and API specific erros (e.g. https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html#API_PutLogEvents_Errors)
type Client struct {
Expand All @@ -35,6 +39,18 @@ type Client struct {
logger *zap.Logger
}

type ClientOption func(*cwLogClientConfig)

type cwLogClientConfig struct {
userAgentExtras []string
}

func WithUserAgentExtras(userAgentExtras ...string) ClientOption {
return func(config *cwLogClientConfig) {
config.userAgentExtras = append(config.userAgentExtras, userAgentExtras...)
}
}

// Create a log client based on the actual cloudwatch logs client.
func newCloudWatchLogClient(svc cloudwatchlogsiface.CloudWatchLogsAPI, logRetention int64, tags map[string]*string, logger *zap.Logger) *Client {
logClient := &Client{svc: svc,
Expand All @@ -45,10 +61,19 @@ func newCloudWatchLogClient(svc cloudwatchlogsiface.CloudWatchLogsAPI, logRetent
}

// NewClient create Client
func NewClient(logger *zap.Logger, awsConfig *aws.Config, buildInfo component.BuildInfo, logGroupName string, logRetention int64, tags map[string]*string, sess *session.Session, componentName string) *Client {
func NewClient(logger *zap.Logger, awsConfig *aws.Config, buildInfo component.BuildInfo, logGroupName string, logRetention int64, tags map[string]*string, sess *session.Session, componentName string, opts ...ClientOption) *Client {
client := cloudwatchlogs.New(sess, awsConfig)
client.Handlers.Build.PushBackNamed(handler.RequestStructuredLogHandler)
client.Handlers.Build.PushFrontNamed(newCollectorUserAgentHandler(buildInfo, logGroupName, componentName))

// Loop through each option
option := &cwLogClientConfig{
userAgentExtras: []string{},
}
for _, opt := range opts {
opt(option)
}

client.Handlers.Build.PushFrontNamed(newCollectorUserAgentHandler(buildInfo, logGroupName, componentName, option))
return newCloudWatchLogClient(client, logRetention, tags, logger)
}

Expand Down Expand Up @@ -175,19 +200,18 @@ func (client *Client) CreateStream(logGroup, streamName *string) error {
return nil
}

func newCollectorUserAgentHandler(buildInfo component.BuildInfo, logGroupName string, componentName string) request.NamedHandler {
fn := request.MakeAddToUserAgentHandler(buildInfo.Command, buildInfo.Version, componentName)
if matchContainerInsightsPattern(logGroupName) {
fn = request.MakeAddToUserAgentHandler(buildInfo.Command, buildInfo.Version, componentName, "ContainerInsights")
func newCollectorUserAgentHandler(buildInfo component.BuildInfo, logGroupName string, componentName string, clientConfig *cwLogClientConfig) request.NamedHandler {
extraStrs := []string{componentName}
extraStrs = append(extraStrs, clientConfig.userAgentExtras...)

if containerInsightsRegexPattern.MatchString(logGroupName) {
extraStrs = append(extraStrs, "ContainerInsights")
}

fn := request.MakeAddToUserAgentHandler(buildInfo.Command, buildInfo.Version, extraStrs...)

return request.NamedHandler{
Name: "otel.collector.UserAgentHandler",
Fn: fn,
}
}

func matchContainerInsightsPattern(logGroupName string) bool {
regexP := "^/aws/.*containerinsights/.*/(performance|prometheus)$"
r, _ := regexp.Compile(regexP)
return r.MatchString(logGroupName)
}
54 changes: 48 additions & 6 deletions internal/aws/cwlogs/cwlog_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,50 +538,92 @@ func TestUserAgent(t *testing.T) {
name string
buildInfo component.BuildInfo
logGroupName string
clientOptions []ClientOption
expectedUserAgentStr string
}{
{
"emptyLogGroup",
"emptyLogGroupAndEmptyClientOptions",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"",
[]ClientOption{},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s)", expectedComponentName),
},
{
"emptyLogGroupWithEmptyUserAgentExtras",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"",
[]ClientOption{WithUserAgentExtras()},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s)", expectedComponentName),
},
{
"buildInfoCommandUsed",
component.BuildInfo{Command: "test-collector-contrib", Version: "1.0"},
"",
[]ClientOption{},
fmt.Sprintf("test-collector-contrib/1.0 (%s)", expectedComponentName),
},
{
"buildInfoCommandUsedWithEmptyUserAgentExtras",
component.BuildInfo{Command: "test-collector-contrib", Version: "1.0"},
"",
[]ClientOption{WithUserAgentExtras()},
fmt.Sprintf("test-collector-contrib/1.0 (%s)", expectedComponentName),
},
{
"non container insights",
"nonContainerInsights",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.1"},
"test-group",
[]ClientOption{},
fmt.Sprintf("opentelemetry-collector-contrib/1.1 (%s)", expectedComponentName),
},
{
"container insights EKS",
"containerInsightsEKS",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"/aws/containerinsights/eks-cluster-name/performance",
[]ClientOption{},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s; ContainerInsights)", expectedComponentName),
},
{
"container insights ECS",
"containerInsightsECS",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"/aws/ecs/containerinsights/ecs-cluster-name/performance",
[]ClientOption{},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s; ContainerInsights)", expectedComponentName),
},
{
"container insights prometheus",
"containerInsightsPrometheus",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"/aws/containerinsights/cluster-name/prometheus",
[]ClientOption{},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s; ContainerInsights)", expectedComponentName),
},
{
"validAppSignalsLogGroupAndAgentString",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"/aws/appsignals",
[]ClientOption{WithUserAgentExtras("AppSignals")},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s; AppSignals)", expectedComponentName),
},
{
"multipleAgentStringExtras",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"/aws/appsignals",
[]ClientOption{WithUserAgentExtras("abcde", "vwxyz", "12345")},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s; abcde; vwxyz; 12345)", expectedComponentName),
},
{
"containerInsightsEKSWithMultipleAgentStringExtras",
component.BuildInfo{Command: "opentelemetry-collector-contrib", Version: "1.0"},
"/aws/containerinsights/eks-cluster-name/performance",
[]ClientOption{WithUserAgentExtras("extra0", "extra1", "extra2")},
fmt.Sprintf("opentelemetry-collector-contrib/1.0 (%s; extra0; extra1; extra2; ContainerInsights)", expectedComponentName),
},
}

testSession, _ := session.NewSession()
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cwlog := NewClient(logger, &aws.Config{}, tc.buildInfo, tc.logGroupName, 0, map[string]*string{}, testSession, expectedComponentName)
cwlog := NewClient(logger, &aws.Config{}, tc.buildInfo, tc.logGroupName, 0, map[string]*string{}, testSession, expectedComponentName, tc.clientOptions...)
logClient := cwlog.svc.(*cloudwatchlogs.CloudWatchLogs)

req := request.New(aws.Config{}, metadata.ClientInfo{}, logClient.Handlers, nil, &request.Operation{
Expand Down

0 comments on commit 041812a

Please sign in to comment.