Skip to content

Commit

Permalink
[exporter/awsemfexporter] Add app signals specific user agent. (open-…
Browse files Browse the repository at this point in the history
  • Loading branch information
jefchien authored Feb 27, 2024
1 parent 233dd19 commit 819e4fe
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 7 deletions.
21 changes: 21 additions & 0 deletions .chloggen-aws/sdk-telemetry-user-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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: Adds additional user agents for the telemetry SDKs if AppSignals is enabled.

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

# (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:

# e.g. '[aws]'
# Include 'aws' if the change is done done by cwa
# Default: '[user]'
change_logs: [aws]
1 change: 1 addition & 0 deletions cmd/configschema/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ require (
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jellydator/ttlcache/v3 v3.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions cmd/configschema/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cmd/otelcontribcol/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ require (
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jellydator/ttlcache/v3 v3.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions cmd/otelcontribcol/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 18 additions & 7 deletions exporter/awsemfexporter/emf_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"go.opentelemetry.io/collector/pdata/pmetric"
"go.uber.org/zap"

"github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsemfexporter/internal/appsignals"
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/aws/awsutil"
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/aws/cwlogs"
)
Expand All @@ -44,6 +45,8 @@ type emfExporter struct {
pusherMapLock sync.Mutex
retryCnt int
collectorID string

processResourceLabels func(map[string]string)
}

// newEmfExporter creates a new exporter using exporterhelper
Expand Down Expand Up @@ -71,19 +74,26 @@ func newEmfExporter(config *Config, set exporter.CreateSettings) (*emfExporter,
cwlogs.WithEnabledContainerInsights(config.IsEnhancedContainerInsights()),
cwlogs.WithEnabledAppSignals(config.IsAppSignalsEnabled()),
)
collectorIdentifier, err := uuid.NewRandom()

collectorIdentifier, err := uuid.NewRandom()
if err != nil {
return nil, err
}

emfExporter := &emfExporter{
svcStructuredLog: svcStructuredLog,
config: config,
metricTranslator: newMetricTranslator(*config),
retryCnt: *awsConfig.MaxRetries,
collectorID: collectorIdentifier.String(),
pusherMap: map[cwlogs.StreamKey]cwlogs.Pusher{},
svcStructuredLog: svcStructuredLog,
config: config,
metricTranslator: newMetricTranslator(*config),
retryCnt: *awsConfig.MaxRetries,
collectorID: collectorIdentifier.String(),
pusherMap: map[cwlogs.StreamKey]cwlogs.Pusher{},
processResourceLabels: func(map[string]string) {},
}

if config.IsAppSignalsEnabled() {
userAgent := appsignals.NewUserAgent()
svcStructuredLog.Handlers().Build.PushBackNamed(userAgent.Handler())
emfExporter.processResourceLabels = userAgent.Process
}

config.logger.Warn("the default value for DimensionRollupOption will be changing to NoDimensionRollup" +
Expand All @@ -107,6 +117,7 @@ func (emf *emfExporter) pushMetricsData(_ context.Context, md pmetric.Metrics) e
}
}
emf.config.logger.Info("Start processing resource metrics", zap.Any("labels", labels))
emf.processResourceLabels(labels)

groupedMetrics := make(map[any]*groupedMetric)
defaultLogStream := fmt.Sprintf("otel-stream-%s", emf.collectorID)
Expand Down
2 changes: 2 additions & 0 deletions exporter/awsemfexporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/amazon-contributing/opentelemetry-collector-contrib/extension/awsmiddleware v0.0.0-00010101000000-000000000000
github.com/aws/aws-sdk-go v1.47.10
github.com/google/uuid v1.4.0
github.com/jellydator/ttlcache/v3 v3.1.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/aws/awsutil v0.89.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/aws/cwlogs v0.89.0
github.com/open-telemetry/opentelemetry-collector-contrib/internal/aws/metrics v0.89.0
Expand Down Expand Up @@ -54,6 +55,7 @@ require (
go.opentelemetry.io/otel/metric v1.20.0 // indirect
go.opentelemetry.io/otel/trace v1.20.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
Expand Down
4 changes: 4 additions & 0 deletions exporter/awsemfexporter/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 116 additions & 0 deletions exporter/awsemfexporter/internal/appsignals/useragent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package appsignals

import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"

"github.com/aws/aws-sdk-go/aws/request"
"github.com/jellydator/ttlcache/v3"
semconv "go.opentelemetry.io/collector/semconv/v1.18.0"
)

const (
handlerName = "aws.appsignals.UserAgentHandler"
// defaultTTL is how long an item in the cache will remain if it has not been re-seen.
defaultTTL = time.Minute
// cacheSize is the maximum number of unique telemetry SDK languages that can be stored before one will be evicted.
cacheSize = 5
// attrLengthLimit is the maximum length of the language and version that will be used for the user agent.
attrLengthLimit = 20

// TODO: Available in semconv/v1.21.0+. Replace after collector dependency is v0.91.0+.
attributeTelemetryDistroVersion = "telemetry.distro.version"
)

type UserAgent struct {
mu sync.RWMutex
prebuiltStr string
cache *ttlcache.Cache[string, string]
}

func NewUserAgent() *UserAgent {
return newUserAgent(defaultTTL)
}

func newUserAgent(ttl time.Duration) *UserAgent {
ua := &UserAgent{
cache: ttlcache.New[string, string](
ttlcache.WithTTL[string, string](ttl),
ttlcache.WithCapacity[string, string](cacheSize),
),
}
ua.cache.OnEviction(func(context.Context, ttlcache.EvictionReason, *ttlcache.Item[string, string]) {
ua.build()
})
go ua.cache.Start()
return ua
}

// Handler creates a named handler with the UserAgent's handle function.
func (ua *UserAgent) Handler() request.NamedHandler {
return request.NamedHandler{
Name: handlerName,
Fn: ua.handle,
}
}

// handle adds the pre-built user agent string to the user agent header.
func (ua *UserAgent) handle(r *request.Request) {
ua.mu.RLock()
defer ua.mu.RUnlock()
request.AddToUserAgent(r, ua.prebuiltStr)
}

// Process takes the telemetry SDK language and version and adds them to the cache. If it already exists in the
// cache and has the same value, extends the TTL. If not, then it sets it and rebuilds the user agent string.
func (ua *UserAgent) Process(labels map[string]string) {
language := labels[semconv.AttributeTelemetrySDKLanguage]
version := labels[attributeTelemetryDistroVersion]
if version == "" {
version = labels[semconv.AttributeTelemetryAutoVersion]
}
if language != "" && version != "" {
language = truncate(language, attrLengthLimit)
version = truncate(version, attrLengthLimit)
value := ua.cache.Get(language)
if value == nil || value.Value() != version {
ua.cache.Set(language, version, ttlcache.DefaultTTL)
ua.build()
}
}
}

// build the user agent string from the items in the cache. Format is telemetry-sdk (<lang1>/<ver1>;<lang2>/<ver2>).
func (ua *UserAgent) build() {
ua.mu.Lock()
defer ua.mu.Unlock()
var items []string
for _, item := range ua.cache.Items() {
items = append(items, formatStr(item.Key(), item.Value()))
}
ua.prebuiltStr = ""
if len(items) > 0 {
sort.Strings(items)
ua.prebuiltStr = fmt.Sprintf("telemetry-sdk (%s)", strings.Join(items, ";"))
}
}

// formatStr formats the telemetry SDK language and version into the user agent format.
func formatStr(language, version string) string {
return language + "/" + version
}

func truncate(s string, n int) string {
s = strings.TrimSpace(s)
if len(s) > n {
return strings.TrimSpace(s[:n])
}
return s
}
111 changes: 111 additions & 0 deletions exporter/awsemfexporter/internal/appsignals/useragent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package appsignals

import (
"net/http"
"testing"
"time"

"github.com/aws/aws-sdk-go/aws/request"
"github.com/stretchr/testify/assert"
semconv "go.opentelemetry.io/collector/semconv/v1.18.0"
)

func TestUserAgent(t *testing.T) {
testCases := map[string]struct {
labelSets []map[string]string
want string
}{
"WithEmpty": {},
"WithPartialAttributes": {
labelSets: []map[string]string{
{
semconv.AttributeTelemetrySDKLanguage: "foo",
},
{
semconv.AttributeTelemetryAutoVersion: "1.0",
},
},
},
"WithMultipleLanguages": {
labelSets: []map[string]string{
{
semconv.AttributeTelemetrySDKLanguage: "foo",
attributeTelemetryDistroVersion: "1.1",
},
{
semconv.AttributeTelemetrySDKLanguage: "bar",
semconv.AttributeTelemetryAutoVersion: "2.0",
attributeTelemetryDistroVersion: "1.0",
},
{
semconv.AttributeTelemetrySDKLanguage: "baz",
semconv.AttributeTelemetryAutoVersion: "2.0",
},
},
want: "telemetry-sdk (bar/1.0;baz/2.0;foo/1.1)",
},
"WithMultipleVersions": {
labelSets: []map[string]string{
{
semconv.AttributeTelemetrySDKLanguage: "test",
semconv.AttributeTelemetryAutoVersion: "1.1",
},
{
semconv.AttributeTelemetrySDKLanguage: "test",
attributeTelemetryDistroVersion: "1.0",
},
},
want: "telemetry-sdk (test/1.0)",
},
"WithTruncatedAttributes": {
labelSets: []map[string]string{
{
semconv.AttributeTelemetrySDKLanguage: " incrediblyverboselanguagename",
semconv.AttributeTelemetryAutoVersion: "notsemanticversioningversion",
},
},
want: "telemetry-sdk (incrediblyverboselan/notsemanticversionin)",
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
userAgent := NewUserAgent()
for _, labelSet := range testCase.labelSets {
userAgent.Process(labelSet)
}
req := &request.Request{
HTTPRequest: &http.Request{
Header: http.Header{},
},
}
userAgent.Handler().Fn(req)
assert.Equal(t, testCase.want, req.HTTPRequest.Header.Get("User-Agent"))
})
}
}

func TestUserAgentExpiration(t *testing.T) {
userAgent := newUserAgent(50 * time.Millisecond)
req := &request.Request{
HTTPRequest: &http.Request{
Header: http.Header{},
},
}
labels := map[string]string{
semconv.AttributeTelemetrySDKLanguage: "test",
semconv.AttributeTelemetryAutoVersion: "1.0",
}
userAgent.Process(labels)
userAgent.handle(req)
assert.Equal(t, "telemetry-sdk (test/1.0)", req.HTTPRequest.Header.Get("User-Agent"))

// wait for expiration
time.Sleep(100 * time.Millisecond)
// reset user-agent header
req.HTTPRequest.Header.Del("User-Agent")
userAgent.handle(req)
assert.Empty(t, req.HTTPRequest.Header.Get("User-Agent"))
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ require (
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jellydator/ttlcache/v3 v3.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 819e4fe

Please sign in to comment.