diff --git a/.chloggen/appsignals-flag-in-emfexporter-user-agent.yaml b/.chloggen/appsignals-flag-in-emfexporter-user-agent.yaml new file mode 100644 index 000000000000..8c60f5adb85f --- /dev/null +++ b/.chloggen/appsignals-flag-in-emfexporter-user-agent.yaml @@ -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] diff --git a/exporter/awsemfexporter/config.go b/exporter/awsemfexporter/config.go index 9a93c558bf21..969a69e67798 100644 --- a/exporter/awsemfexporter/config.go +++ b/exporter/awsemfexporter/config.go @@ -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" @@ -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 { diff --git a/exporter/awsemfexporter/config_test.go b/exporter/awsemfexporter/config_test.go index 21d664f23d85..88614335cdeb 100644 --- a/exporter/awsemfexporter/config_test.go +++ b/exporter/awsemfexporter/config_test.go @@ -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) + }) + } +} diff --git a/exporter/awsemfexporter/emf_exporter.go b/exporter/awsemfexporter/emf_exporter.go index adeaa48b6354..ed3ffa24f156 100644 --- a/exporter/awsemfexporter/emf_exporter.go +++ b/exporter/awsemfexporter/emf_exporter.go @@ -27,6 +27,10 @@ const ( // OutputDestination Options outputDestinationCloudWatch = "cloudwatch" outputDestinationStdout = "stdout" + + // AppSignals EMF config + appSignalsMetricNamespace = "AppSignals" + appSignalsLogGroupNamePrefix = "/aws/appsignals/" ) type emfExporter struct { @@ -55,8 +59,22 @@ func newEmfExporter(config *Config, set exporter.CreateSettings) (*emfExporter, 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 { diff --git a/internal/aws/cwlogs/cwlog_client.go b/internal/aws/cwlogs/cwlog_client.go index 07da718a3fb3..1cbf21ab1080 100644 --- a/internal/aws/cwlogs/cwlog_client.go +++ b/internal/aws/cwlogs/cwlog_client.go @@ -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 { @@ -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, @@ -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) } @@ -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) -} diff --git a/internal/aws/cwlogs/cwlog_client_test.go b/internal/aws/cwlogs/cwlog_client_test.go index 919d1f59607f..36c5f30004d9 100644 --- a/internal/aws/cwlogs/cwlog_client_test.go +++ b/internal/aws/cwlogs/cwlog_client_test.go @@ -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{