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

add SDK-specific feature tracking #2682

Merged
merged 4 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
401 changes: 401 additions & 0 deletions .changelog/85f058b6d0bb40eea8548e539473d404.json

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions .changelog/a048e0bc50884e76a29c9524e4ee34eb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"id": "a048e0bc-5088-4e76-a29c-9524e4ee34eb",
"type": "feature",
"collapse": true,
"description": "Add framework for tracking specific features in user-agent string.",
"modules": [
"."
]
}
44 changes: 44 additions & 0 deletions aws/middleware/user_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"runtime"
"sort"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
Expand All @@ -30,6 +31,7 @@ const (
FrameworkMetadata
AdditionalMetadata
ApplicationIdentifier
FeatureMetadata2
)

func (k SDKAgentKeyType) string() string {
Expand All @@ -50,6 +52,8 @@ func (k SDKAgentKeyType) string() string {
return "lib"
case ApplicationIdentifier:
return "app"
case FeatureMetadata2:
return "m"
case AdditionalMetadata:
fallthrough
default:
Expand All @@ -64,9 +68,29 @@ var validChars = map[rune]bool{
'-': true, '.': true, '^': true, '_': true, '`': true, '|': true, '~': true,
}

// UserAgentFeature enumerates tracked SDK features.
type UserAgentFeature string

// Enumerates UserAgentFeature.
const (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked that these are correct (hate when I push a PR and then whoops typo)

UserAgentFeatureResourceModel UserAgentFeature = "A" // n/a (we don't generate separate resource types)
UserAgentFeatureWaiter = "B"
UserAgentFeaturePaginator = "C"
UserAgentFeatureRetryModeLegacy = "D" // n/a (equivalent to standard)
UserAgentFeatureRetryModeStandard = "E"
UserAgentFeatureRetryModeAdaptive = "F"
UserAgentFeatureS3Transfer = "G"
UserAgentFeatureS3CryptoV1N = "H" // n/a (crypto client is external)
UserAgentFeatureS3CryptoV2 = "I" // n/a
UserAgentFeatureS3ExpressBucket = "J"
UserAgentFeatureS3AccessGrants = "K" // not yet implemented
UserAgentFeatureGZIPRequestCompression = "L"
)

// RequestUserAgent is a build middleware that set the User-Agent for the request.
type RequestUserAgent struct {
sdkAgent, userAgent *smithyhttp.UserAgentBuilder
features map[UserAgentFeature]struct{}
}

// NewRequestUserAgent returns a new requestUserAgent which will set the User-Agent and X-Amz-User-Agent for the
Expand All @@ -87,6 +111,7 @@ func NewRequestUserAgent() *RequestUserAgent {
r := &RequestUserAgent{
sdkAgent: sdkAgent,
userAgent: userAgent,
features: map[UserAgentFeature]struct{}{},
}

addSDKMetadata(r)
Expand Down Expand Up @@ -191,6 +216,12 @@ func (u *RequestUserAgent) AddUserAgentKeyValue(key, value string) {
u.userAgent.AddKeyValue(strings.Map(rules, key), strings.Map(rules, value))
}

// AddUserAgentFeature adds the feature ID to the tracking list to be emitted
// in the final User-Agent string.
func (u *RequestUserAgent) AddUserAgentFeature(feature UserAgentFeature) {
u.features[feature] = struct{}{}
}

// AddSDKAgentKey adds the component identified by name to the User-Agent string.
func (u *RequestUserAgent) AddSDKAgentKey(keyType SDKAgentKeyType, key string) {
// TODO: should target sdkAgent
Expand Down Expand Up @@ -227,6 +258,9 @@ func (u *RequestUserAgent) HandleBuild(ctx context.Context, in middleware.BuildI
func (u *RequestUserAgent) addHTTPUserAgent(request *smithyhttp.Request) {
const userAgent = "User-Agent"
updateHTTPHeader(request, userAgent, u.userAgent.Build())
if len(u.features) > 0 {
updateHTTPHeader(request, userAgent, buildFeatureMetrics(u.features))
}
}

func (u *RequestUserAgent) addHTTPSDKAgent(request *smithyhttp.Request) {
Expand Down Expand Up @@ -259,3 +293,13 @@ func rules(r rune) rune {
return '-'
}
}

func buildFeatureMetrics(features map[UserAgentFeature]struct{}) string {
fs := make([]string, 0, len(features))
for f := range features {
fs = append(fs, string(f))
}

sort.Strings(fs)
return fmt.Sprintf("%s/%s", FeatureMetadata2.string(), strings.Join(fs, ","))
}
65 changes: 65 additions & 0 deletions aws/middleware/user_agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,71 @@ func TestAddUserAgentKeyValue(t *testing.T) {
}
}

func TestAddUserAgentFeature(t *testing.T) {
restoreEnv := clearEnv()
defer restoreEnv()

cases := map[string]struct {
Features []UserAgentFeature
Expect string
}{
"none": {
Features: []UserAgentFeature{},
Expect: expectedAgent,
},
"one": {
Features: []UserAgentFeature{
UserAgentFeatureWaiter,
},
Expect: "m/B " + expectedAgent,
},
"two": {
Features: []UserAgentFeature{
UserAgentFeatureRetryModeAdaptive, // ensure stable order, and idempotent
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] not sure if I'd call it stable order since this maps to a set at the end, and we end up sorting the feature set

UserAgentFeatureRetryModeAdaptive,
UserAgentFeatureWaiter,
},
Expect: "m/B,F " + expectedAgent,
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
b := NewRequestUserAgent()
stack := middleware.NewStack("testStack", smithyhttp.NewStackRequest)
err := stack.Build.Add(b, middleware.After)
if err != nil {
t.Fatalf("expect no error, got %v", err)
}

for _, f := range c.Features {
b.AddUserAgentFeature(f)
}

in := middleware.BuildInput{
Request: &smithyhttp.Request{
Request: &http.Request{
Header: map[string][]string{},
},
},
}
_, _, err = b.HandleBuild(context.Background(), in, middleware.BuildHandlerFunc(func(ctx context.Context, input middleware.BuildInput) (o middleware.BuildOutput, m middleware.Metadata, err error) {
return o, m, err
}))
if err != nil {
t.Fatalf("expect no error, got %v", err)
}
ua, ok := in.Request.(*smithyhttp.Request).Header["User-Agent"]
if !ok {
t.Fatalf("expect User-Agent to be present")
}
if ua[0] != c.Expect {
t.Errorf("User-Agent did not match expected, %v != %v", c.Expect, ua[0])
}
})
}
}

func TestAddSDKAgentKey(t *testing.T) {
restoreEnv := clearEnv()
defer restoreEnv()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.aws.go.codegen.customization;

import java.util.Map;
import java.util.Set;

import software.amazon.smithy.aws.go.codegen.AwsGoDependency;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.go.codegen.GoDelegator;
import software.amazon.smithy.go.codegen.GoSettings;
import software.amazon.smithy.go.codegen.SmithyGoDependency;
import software.amazon.smithy.go.codegen.integration.Paginators;
import software.amazon.smithy.model.Model;

import static software.amazon.smithy.go.codegen.GoWriter.goTemplate;
import static software.amazon.smithy.go.codegen.SymbolUtils.buildPackageSymbol;

/**
* Extends the base smithy Paginators integration to track in the User-Agent string.
*/
public class AwsPaginators extends Paginators {
@Override
public Set<Symbol> getAdditionalClientOptions() {
return Set.of(buildPackageSymbol("addIsPaginatorUserAgent"));
}

@Override
public void writeAdditionalFiles(GoSettings settings, Model model, SymbolProvider symbolProvider, GoDelegator goDelegator) {
super.writeAdditionalFiles(settings, model, symbolProvider, goDelegator);

goDelegator.useFileWriter("api_client.go", settings.getModuleName(), goTemplate("""
func addIsPaginatorUserAgent(o *Options) {
o.APIOptions = append(o.APIOptions, func(stack $stack:P) error {
ua, err := getOrAddRequestUserAgent(stack)
if err != nil {
return err
}

ua.AddUserAgentFeature($featurePaginator:T)
return nil
})
}""",
Map.of(
"stack", SmithyGoDependency.SMITHY_MIDDLEWARE.struct("Stack"),
"featurePaginator", AwsGoDependency.AWS_MIDDLEWARE.valueSymbol("UserAgentFeaturePaginator")
)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.aws.go.codegen.customization;

import java.util.Map;
import java.util.Set;

import software.amazon.smithy.aws.go.codegen.AwsGoDependency;
import software.amazon.smithy.codegen.core.Symbol;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.go.codegen.GoDelegator;
import software.amazon.smithy.go.codegen.GoSettings;
import software.amazon.smithy.go.codegen.SmithyGoDependency;
import software.amazon.smithy.go.codegen.integration.Waiters;
import software.amazon.smithy.model.Model;

import static software.amazon.smithy.go.codegen.GoWriter.goTemplate;
import static software.amazon.smithy.go.codegen.SymbolUtils.buildPackageSymbol;

/**
* Extends the base smithy Waiters integration to track in the User-Agent string.
*/
public class AwsWaiters extends Waiters {
@Override
public Set<Symbol> getAdditionalClientOptions() {
return Set.of(buildPackageSymbol("addIsWaiterUserAgent"));
}

@Override
public void writeAdditionalFiles(GoSettings settings, Model model, SymbolProvider symbolProvider, GoDelegator goDelegator) {
super.writeAdditionalFiles(settings, model, symbolProvider, goDelegator);

goDelegator.useFileWriter("api_client.go", settings.getModuleName(), goTemplate("""
func addIsWaiterUserAgent(o *Options) {
o.APIOptions = append(o.APIOptions, func(stack $stack:P) error {
ua, err := getOrAddRequestUserAgent(stack)
if err != nil {
return err
}

ua.AddUserAgentFeature($featureWaiter:T)
return nil
})
}""",
Map.of(
"stack", SmithyGoDependency.SMITHY_MIDDLEWARE.struct("Stack"),
"featureWaiter", AwsGoDependency.AWS_MIDDLEWARE.valueSymbol("UserAgentFeatureWaiter")
)));
}
}
Loading
Loading