Skip to content

Commit 873af21

Browse files
Generate and Send the User Agent String with Business Metrics (#3729)
1 parent 80c45bb commit 873af21

File tree

22 files changed

+435
-454
lines changed

22 files changed

+435
-454
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"core": {
3+
"changeLogMessages": [
4+
"Update the AWS SDK for .NET to include encoded metrics in the `User-Agent` header to track which features were used for a given request (for example, which retry behavior and how credentials were resolved)"
5+
],
6+
"type": "patch",
7+
"updateMinimum": true
8+
}
9+
}

Diff for: generator/ServiceModels/s3/s3.customizations.json

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
{ "operation": "addAfter", "newType": "Amazon.S3.Internal.AmazonS3RedirectHandler", "targetType": "Amazon.Runtime.Internal.Unmarshaller" },
99
{ "operation": "addBefore", "newType": "Amazon.S3.Internal.S3Express.S3ExpressPreSigner", "targetType": "Amazon.Runtime.Internal.Signer" },
1010
{ "operation": "addAfter", "newType": "Amazon.S3.Internal.AmazonS3PostMarshallHandler", "targetType": "Amazon.Runtime.Internal.EndpointResolver" },
11-
{ "operation": "addAfter", "newType": "Amazon.S3.Internal.AmazonS3UserAgentHandler", "targetType": "Amazon.Runtime.Internal.ChecksumHandler" },
1211
{
1312
"condition":"this.Config.RetryMode == RequestRetryMode.Standard",
1413
"operation": "replace",

Diff for: sdk/src/Core/Amazon.Runtime/AmazonWebServiceRequest.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515
using System;
1616
using System.Collections.Generic;
17-
using System.Text;
17+
using Amazon.Runtime.Internal.UserAgent;
1818

1919
namespace Amazon.Runtime
2020
{
@@ -26,7 +26,7 @@ public abstract partial class AmazonWebServiceRequest : Amazon.Runtime.Internal.
2626
private readonly object _lock = new object();
2727

2828
internal RequestEventHandler mBeforeRequestEvent;
29-
internal string UserAgentAddition { get; set; } = null;
29+
UserAgentDetails Amazon.Runtime.Internal.IAmazonWebServiceRequest.UserAgentDetails { get; } = new UserAgentDetails();
3030

3131
internal event RequestEventHandler BeforeRequestEvent
3232
{

Diff for: sdk/src/Core/Amazon.Runtime/Internal/IAmazonWebServiceRequest.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using Amazon.Runtime.Internal.UserAgent;
2+
using System;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Text;
@@ -15,7 +16,7 @@ public interface IAmazonWebServiceRequest
1516

1617
Dictionary<string, object> RequestState { get; }
1718

18-
1919
SignatureVersion SignatureVersion { get; set; }
20+
UserAgentDetails UserAgentDetails { get; }
2021
}
2122
}

Diff for: sdk/src/Core/Amazon.Runtime/Internal/UserAgent/UserAgentDetails.cs

+59-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* permissions and limitations under the License.
1414
*/
1515

16+
using System.Text;
1617
using System.Collections.Generic;
1718

1819
namespace Amazon.Runtime.Internal.UserAgent
@@ -26,12 +27,25 @@ public class UserAgentDetails
2627
{
2728
private const int MaxSizeBytes = 1024; // 1 KB size limit
2829
private readonly HashSet<string> _trackedFeatureIds = new HashSet<string>();
30+
private readonly StringBuilder _userAgentBuilder = new StringBuilder();
2931

3032
/// <summary>
3133
/// Gets the list of tracked feature IDs.
3234
/// </summary>
3335
public IEnumerable<string> TrackedFeatureIds => _trackedFeatureIds;
3436

37+
/// <summary>
38+
/// Adds a component to the user-agent string.
39+
/// </summary>
40+
/// <param name="component">The user-agent component to append.</param>
41+
public void AddUserAgentComponent(string component)
42+
{
43+
if (!string.IsNullOrEmpty(component))
44+
{
45+
_userAgentBuilder.Append(' ').Append(component);
46+
}
47+
}
48+
3549
/// <summary>
3650
/// Adds a feature metric to be included in the User-Agent string.
3751
/// Duplicate entries are ignored.
@@ -43,17 +57,57 @@ public void AddFeature(UserAgentFeatureId featureId)
4357
_trackedFeatureIds.Add(featureId.Value);
4458
}
4559

60+
/// <summary>
61+
/// Appends the metrics user-agent to the existing user-agent and returns the full string.
62+
/// </summary>
63+
/// <returns>The final user-agent string including metrics data.</returns>
64+
public string GenerateUserAgentWithMetrics()
65+
{
66+
var metricsUserAgent = GenerateMetricsUserAgent();
67+
if (!string.IsNullOrEmpty(metricsUserAgent))
68+
{
69+
return $"{_userAgentBuilder.ToString().Trim()} {metricsUserAgent}";
70+
}
71+
return _userAgentBuilder.ToString().Trim();
72+
}
73+
4674
/// <summary>
4775
/// Generates the User-Agent metrics string.
4876
/// Ensures the final string does not exceed 1024 bytes.
4977
/// </summary>
5078
/// <returns>The formatted User-Agent metrics string.</returns>
51-
#pragma warning disable CA1822 // Mark members as static
52-
public string GenerateMetricsUserAgent()
53-
#pragma warning restore CA1822 // Mark members as static
79+
private string GenerateMetricsUserAgent()
5480
{
55-
// TODO
56-
return "m/{metricsString}";
81+
if (_trackedFeatureIds.Count == 0)
82+
{
83+
return string.Empty;
84+
}
85+
var metricsString = $"m/{string.Join(",", _trackedFeatureIds)}";
86+
metricsString = TruncateToSize(metricsString);
87+
88+
return metricsString;
89+
}
90+
91+
/// <summary>
92+
/// Truncates a comma-separated string to ensure it fits within a given byte limit.
93+
/// </summary>
94+
/// <param name="input">The input string.</param>
95+
/// <returns>A truncated version of the string within the byte limit.</returns>
96+
private static string TruncateToSize(string input)
97+
{
98+
byte[] bytes = Encoding.UTF8.GetBytes(input);
99+
if (bytes.Length <= MaxSizeBytes)
100+
{
101+
return input;
102+
}
103+
104+
var cutOffIndex = MaxSizeBytes;
105+
while (cutOffIndex > 0 && bytes[cutOffIndex - 1] != ',')
106+
{
107+
cutOffIndex--;
108+
}
109+
110+
return Encoding.UTF8.GetString(bytes, 0, cutOffIndex - 1); // Remove last comma
57111
}
58112
}
59113
}

Diff for: sdk/src/Core/Amazon.Runtime/Pipeline/Contexts.cs

+9-10
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public interface IAsyncRequestContext : IRequestContext
6767
{
6868
AsyncCallback Callback { get; }
6969
object State { get; }
70-
}
70+
}
7171

7272
public interface IAsyncResponseContext : IResponseContext
7373
{
@@ -96,7 +96,7 @@ public class RequestContext : IRequestContext
9696
IDictionary<string, object> _contextAttributes;
9797

9898
public RequestContext(bool enableMetric)
99-
: this (enableMetric, null)
99+
: this(enableMetric, null)
100100
{
101101
}
102102

@@ -106,7 +106,6 @@ public RequestContext(bool enableMetrics, ISigner clientSigner)
106106
this.Metrics = new RequestMetrics();
107107
this.Metrics.IsEnabled = enableMetrics;
108108
this.InvocationId = Guid.NewGuid();
109-
this.UserAgentDetails = new UserAgentDetails();
110109
}
111110

112111
public IRequest Request { get; set; }
@@ -120,10 +119,10 @@ public RequestContext(bool enableMetrics, ISigner clientSigner)
120119
public AmazonWebServiceRequest OriginalRequest { get; set; }
121120
public IMarshaller<IRequest, AmazonWebServiceRequest> Marshaller { get; set; }
122121
public ResponseUnmarshaller Unmarshaller { get; set; }
123-
public InvokeOptionsBase Options { get; set; }
122+
public InvokeOptionsBase Options { get; set; }
124123
public ISigner Signer { get; set; }
125124
public BaseIdentity Identity { get; set; }
126-
public UserAgentDetails UserAgentDetails { get; }
125+
public UserAgentDetails UserAgentDetails { get => ((IAmazonWebServiceRequest)OriginalRequest).UserAgentDetails; }
127126

128127
#if AWS_ASYNC_API
129128
public System.Threading.CancellationToken CancellationToken { get; set; }
@@ -162,11 +161,11 @@ internal set
162161

163162
public Guid InvocationId { get; private set; }
164163

165-
public IDictionary<string, object> ContextAttributes
166-
{
164+
public IDictionary<string, object> ContextAttributes
165+
{
167166
get
168167
{
169-
if(_contextAttributes == null)
168+
if (_contextAttributes == null)
170169
{
171170
_contextAttributes = new Dictionary<string, object>();
172171
}
@@ -180,7 +179,7 @@ public IDictionary<string, object> ContextAttributes
180179

181180
public class AsyncRequestContext : RequestContext, IAsyncRequestContext
182181
{
183-
public AsyncRequestContext(bool enableMetrics, ISigner clientSigner):
182+
public AsyncRequestContext(bool enableMetrics, ISigner clientSigner) :
184183
base(enableMetrics, clientSigner)
185184
{
186185
}
@@ -191,7 +190,7 @@ public AsyncRequestContext(bool enableMetrics, ISigner clientSigner):
191190

192191
public class ResponseContext : IResponseContext
193192
{
194-
public AmazonWebServiceResponse Response { get; set; }
193+
public AmazonWebServiceResponse Response { get; set; }
195194
public IWebResponseData HttpResponse { get; set; }
196195
}
197196

Diff for: sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/BaseAuthResolverHandler.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected void PreInvoke(IExecutionContext executionContext)
7676
{
7777
// We can use DefaultAWSCredentials if it was set by the user for these schemes.
7878
executionContext.RequestContext.Identity = clientConfig.DefaultAWSCredentials;
79-
return;
79+
break;
8080
}
8181

8282
if (scheme is BearerAuthScheme && clientConfig.AWSTokenProvider != null)
@@ -99,15 +99,15 @@ protected void PreInvoke(IExecutionContext executionContext)
9999
#endif
100100

101101
executionContext.RequestContext.Identity = token;
102-
return;
102+
break;
103103
}
104104

105105
var identityResolver = scheme.GetIdentityResolver(clientConfig.IdentityResolverConfiguration);
106106
executionContext.RequestContext.Identity = identityResolver.ResolveIdentity();
107107

108108
if (executionContext.RequestContext.Identity != null)
109109
{
110-
return;
110+
break;
111111
}
112112
}
113113
catch (Exception ex)

Diff for: sdk/src/Core/Amazon.Runtime/Pipeline/Handlers/Marshaller.cs

+6-38
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ protected static void PreInvoke(IExecutionContext executionContext)
102102
}
103103

104104
SetRecursionDetectionHeader(requestContext.Request.Headers);
105-
SetUserAgentHeader(requestContext);
105+
UpdateUserAgentDetails(requestContext);
106106
}
107107
}
108108

@@ -124,51 +124,19 @@ private static void SetRecursionDetectionHeader(IDictionary<string, string> head
124124
}
125125
}
126126

127-
private static void SetUserAgentHeader(IRequestContext requestContext)
127+
private static void UpdateUserAgentDetails(IRequestContext requestContext)
128128
{
129129
var sb = new StringBuilder(256);
130130

131-
sb.Append(InternalSDKUtils.ReplaceInvalidUserAgentCharacters(requestContext.ClientConfig.UserAgent));
131+
requestContext.UserAgentDetails.AddUserAgentComponent(InternalSDKUtils.ReplaceInvalidUserAgentCharacters(requestContext.ClientConfig.UserAgent));
132132

133133
var clientAppId = requestContext.ClientConfig.ClientAppId;
134134
if (!string.IsNullOrEmpty(clientAppId))
135-
sb.Append(" app/").Append(InternalSDKUtils.ReplaceInvalidUserAgentCharacters(clientAppId));
135+
requestContext.UserAgentDetails.AddUserAgentComponent($"app/{InternalSDKUtils.ReplaceInvalidUserAgentCharacters(clientAppId)}");
136136

137-
sb.Append(" cfg/retry-mode#").Append(ToUserAgentHeaderString(requestContext.ClientConfig.RetryMode));
137+
requestContext.UserAgentDetails.AddUserAgentComponent($"md/{(requestContext.IsAsync ? "ClientAsync" : "ClientSync")}");
138138

139-
sb.Append(" md/").Append(requestContext.IsAsync ? "ClientAsync" : "ClientSync");
140-
141-
sb.Append(" cfg/init-coll#").Append(AWSConfigs.InitializeCollections ? '1' : '0');
142-
143-
var userAgentAddition = requestContext.OriginalRequest.UserAgentAddition;
144-
if (!string.IsNullOrEmpty(userAgentAddition))
145-
{
146-
sb.Append(' ').Append(InternalSDKUtils.ReplaceInvalidUserAgentCharacters(userAgentAddition));
147-
}
148-
149-
var userAgent = sb.ToString();
150-
151-
if (requestContext.ClientConfig.UseAlternateUserAgentHeader)
152-
{
153-
requestContext.Request.Headers[HeaderKeys.XAmzUserAgentHeader] = userAgent;
154-
}
155-
else
156-
{
157-
requestContext.Request.Headers[HeaderKeys.UserAgentHeader] = userAgent;
158-
}
159-
}
160-
161-
private static string ToUserAgentHeaderString(RequestRetryMode requestRetryMode)
162-
{
163-
switch (requestRetryMode)
164-
{
165-
case RequestRetryMode.Standard:
166-
return "standard";
167-
case RequestRetryMode.Adaptive:
168-
return "adaptive";
169-
default:
170-
return requestRetryMode.ToString().ToLowerInvariant();
171-
}
139+
requestContext.UserAgentDetails.AddUserAgentComponent($"cfg/init-coll#{(AWSConfigs.InitializeCollections ? '1' : '0')}");
172140
}
173141
}
174142
}

Diff for: sdk/src/Core/Amazon.Runtime/Pipeline/HttpHandler/HttpHandler.cs

+19
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public override void InvokeSync(IExecutionContext executionContext)
6666
try
6767
{
6868
SetMetrics(executionContext.RequestContext);
69+
SetUserAgentHeader(executionContext.RequestContext);
70+
6971
IRequest wrappedRequest = executionContext.RequestContext.Request;
7072
httpRequest = CreateWebRequest(executionContext.RequestContext);
7173
httpRequest.SetRequestHeaders(wrappedRequest.Headers);
@@ -183,6 +185,8 @@ public override async System.Threading.Tasks.Task<T> InvokeAsync<T>(IExecutionCo
183185
try
184186
{
185187
SetMetrics(executionContext.RequestContext);
188+
SetUserAgentHeader(executionContext.RequestContext);
189+
186190
IRequest wrappedRequest = executionContext.RequestContext.Request;
187191
httpRequest = CreateWebRequest(executionContext.RequestContext);
188192
httpRequest.SetRequestHeaders(wrappedRequest.Headers);
@@ -496,5 +500,20 @@ private static System.IO.Stream GetInputStream(IRequestContext requestContext, S
496500
}
497501
return originalStream;
498502
}
503+
504+
private void SetUserAgentHeader(IRequestContext requestContext)
505+
{
506+
var metricsUserAgent = requestContext.UserAgentDetails.GenerateUserAgentWithMetrics();
507+
Logger.DebugFormat("User-Agent Header: {0}", metricsUserAgent);
508+
509+
if (requestContext.ClientConfig.UseAlternateUserAgentHeader)
510+
{
511+
requestContext.Request.Headers[HeaderKeys.XAmzUserAgentHeader] = metricsUserAgent;
512+
}
513+
else
514+
{
515+
requestContext.Request.Headers[HeaderKeys.UserAgentHeader] = metricsUserAgent;
516+
}
517+
}
499518
}
500519
}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Text;
1+
using Amazon.Runtime.Internal;
2+
using Amazon.Runtime.Internal.UserAgent;
43

54
namespace Amazon.Runtime
65
{
76
public static class PaginatorUtils
87
{
98
public static void SetUserAgentAdditionOnRequest(AmazonWebServiceRequest request)
109
{
11-
request.UserAgentAddition = $" ft/paginator";
10+
((IAmazonWebServiceRequest)request).UserAgentDetails.AddFeature(UserAgentFeatureId.PAGINATOR);
1211
}
1312
}
1413
}

Diff for: sdk/src/Services/Glacier/Custom/Transfer/ArchiveTransferManager.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ internal void UserAgentRequestEventHandlerSync(object sender, RequestEventArgs a
170170
WebServiceRequestEventArgs wsArgs = args as WebServiceRequestEventArgs;
171171
if (wsArgs != null)
172172
{
173-
string currentUserAgent = wsArgs.Headers[AWSSDKUtils.UserAgentHeader];
174-
wsArgs.Headers[AWSSDKUtils.UserAgentHeader] = currentUserAgent + " ft/ArchiveTransferManager md/" + this.operation;
173+
((Runtime.Internal.IAmazonWebServiceRequest)wsArgs.Request).UserAgentDetails.AddUserAgentComponent("ft/ArchiveTransferManager md/" + this.operation);
175174
}
176175
}
177176
}

0 commit comments

Comments
 (0)