From 607820978fbe21c19e5b0a638cb1562efaeaab81 Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 16 Apr 2026 08:42:23 +0100 Subject: [PATCH 1/3] [Resources.AWS] Limit response body size read - Limit the length of the HTTP response body that is read from the AWS EC2/ECS/EKS instance metadata service. - Simplify stream reading. - Remove redundant sync-over-async calls. --- .../AWSEC2Detector.cs | 20 +--- .../AWSECSDetector.cs | 17 ++- .../AWSEKSDetector.cs | 26 ++--- .../AsyncHelper.cs | 44 ------- src/OpenTelemetry.Resources.AWS/CHANGELOG.md | 3 + .../OpenTelemetry.Resources.AWS.csproj | 1 + .../ResourceDetectorUtils.cs | 77 ++++++------- .../AsyncHelperTests.cs | 107 ------------------ 8 files changed, 69 insertions(+), 226 deletions(-) delete mode 100644 src/OpenTelemetry.Resources.AWS/AsyncHelper.cs delete mode 100644 test/OpenTelemetry.Resources.AWS.Tests/AsyncHelperTests.cs diff --git a/src/OpenTelemetry.Resources.AWS/AWSEC2Detector.cs b/src/OpenTelemetry.Resources.AWS/AWSEC2Detector.cs index 01be7461e3..891fca2147 100644 --- a/src/OpenTelemetry.Resources.AWS/AWSEC2Detector.cs +++ b/src/OpenTelemetry.Resources.AWS/AWSEC2Detector.cs @@ -51,14 +51,12 @@ public Resource Detect() return Resource.Empty; } - internal static AWSEC2IdentityDocumentModel? DeserializeResponse(string response) - { + internal static AWSEC2IdentityDocumentModel? DeserializeResponse(string response) => #if NETFRAMEWORK - return ResourceDetectorUtils.DeserializeFromString(response); + ResourceDetectorUtils.DeserializeFromString(response); #else - return ResourceDetectorUtils.DeserializeFromString(response, SourceGenerationContext.Default.AWSEC2IdentityDocumentModel); + ResourceDetectorUtils.DeserializeFromString(response, SourceGenerationContext.Default.AWSEC2IdentityDocumentModel); #endif - } internal List> ExtractResourceAttributes(AWSEC2IdentityDocumentModel? identity, string hostName) { @@ -79,9 +77,7 @@ internal List> ExtractResourceAttributes(AWSEC2Iden } private static string GetAWSEC2Token() - { - return AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync(AWSEC2MetadataTokenUrl, HttpMethod.Put, new KeyValuePair(AWSEC2MetadataTokenTTLHeader, "60"))); - } + => ResourceDetectorUtils.SendOutRequest(AWSEC2MetadataTokenUrl, HttpMethod.Put, new KeyValuePair(AWSEC2MetadataTokenTTLHeader, "60")); private static AWSEC2IdentityDocumentModel? GetAWSEC2Identity(string token) { @@ -92,12 +88,8 @@ private static string GetAWSEC2Token() } private static string GetIdentityResponse(string token) - { - return AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync(AWSEC2IdentityDocumentUrl, HttpMethod.Get, new KeyValuePair(AWSEC2MetadataTokenHeader, token))); - } + => ResourceDetectorUtils.SendOutRequest(AWSEC2IdentityDocumentUrl, HttpMethod.Get, new KeyValuePair(AWSEC2MetadataTokenHeader, token)); private static string GetAWSEC2HostName(string token) - { - return AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync(AWSEC2HostNameUrl, HttpMethod.Get, new KeyValuePair(AWSEC2MetadataTokenHeader, token))); - } + => ResourceDetectorUtils.SendOutRequest(AWSEC2HostNameUrl, HttpMethod.Get, new KeyValuePair(AWSEC2MetadataTokenHeader, token)); } diff --git a/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs b/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs index fa989e1766..37e70ac84b 100644 --- a/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs +++ b/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs @@ -71,13 +71,14 @@ public Resource Detect() using (var streamReader = ResourceDetectorUtils.GetStreamReader(path)) { - while (!streamReader.EndOfStream) + string? line = null; + + while ((line = streamReader.ReadLine()) is not null) { - var trimmedLine = streamReader.ReadLine()?.Trim(); - if (trimmedLine?.Length > 64) + var trimmedLine = line.Trim(); + if (trimmedLine.Length > 64) { - containerId = trimmedLine.Substring(trimmedLine.Length - 64); - return containerId; + return trimmedLine.Substring(trimmedLine.Length - 64); } } } @@ -100,10 +101,8 @@ internal void ExtractMetadataV4ResourceAttributes(AWSSemanticConventions.Attribu using var scope = SuppressInstrumentationScope.Begin(); using var httpClientHandler = new HttpClientHandler(); -#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks - var metadataV4ContainerResponse = AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync(metadataV4Url, HttpMethod.Get, null, httpClientHandler)); - var metadataV4TaskResponse = AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync($"{metadataV4Url.TrimEnd('/')}/task", HttpMethod.Get, null, httpClientHandler)); -#pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks + var metadataV4ContainerResponse = ResourceDetectorUtils.SendOutRequest(metadataV4Url, HttpMethod.Get, null, httpClientHandler); + var metadataV4TaskResponse = ResourceDetectorUtils.SendOutRequest($"{metadataV4Url.TrimEnd('/')}/task", HttpMethod.Get, null, httpClientHandler); using var containerResponse = JsonDocument.Parse(metadataV4ContainerResponse); using var taskResponse = JsonDocument.Parse(metadataV4TaskResponse); diff --git a/src/OpenTelemetry.Resources.AWS/AWSEKSDetector.cs b/src/OpenTelemetry.Resources.AWS/AWSEKSDetector.cs index 3dcc475234..2794ebb2db 100644 --- a/src/OpenTelemetry.Resources.AWS/AWSEKSDetector.cs +++ b/src/OpenTelemetry.Resources.AWS/AWSEKSDetector.cs @@ -50,10 +50,7 @@ public Resource Detect() using (var streamReader = ResourceDetectorUtils.GetStreamReader(path)) { - while (!streamReader.EndOfStream) - { - stringBuilder.Append(streamReader.ReadLine()?.Trim()); - } + stringBuilder.Append(streamReader.ReadToEnd().Trim()); } return stringBuilder.ToString(); @@ -71,10 +68,13 @@ public Resource Detect() try { using var streamReader = ResourceDetectorUtils.GetStreamReader(path); - while (!streamReader.EndOfStream) + + string? line = null; + + while ((line = streamReader.ReadLine()) is not null) { - var trimmedLine = streamReader.ReadLine()?.Trim(); - if (trimmedLine?.Length > 64) + var trimmedLine = line.Trim(); + if (trimmedLine.Length > 64) { return trimmedLine.Substring(trimmedLine.Length - 64); } @@ -88,14 +88,12 @@ public Resource Detect() return null; } - internal static AWSEKSClusterInformationModel? DeserializeResponse(string response) - { + internal static AWSEKSClusterInformationModel? DeserializeResponse(string response) => #if NET - return ResourceDetectorUtils.DeserializeFromString(response, SourceGenerationContext.Default.AWSEKSClusterInformationModel); + ResourceDetectorUtils.DeserializeFromString(response, SourceGenerationContext.Default.AWSEKSClusterInformationModel); #else - return ResourceDetectorUtils.DeserializeFromString(response); + ResourceDetectorUtils.DeserializeFromString(response); #endif - } internal List> ExtractResourceAttributes(string? clusterName, string? containerId) { @@ -132,7 +130,7 @@ private static bool IsEKSProcess(string credentials, HttpClientHandler? httpClie try { using var scope = SuppressInstrumentationScope.Begin(); - awsAuth = AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync(AWSAuthUrl, HttpMethod.Get, new KeyValuePair("Authorization", credentials), httpClientHandler)); + awsAuth = ResourceDetectorUtils.SendOutRequest(AWSAuthUrl, HttpMethod.Get, new KeyValuePair("Authorization", credentials), httpClientHandler); } catch (Exception ex) { @@ -145,7 +143,7 @@ private static bool IsEKSProcess(string credentials, HttpClientHandler? httpClie private static string GetEKSClusterInfo(string credentials, HttpClientHandler? httpClientHandler) { using var scope = SuppressInstrumentationScope.Begin(); - return AsyncHelper.RunSync(() => ResourceDetectorUtils.SendOutRequestAsync(AWSClusterInfoUrl, HttpMethod.Get, new KeyValuePair("Authorization", credentials), httpClientHandler)); + return ResourceDetectorUtils.SendOutRequest(AWSClusterInfoUrl, HttpMethod.Get, new KeyValuePair("Authorization", credentials), httpClientHandler); } } #endif diff --git a/src/OpenTelemetry.Resources.AWS/AsyncHelper.cs b/src/OpenTelemetry.Resources.AWS/AsyncHelper.cs deleted file mode 100644 index a75d5579c9..0000000000 --- a/src/OpenTelemetry.Resources.AWS/AsyncHelper.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.Resources.AWS; - -/// -/// A helper class for running asynchronous methods synchronously. -/// -internal static class AsyncHelper -{ - private static readonly TaskFactory TaskFactory = new( - CancellationToken.None, - TaskCreationOptions.None, - TaskContinuationOptions.None, - TaskScheduler.Default); - - /// - /// Executes an async task which has a void return value synchronously. - /// - /// The async task to execute. - public static void RunSync(Func task) - { - TaskFactory - .StartNew(() => task(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) - .Unwrap() - .GetAwaiter() - .GetResult(); - } - - /// - /// Executes an async task which has a TResult return value synchronously. - /// - /// The return type of the task. - /// The async task to execute. - /// The result of the executed task. - public static TResult RunSync(Func> task) - { - return TaskFactory - .StartNew(() => task(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) - .Unwrap() - .GetAwaiter() - .GetResult(); - } -} diff --git a/src/OpenTelemetry.Resources.AWS/CHANGELOG.md b/src/OpenTelemetry.Resources.AWS/CHANGELOG.md index 90536267f1..749cde01c5 100644 --- a/src/OpenTelemetry.Resources.AWS/CHANGELOG.md +++ b/src/OpenTelemetry.Resources.AWS/CHANGELOG.md @@ -9,6 +9,9 @@ Windows containers running on AWS ECS. ([#4028](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4028)) +* Limit how much of the response body is consumed from metadata service HTTP responses. + ([#4122](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4122)) + ## 1.15.0 Released 2026-Jan-21 diff --git a/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj b/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj index b623e5582a..c8e3516923 100644 --- a/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj +++ b/src/OpenTelemetry.Resources.AWS/OpenTelemetry.Resources.AWS.csproj @@ -33,6 +33,7 @@ + diff --git a/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs b/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs index b870fd499d..4c98840abb 100644 --- a/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs +++ b/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs @@ -21,69 +21,70 @@ internal static class ResourceDetectorUtils private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web); #endif - internal static async Task SendOutRequestAsync( + internal static string SendOutRequest( string url, HttpMethod method, KeyValuePair? header, - HttpClientHandler? handler = null) + HttpClientHandler? handler = null, + CancellationToken cancellationToken = default) { - using (var httpRequestMessage = new HttpRequestMessage()) + using var httpRequestMessage = new HttpRequestMessage(); + + httpRequestMessage.RequestUri = new Uri(url); + httpRequestMessage.Method = method; + + if (header is { } headerValue) { - httpRequestMessage.RequestUri = new Uri(url); - httpRequestMessage.Method = method; - if (header.HasValue) - { - httpRequestMessage.Headers.Add(header.Value.Key, header.Value.Value); - } + httpRequestMessage.Headers.Add(headerValue.Key, headerValue.Value); + } #pragma warning disable CA2000 // Dispose objects before losing scope - var httpClient = handler == null ? new HttpClient() : new HttpClient(handler); + var httpClient = handler == null ? new HttpClient() : new HttpClient(handler); #pragma warning restore CA2000 // Dispose objects before losing scope - using (var response = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false)) - { - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync().ConfigureAwait(false); - } - } + +#if NET + using var response = httpClient.Send(httpRequestMessage, cancellationToken); +#else +#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks + using var response = httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); +#pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks +#endif + + response.EnsureSuccessStatusCode(); + + return HttpClientHelpers.GetResponseBodyAsString(response, cancellationToken) ?? string.Empty; } #if NETFRAMEWORK internal static T? DeserializeFromFile(string filePath) { - using (var stream = GetStream(filePath)) - { - return JsonSerializer.Deserialize(stream, JsonSerializerOptions); - } + using var stream = GetStream(filePath); + return JsonSerializer.Deserialize(stream, JsonSerializerOptions); } - internal static T? DeserializeFromString(string json) - { - return JsonSerializer.Deserialize(json, JsonSerializerOptions); - } + internal static T? DeserializeFromString(string json) => + JsonSerializer.Deserialize(json, JsonSerializerOptions); #else internal static T? DeserializeFromFile(string filePath, JsonTypeInfo jsonTypeInfo) { - using (var stream = GetStream(filePath)) - { - return JsonSerializer.Deserialize(stream, jsonTypeInfo); - } + using var stream = GetStream(filePath); + return JsonSerializer.Deserialize(stream, jsonTypeInfo); } - internal static T? DeserializeFromString(string json, JsonTypeInfo jsonTypeInfo) - { - return JsonSerializer.Deserialize(json, jsonTypeInfo); - } + internal static T? DeserializeFromString(string json, JsonTypeInfo jsonTypeInfo) => + JsonSerializer.Deserialize(json, jsonTypeInfo); #endif - internal static Stream GetStream(string filePath) - { - return new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - } + internal static Stream GetStream(string filePath) => + new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); internal static StreamReader GetStreamReader(string filePath) { var fileStream = GetStream(filePath); - var streamReader = new StreamReader(fileStream, Encoding.UTF8); - return streamReader; +#if NET + return new StreamReader(fileStream, Encoding.UTF8, leaveOpen: false); +#else + return new StreamReader(fileStream, Encoding.UTF8); +#endif } } diff --git a/test/OpenTelemetry.Resources.AWS.Tests/AsyncHelperTests.cs b/test/OpenTelemetry.Resources.AWS.Tests/AsyncHelperTests.cs deleted file mode 100644 index fe0efe2a81..0000000000 --- a/test/OpenTelemetry.Resources.AWS.Tests/AsyncHelperTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using Xunit; - -namespace OpenTelemetry.Resources.AWS.Tests; - -public class AsyncHelperTests -{ - [Fact] - public void RunSyncTaskCompletesSuccessfully() - { - // Arrange - static async Task Task() - { - await System.Threading.Tasks.Task.Delay(100); - } - - // Act - var exception = Record.Exception(() => AsyncHelper.RunSync(Task)); - - // Assert - Assert.Null(exception); - } - - [Fact] - public void RunSyncTaskThrowsException() - { - // Arrange - static async Task Task() - { - await System.Threading.Tasks.Task.Delay(100); - throw new InvalidOperationException("Test exception"); - } - - // Act & Assert - var exception = Assert.Throws(() => AsyncHelper.RunSync(Task)); - Assert.Equal("Test exception", exception.Message); - } - - [Fact] - public void RunSyncTaskCancellationThrowsTaskCanceledException() - { - // Arrange - var cts = new CancellationTokenSource(); - cts.Cancel(); - - async Task Task() - { - await System.Threading.Tasks.Task.Delay(100, cts.Token); - } - - // Act & Assert - var exception = Assert.Throws(() => AsyncHelper.RunSync(Task)); - Assert.Equal(cts.Token, exception.CancellationToken); - } - - [Fact] - public void RunSyncTaskWithResultCompletesSuccessfully() - { - // Arrange - static async Task Task() - { - await System.Threading.Tasks.Task.Delay(100); - return "Completed"; - } - - // Act - var result = AsyncHelper.RunSync(Task); - - // Assert - Assert.Equal("Completed", result); - } - - [Fact] - public void RunSyncTaskWithResultThrowsException() - { - // Arrange - static async Task Task() - { - await System.Threading.Tasks.Task.Delay(100); - throw new InvalidOperationException("Test exception"); - } - - // Act & Assert - var exception = Assert.Throws(() => AsyncHelper.RunSync(Task)); - Assert.Equal("Test exception", exception.Message); - } - - [Fact] - public void RunSyncTaskWithResultCancellationThrowsTaskCanceledException() - { - // Arrange - var cts = new CancellationTokenSource(); - cts.Cancel(); - - async Task Task() - { - await System.Threading.Tasks.Task.Delay(100, cts.Token); - return "Completed"; - } - - // Act & Assert - var exception = Assert.Throws(() => AsyncHelper.RunSync(Task)); - Assert.Equal(cts.Token, exception.CancellationToken); - } -} From e5d489ec7099640cd8b84e47b844d6b81236e50b Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 16 Apr 2026 09:49:41 +0100 Subject: [PATCH 2/3] [Resources.AWS] Address feedback - Use `HttpCompletionOption.ResponseHeadersRead`. - Add test. --- .../ResourceDetectorUtils.cs | 4 +- .../AWSECSDetectorTests.cs | 160 +++++++++++------- 2 files changed, 102 insertions(+), 62 deletions(-) diff --git a/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs b/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs index 4c98840abb..cb6ffc744b 100644 --- a/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs +++ b/src/OpenTelemetry.Resources.AWS/ResourceDetectorUtils.cs @@ -43,10 +43,10 @@ internal static string SendOutRequest( #pragma warning restore CA2000 // Dispose objects before losing scope #if NET - using var response = httpClient.Send(httpRequestMessage, cancellationToken); + using var response = httpClient.Send(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); #else #pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks - using var response = httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); + using var response = httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); #pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks #endif diff --git a/test/OpenTelemetry.Resources.AWS.Tests/AWSECSDetectorTests.cs b/test/OpenTelemetry.Resources.AWS.Tests/AWSECSDetectorTests.cs index 7d6f4da5a2..47a40a63aa 100644 --- a/test/OpenTelemetry.Resources.AWS.Tests/AWSECSDetectorTests.cs +++ b/test/OpenTelemetry.Resources.AWS.Tests/AWSECSDetectorTests.cs @@ -60,38 +60,39 @@ public async Task TestEcsMetadataV4Ec2() var source = new CancellationTokenSource(); var token = source.Token; - await using (var metadataEndpoint = new MockEcsMetadataEndpoint("ecs_metadata/metadatav4-response-container-ec2.json", "ecs_metadata/metadatav4-response-task-ec2.json")) + await using var metadataEndpoint = new MockEcsMetadataEndpoint( + "ecs_metadata/metadatav4-response-container-ec2.json", + "ecs_metadata/metadatav4-response-task-ec2.json"); + + using (EnvironmentVariableScope.Create(AWSECSMetadataURLV4Key, metadataEndpoint.Address.ToString())) { - using (EnvironmentVariableScope.Create(AWSECSMetadataURLV4Key, metadataEndpoint.Address.ToString())) - { - var ecsResourceDetector = new AWSECSDetector( - new OpenTelemetry.AWS.AWSSemanticConventions( - SemanticConventionVersion.Latest)); - - var resourceAttributes = ecsResourceDetector.Detect().Attributes.ToDictionary(x => x.Key, x => x.Value); - - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudProvider], "aws"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudPlatform], "aws_ecs"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAccountID], "111122223333"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAvailabilityZone], "us-west-2d"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudRegion], "us-west-2"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudResourceId], "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsContainerArn], "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsLaunchtype], "ec2"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskArn], "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskFamily], "curltest"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskRevision], "26"); + var ecsResourceDetector = new AWSECSDetector( + new OpenTelemetry.AWS.AWSSemanticConventions( + SemanticConventionVersion.Latest)); + + var resourceAttributes = ecsResourceDetector.Detect().Attributes.ToDictionary(x => x.Key, x => x.Value); + + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudProvider], "aws"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudPlatform], "aws_ecs"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAccountID], "111122223333"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAvailabilityZone], "us-west-2d"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudRegion], "us-west-2"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudResourceId], "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsContainerArn], "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsLaunchtype], "ec2"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskArn], "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskFamily], "curltest"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskRevision], "26"); #pragma warning disable CA1861 // Avoid constant arrays as arguments - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupNames], new string[] { "/ecs/metadata" }); - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata" }); - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamNames], new string[] { "ecs/curl/8f03e41243824aea923aca126495f665" }); - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:log-stream:ecs/curl/8f03e41243824aea923aca126495f665" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupNames], new string[] { "/ecs/metadata" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamNames], new string[] { "ecs/curl/8f03e41243824aea923aca126495f665" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:log-stream:ecs/curl/8f03e41243824aea923aca126495f665" }); #pragma warning restore CA1861 // Avoid constant arrays as arguments - if (OperatingSystem.IsWindows()) - { - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeContainerId], "ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66"); - } + if (OperatingSystem.IsWindows()) + { + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeContainerId], "ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66"); } } } @@ -102,47 +103,80 @@ public async Task TestEcsMetadataV4Fargate() var source = new CancellationTokenSource(); var token = source.Token; - await using (var metadataEndpoint = new MockEcsMetadataEndpoint("ecs_metadata/metadatav4-response-container-fargate.json", "ecs_metadata/metadatav4-response-task-fargate.json")) + await using var metadataEndpoint = new MockEcsMetadataEndpoint( + "ecs_metadata/metadatav4-response-container-fargate.json", + "ecs_metadata/metadatav4-response-task-fargate.json"); + + using (EnvironmentVariableScope.Create(AWSECSMetadataURLV4Key, metadataEndpoint.Address.ToString())) { - using (EnvironmentVariableScope.Create(AWSECSMetadataURLV4Key, metadataEndpoint.Address.ToString())) - { - var ecsResourceDetector = new AWSECSDetector( - new OpenTelemetry.AWS.AWSSemanticConventions( - SemanticConventionVersion.Latest)); - - var resourceAttributes = ecsResourceDetector.Detect().Attributes.ToDictionary(x => x.Key, x => x.Value); - - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudProvider], "aws"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudPlatform], "aws_ecs"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAccountID], "111122223333"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAvailabilityZone], "us-west-2a"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudRegion], "us-west-2"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudResourceId], "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsContainerArn], "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsLaunchtype], "fargate"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskArn], "arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskFamily], "curltest"); - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskRevision], "3"); + var ecsResourceDetector = new AWSECSDetector( + new OpenTelemetry.AWS.AWSSemanticConventions( + SemanticConventionVersion.Latest)); + + var resourceAttributes = ecsResourceDetector.Detect().Attributes.ToDictionary(x => x.Key, x => x.Value); + + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudProvider], "aws"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudPlatform], "aws_ecs"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAccountID], "111122223333"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudAvailabilityZone], "us-west-2a"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudRegion], "us-west-2"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudResourceId], "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsContainerArn], "arn:aws:ecs:us-west-2:111122223333:container/05966557-f16c-49cb-9352-24b3a0dcd0e1"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsLaunchtype], "fargate"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskArn], "arn:aws:ecs:us-west-2:111122223333:task/default/e9028f8d5d8e4f258373e7b93ce9a3c3"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskFamily], "curltest"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeEcsTaskRevision], "3"); #pragma warning disable CA1861 // Avoid constant arrays as arguments - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupNames], new string[] { "/ecs/containerlogs" }); - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs" }); - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamNames], new string[] { "ecs/curl/cd189a933e5849daa93386466019ab50" }); - Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:log-stream:ecs/curl/cd189a933e5849daa93386466019ab50" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupNames], new string[] { "/ecs/containerlogs" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogGroupArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamNames], new string[] { "ecs/curl/cd189a933e5849daa93386466019ab50" }); + Assert.NotStrictEqual(resourceAttributes[ExpectedSemanticConventions.AttributeLogStreamArns], new string[] { "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/containerlogs:log-stream:ecs/curl/cd189a933e5849daa93386466019ab50" }); #pragma warning restore CA1861 // Avoid constant arrays as arguments - if (OperatingSystem.IsWindows()) - { - Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeContainerId], "cd189a933e5849daa93386466019ab50-2495160603"); - } + if (OperatingSystem.IsWindows()) + { + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeContainerId], "cd189a933e5849daa93386466019ab50-2495160603"); } } } + [Fact] + public async Task TestResponseBodyIsBeyondSizeLimit() + { + var source = new CancellationTokenSource(); + var token = source.Token; + + await using var metadataEndpoint = new MockEcsMetadataEndpoint( + "ecs_metadata/metadatav4-response-container-fargate.json", + "ecs_metadata/metadatav4-response-task-fargate.json"); + + const int ResponseSize = 10 * 1024 * 1024; // 10 MB + + metadataEndpoint.SetContainerJson(new string('a', ResponseSize)); + metadataEndpoint.SetTaskJson(new string('b', ResponseSize)); + + using (EnvironmentVariableScope.Create(AWSECSMetadataURLV4Key, metadataEndpoint.Address.ToString())) + { + var ecsResourceDetector = new AWSECSDetector( + new OpenTelemetry.AWS.AWSSemanticConventions( + SemanticConventionVersion.Latest)); + + var resourceAttributes = ecsResourceDetector.Detect().Attributes.ToDictionary(x => x.Key, x => x.Value); + + Assert.Equal(2, resourceAttributes.Count); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudProvider], "aws"); + Assert.Equal(resourceAttributes[ExpectedSemanticConventions.AttributeCloudPlatform], "aws_ecs"); + } + } + internal class MockEcsMetadataEndpoint : IAsyncDisposable { public readonly Uri Address; private readonly IHost host; + private string? containerJson; + private string? taskJson; + public MockEcsMetadataEndpoint(string containerJsonPath, string taskJsonPath) { this.host = new HostBuilder() @@ -155,15 +189,17 @@ public MockEcsMetadataEndpoint(string containerJsonPath, string taskJsonPath) { if (context.Request.Method == HttpMethods.Get && context.Request.Path == "/") { - var content = await File.ReadAllTextAsync($"{Environment.CurrentDirectory}/{containerJsonPath}"); - var data = Encoding.UTF8.GetBytes(content); + this.containerJson ??= await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, containerJsonPath)); + + var data = Encoding.UTF8.GetBytes(this.containerJson); context.Response.ContentType = "application/json"; await context.Response.Body.WriteAsync(data); } else if (context.Request.Method == HttpMethods.Get && context.Request.Path == "/task") { - var content = await File.ReadAllTextAsync($"{Environment.CurrentDirectory}/{taskJsonPath}"); - var data = Encoding.UTF8.GetBytes(content); + this.taskJson ??= await File.ReadAllTextAsync(Path.Combine(Environment.CurrentDirectory, taskJsonPath)); + + var data = Encoding.UTF8.GetBytes(this.taskJson); context.Response.ContentType = "application/json"; await context.Response.Body.WriteAsync(data); } @@ -180,6 +216,10 @@ public MockEcsMetadataEndpoint(string containerJsonPath, string taskJsonPath) this.Address = new Uri(server.Features.Get()!.Addresses.First()); } + public void SetContainerJson(string containerJson) => this.containerJson = containerJson; + + public void SetTaskJson(string taskJson) => this.taskJson = taskJson; + public async ValueTask DisposeAsync() => await this.DisposeAsyncCore(); From 647273512421165346e8932593411917afa77404 Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 16 Apr 2026 09:54:08 +0100 Subject: [PATCH 3/3] [Resources.AWS] Address feedback Remove redundant variable. --- .../AWSECSDetector.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs b/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs index 37e70ac84b..ab424d7b41 100644 --- a/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs +++ b/src/OpenTelemetry.Resources.AWS/AWSECSDetector.cs @@ -67,23 +67,19 @@ public Resource Detect() internal static string? GetECSContainerId(string path) { - string? containerId = null; + using var streamReader = ResourceDetectorUtils.GetStreamReader(path); + string? line = null; - using (var streamReader = ResourceDetectorUtils.GetStreamReader(path)) + while ((line = streamReader.ReadLine()) is not null) { - string? line = null; - - while ((line = streamReader.ReadLine()) is not null) + var trimmedLine = line.Trim(); + if (trimmedLine.Length > 64) { - var trimmedLine = line.Trim(); - if (trimmedLine.Length > 64) - { - return trimmedLine.Substring(trimmedLine.Length - 64); - } + return trimmedLine.Substring(trimmedLine.Length - 64); } } - return containerId; + return null; } internal static bool IsECSProcess() =>