Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[exporter/awsemf] Add AppSignals metadata flag into user-agent in EMF exporter #32998

Merged
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.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 {
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