diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/AWSLambdaWrapper.cs b/src/OpenTelemetry.Instrumentation.AWSLambda/AWSLambdaWrapper.cs index c2995f5231..9359fc17b5 100644 --- a/src/OpenTelemetry.Instrumentation.AWSLambda/AWSLambdaWrapper.cs +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/AWSLambdaWrapper.cs @@ -17,6 +17,7 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Threading.Tasks; using Amazon.Lambda.Core; @@ -68,10 +69,7 @@ public static TResult Trace( ILambdaContext context, ActivityContext parentContext = default) { - TResult result = default; - Action action = () => result = lambdaHandler(input, context); - TraceInternal(tracerProvider, action, input, context, parentContext); - return result; + return TraceInternal(tracerProvider, lambdaHandler, input, context, parentContext); } /// @@ -95,8 +93,12 @@ public static void Trace( ILambdaContext context, ActivityContext parentContext = default) { - Action action = () => lambdaHandler(input, context); - TraceInternal(tracerProvider, action, input, context, parentContext); + Func func = (input, context) => + { + lambdaHandler(input, context); + return null; + }; + TraceInternal(tracerProvider, func, input, context, parentContext); } /// @@ -121,8 +123,12 @@ public static Task TraceAsync( ILambdaContext context, ActivityContext parentContext = default) { - Func action = async () => await lambdaHandler(input, context); - return TraceInternalAsync(tracerProvider, action, input, context, parentContext); + Func> func = async (input, context) => + { + await lambdaHandler(input, context); + return null; + }; + return TraceInternalAsync(tracerProvider, func, input, context, parentContext); } /// @@ -141,17 +147,14 @@ public static Task TraceAsync( /// unless X-Ray propagation is disabled in the configuration for this wrapper. /// /// Task of result. - public static async Task TraceAsync( + public static Task TraceAsync( TracerProvider tracerProvider, Func> lambdaHandler, TInput input, ILambdaContext context, ActivityContext parentContext = default) { - TResult result = default; - Func action = async () => result = await lambdaHandler(input, context); - await TraceInternalAsync(tracerProvider, action, input, context, parentContext); - return result; + return TraceInternalAsync(tracerProvider, lambdaHandler, input, context, parentContext); } #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters @@ -167,9 +170,12 @@ internal static Activity OnFunctionStart(TInput input, ILambdaContext co } } - var tags = AWSLambdaUtils.GetFunctionTags(input, context); + var functionTags = AWSLambdaUtils.GetFunctionTags(input, context); + var httpTags = AWSLambdaHttpUtils.GetHttpTags(input); + + // We assume that functionTags and httpTags have no intersection. var activityName = AWSLambdaUtils.GetFunctionName(context) ?? "AWS Lambda Invoke"; - var activity = AWSLambdaActivitySource.StartActivity(activityName, ActivityKind.Server, parentContext, tags); + var activity = AWSLambdaActivitySource.StartActivity(activityName, ActivityKind.Server, parentContext, functionTags.Concat(httpTags)); return activity; } @@ -197,51 +203,55 @@ private static void OnException(Activity activity, Exception exception) } } - private static void TraceInternal( + private static TResult TraceInternal( TracerProvider tracerProvider, - Action handler, + Func handler, TInput input, ILambdaContext context, ActivityContext parentContext = default) { - var lambdaActivity = OnFunctionStart(input, context, parentContext); + var activity = OnFunctionStart(input, context, parentContext); try { - handler(); + var result = handler(input, context); + AWSLambdaHttpUtils.SetHttpTagsFromResult(activity, result); + return result; } catch (Exception ex) { - OnException(lambdaActivity, ex); + OnException(activity, ex); throw; } finally { - OnFunctionStop(lambdaActivity, tracerProvider); + OnFunctionStop(activity, tracerProvider); } } - private static async Task TraceInternalAsync( + private static async Task TraceInternalAsync( TracerProvider tracerProvider, - Func handlerAsync, + Func> handlerAsync, TInput input, ILambdaContext context, ActivityContext parentContext = default) { - var lambdaActivity = OnFunctionStart(input, context, parentContext); + var activity = OnFunctionStart(input, context, parentContext); try { - await handlerAsync(); + var result = await handlerAsync(input, context); + AWSLambdaHttpUtils.SetHttpTagsFromResult(activity, result); + return result; } catch (Exception ex) { - OnException(lambdaActivity, ex); + OnException(activity, ex); throw; } finally { - OnFunctionStop(lambdaActivity, tracerProvider); + OnFunctionStop(activity, tracerProvider); } } } diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AWSLambda/CHANGELOG.md index db388af70c..729760a2cf 100644 --- a/src/OpenTelemetry.Instrumentation.AWSLambda/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +* Add HTTP server span attributes for API Gateway triggers + ([#626](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/626)) + ## 1.1.0-beta.2 Release PR: [#590](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/590) diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaHttpUtils.cs b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaHttpUtils.cs new file mode 100644 index 0000000000..25adefc903 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaHttpUtils.cs @@ -0,0 +1,136 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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. +// + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Web; +using Amazon.Lambda.APIGatewayEvents; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.AWSLambda.Implementation +{ + internal class AWSLambdaHttpUtils + { + // x-forwarded-... headers are described here https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html + private const string HeaderXForwardedProto = "x-forwarded-proto"; + private const string HeaderHost = "host"; + + internal static IEnumerable> GetHttpTags(TInput input) + { + var tags = new List>(); + + string httpScheme = null; + string httpTarget = null; + string httpMethod = null; + string hostName = null; + int? hostPort = null; + + switch (input) + { + case APIGatewayProxyRequest request: + httpScheme = AWSLambdaUtils.GetHeaderValues(request, HeaderXForwardedProto)?.LastOrDefault(); + httpTarget = string.Concat(request.RequestContext?.Path ?? string.Empty, GetQueryString(request)); + httpMethod = request.HttpMethod; + var hostHeader = AWSLambdaUtils.GetHeaderValues(request, HeaderHost)?.LastOrDefault(); + (hostName, hostPort) = GetHostAndPort(httpScheme, hostHeader); + break; + case APIGatewayHttpApiV2ProxyRequest requestV2: + httpScheme = AWSLambdaUtils.GetHeaderValues(requestV2, HeaderXForwardedProto)?.LastOrDefault(); + httpTarget = string.Concat(requestV2.RawPath ?? string.Empty, GetQueryString(requestV2)); + httpMethod = requestV2.RequestContext?.Http?.Method; + var hostHeaderV2 = AWSLambdaUtils.GetHeaderValues(requestV2, HeaderHost)?.LastOrDefault(); + (hostName, hostPort) = GetHostAndPort(httpScheme, hostHeaderV2); + break; + } + + tags.AddTagIfNotNull(SemanticConventions.AttributeHttpScheme, httpScheme); + tags.AddTagIfNotNull(SemanticConventions.AttributeHttpTarget, httpTarget); + tags.AddTagIfNotNull(SemanticConventions.AttributeHttpMethod, httpMethod); + tags.AddTagIfNotNull(SemanticConventions.AttributeNetHostName, hostName); + tags.AddTagIfNotNull(SemanticConventions.AttributeNetHostPort, hostPort); + + return tags; + } + + internal static void SetHttpTagsFromResult(Activity activity, object result) + { + switch (result) + { + case APIGatewayProxyResponse response: + activity.SetTag(SemanticConventions.AttributeHttpStatusCode, response.StatusCode); + break; + case APIGatewayHttpApiV2ProxyResponse responseV2: + activity.SetTag(SemanticConventions.AttributeHttpStatusCode, responseV2.StatusCode); + break; + } + } + + internal static string GetQueryString(APIGatewayProxyRequest request) + { + if (request.MultiValueQueryStringParameters == null) + { + return string.Empty; + } + + var queryString = new StringBuilder(); + var separator = '?'; + foreach (var parameterKvp in request.MultiValueQueryStringParameters) + { + // Multiple values for the same parameter will be added to query + // as ampersand separated: name=value1&name=value2 + foreach (var value in parameterKvp.Value) + { + queryString.Append(separator) + .Append(HttpUtility.UrlEncode(parameterKvp.Key)) + .Append("=") + .Append(HttpUtility.UrlEncode(value)); + separator = '&'; + } + } + + return queryString.ToString(); + } + + internal static string GetQueryString(APIGatewayHttpApiV2ProxyRequest request) => + string.IsNullOrEmpty(request.RawQueryString) ? string.Empty : "?" + request.RawQueryString; + + internal static (string Host, int? Port) GetHostAndPort(string httpScheme, string hostHeader) + { + if (hostHeader == null) + { + return (null, null); + } + + var hostAndPort = hostHeader.Split(new char[] { ':' }, 2); + if (hostAndPort.Length > 1) + { + var host = hostAndPort[0]; + return int.TryParse(hostAndPort[1], out var port) + ? (host, port) + : (host, null); + } + else + { + return (hostAndPort[0], GetDefaultPort(httpScheme)); + } + } + + private static int? GetDefaultPort(string httpScheme) => + httpScheme == "https" ? 443 : httpScheme == "http" ? 80 : null; + } +} diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs index c2ae886f0d..75d0ba2a63 100644 --- a/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/AWSLambdaUtils.cs @@ -137,6 +137,30 @@ internal static IEnumerable> GetFunctionTags GetHeaderValues(APIGatewayProxyRequest request, string name) + { + var multiValueHeader = request.MultiValueHeaders?.GetValueByKeyIgnoringCase(name); + if (multiValueHeader != null) + { + return multiValueHeader; + } + + var headerValue = request.Headers?.GetValueByKeyIgnoringCase(name); + + return headerValue != null ? new[] { headerValue } : null; + } + + internal static IEnumerable GetHeaderValues(APIGatewayHttpApiV2ProxyRequest request, string name) + { + var headerValue = GetHeaderValue(request, name); + + // Multiple values for the same header will be separated by a comma. + return headerValue?.Split(','); + } + + private static string GetHeaderValue(APIGatewayHttpApiV2ProxyRequest request, string name) => + request.Headers?.GetValueByKeyIgnoringCase(name); + private static string GetAccountId(string functionArn) { // The fifth item of function arn: https://github.com/open-telemetry/opentelemetry-specification/blob/86aeab1e0a7e6c67be09c7f15ff25063ee6d2b5c/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#all-triggers @@ -167,16 +191,11 @@ private static string GetFaasId(string functionArn) return faasId; } - private static string GetFaasTrigger(TInput input) - { - var trigger = "other"; - if (input is APIGatewayProxyRequest || input is APIGatewayHttpApiV2ProxyRequest) - { - trigger = "http"; - } + private static string GetFaasTrigger(TInput input) => + IsHttpRequest(input) ? "http" : "other"; - return trigger; - } + private static bool IsHttpRequest(TInput input) => + input is APIGatewayProxyRequest || input is APIGatewayHttpApiV2ProxyRequest; private static ActivityContext ParseXRayTraceHeader(string rawHeader) { @@ -190,27 +209,4 @@ private static ActivityContext ParseXRayTraceHeader(string rawHeader) var propagationContext = xrayPropagator.Extract(default, carrier, Getter); return propagationContext.ActivityContext; } - - private static IEnumerable GetHeaderValues(APIGatewayProxyRequest request, string name) - { - if (request.MultiValueHeaders != null && - request.MultiValueHeaders.TryGetValue(name, out var values)) - { - return values; - } - - return null; - } - - private static IEnumerable GetHeaderValues(APIGatewayHttpApiV2ProxyRequest request, string name) - { - if (request.Headers != null && - request.Headers.TryGetValue(name, out var header)) - { - // Multiple values for the same header will be separated by a comma. - return header?.Split(','); - } - - return null; - } } diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/CommonExtensions.cs b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/CommonExtensions.cs new file mode 100644 index 0000000000..d82ebfad5e --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/Implementation/CommonExtensions.cs @@ -0,0 +1,57 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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. +// + +using System; +using System.Collections.Generic; + +namespace OpenTelemetry.Instrumentation.AWSLambda.Implementation +{ + internal static class CommonExtensions + { + internal static void AddTagIfNotNull(this List> tags, string tagName, object tagValue) + { + if (tagValue != null) + { + tags.Add(new(tagName, tagValue)); + } + } + + internal static T GetValueByKeyIgnoringCase(this IDictionary dict, string key) + { + // TODO: there may be opportunities for performance improvements of this method. + + // We had to introduce case-insensitive headers search as can't fully rely on + // AWS documentation stating that expected headers are lower-case. AWS test + // console offers JSON example with camel case header names. + // See X-Forwarded-Proto or X-Forwarded-Port for example. + + if (dict == null) + { + return default; + } + + foreach (var kvp in dict) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + return kvp.Value; + } + } + + return default; + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj b/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj index 239fb3e9f1..4afae25088 100644 --- a/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj +++ b/src/OpenTelemetry.Instrumentation.AWSLambda/OpenTelemetry.Instrumentation.AWSLambda.csproj @@ -17,6 +17,7 @@ + diff --git a/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/Implementation/AWSLambdaHttpUtilsTests.cs b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/Implementation/AWSLambdaHttpUtilsTests.cs new file mode 100644 index 0000000000..a5ecf93424 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/Implementation/AWSLambdaHttpUtilsTests.cs @@ -0,0 +1,269 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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. +// + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Amazon.Lambda.APIGatewayEvents; +using Moq; +using OpenTelemetry.Instrumentation.AWSLambda.Implementation; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AWSLambda.Tests.Implementation +{ + public class AWSLambdaHttpUtilsTests + { + [Fact] + public void GetHttpTags_APIGatewayProxyRequest_ReturnsCorrectTags() + { + var request = new APIGatewayProxyRequest + { + MultiValueHeaders = new Dictionary> + { + { "X-Forwarded-Proto", new List { "https" } }, + { "Host", new List { "localhost:1234" } }, + }, + HttpMethod = "GET", + MultiValueQueryStringParameters = new Dictionary> + { + { "q1", new[] { "value1" } }, + }, + RequestContext = new APIGatewayProxyRequest.ProxyRequestContext + { + Path = "/path/test", + }, + }; + + var actualTags = AWSLambdaHttpUtils.GetHttpTags(request); + + var expectedTags = new Dictionary + { + { "http.scheme", "https" }, + { "http.target", "/path/test?q1=value1" }, + { "net.host.name", "localhost" }, + { "net.host.port", 1234 }, + { "http.method", "GET" }, + }; + + AssertTags(expectedTags, actualTags); + } + + [Fact] + public void GetHttpTags_APIGatewayProxyRequestWithMultiValueHeader_UsesLastValue() + { + var request = new APIGatewayProxyRequest + { + MultiValueHeaders = new Dictionary> + { + { "X-Forwarded-Proto", new List { "https", "http" } }, + { "Host", new List { "localhost:1234", "myhost:432" } }, + }, + }; + + var actualTags = AWSLambdaHttpUtils.GetHttpTags(request); + + var expectedTags = new Dictionary + { + { "http.target", string.Empty }, + { "http.scheme", "http" }, + { "net.host.name", "myhost" }, + { "net.host.port", 432 }, + }; + + AssertTags(expectedTags, actualTags); + } + + [Fact] + public void GetHttpTags_APIGatewayHttpApiV2ProxyRequest_ReturnsCorrectTags() + { + var request = new APIGatewayHttpApiV2ProxyRequest + { + Headers = new Dictionary + { + { "X-Forwarded-Proto", "https" }, + { "Host", "localhost:1234" }, + }, + RawPath = "/path/test", + RawQueryString = "q1=value1", + RequestContext = new APIGatewayHttpApiV2ProxyRequest.ProxyRequestContext + { + Http = new APIGatewayHttpApiV2ProxyRequest.HttpDescription + { + Method = "GET", + }, + }, + }; + + var actualTags = AWSLambdaHttpUtils.GetHttpTags(request); + + var expectedTags = new Dictionary + { + { "http.scheme", "https" }, + { "http.target", "/path/test?q1=value1" }, + { "net.host.name", "localhost" }, + { "net.host.port", 1234 }, + { "http.method", "GET" }, + }; + + AssertTags(expectedTags, actualTags); + } + + [Fact] + public void GetHttpTags_APIGatewayHttpApiV2ProxyRequestWithMultiValueHeader_UsesLastValue() + { + var request = new APIGatewayHttpApiV2ProxyRequest + { + Headers = new Dictionary + { + { "X-Forwarded-Proto", "https,http" }, + { "Host", "localhost:1234,myhost:432" }, + }, + }; + + var actualTags = AWSLambdaHttpUtils.GetHttpTags(request); + + var expectedTags = new Dictionary + { + { "http.target", string.Empty }, + { "http.scheme", "http" }, + { "net.host.name", "myhost" }, + { "net.host.port", 432 }, + }; + + AssertTags(expectedTags, actualTags); + } + + [Fact] + public void SetHttpTagsFromResult_APIGatewayProxyResponse_SetsCorrectTags() + { + var response = new APIGatewayProxyResponse + { + StatusCode = 200, + }; + var activityProcessor = new Mock>(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddSource("TestActivitySource") + .Build(); + + using var testActivitySource = new ActivitySource("TestActivitySource"); + using var activity = testActivitySource.StartActivity("TestActivity"); + + AWSLambdaHttpUtils.SetHttpTagsFromResult(activity, response); + + var expectedTags = new Dictionary + { + { "http.status_code", 200 }, + }; + AssertTags(expectedTags, activity.TagObjects); + } + + [Fact] + public void SetHttpTagsFromResult_APIGatewayHttpApiV2ProxyResponse_SetsCorrectTags() + { + var response = new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = 200, + }; + var activityProcessor = new Mock>(); + + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddSource("TestActivitySource") + .Build(); + + using var testActivitySource = new ActivitySource("TestActivitySource"); + using var activity = testActivitySource.StartActivity("TestActivity"); + + AWSLambdaHttpUtils.SetHttpTagsFromResult(activity, response); + + var expectedTags = new Dictionary + { + { "http.status_code", 200 }, + }; + AssertTags(expectedTags, activity.TagObjects); + } + + [Theory] + [InlineData(null, null, null, null)] + [InlineData("", "", "", null)] + [InlineData(null, "localhost:4321", "localhost", 4321)] + [InlineData(null, "localhost:1a", "localhost", null)] + [InlineData(null, "localhost", "localhost", null)] + [InlineData("http", "localhost", "localhost", 80)] + [InlineData("https", "localhost", "localhost", 443)] + public void GetHostAndPort_HostHeader_ReturnsCorrectHostAndPort(string httpSchema, string hostHeader, string expectedHost, int? expectedPort) + { + (var host, var port) = AWSLambdaHttpUtils.GetHostAndPort(httpSchema, hostHeader); + + Assert.Equal(expectedHost, host); + Assert.Equal(expectedPort, port); + } + + [Theory] + [InlineData(null, "")] + [InlineData(new string[] { }, "")] + [InlineData(new[] { "value1" }, "?name=value1")] + [InlineData(new[] { "value$a" }, "?name=value%24a")] + [InlineData(new[] { "value 1" }, "?name=value+1")] + [InlineData(new[] { "value1", "value2" }, "?name=value1&name=value2")] + public void GetQueryString_APIGatewayProxyRequest_CorrectQueryString(IList values, string expectedQueryString) + { + var request = new APIGatewayProxyRequest(); + if (values != null) + { + request.MultiValueQueryStringParameters = new Dictionary> + { + { "name", values }, + }; + } + + var queryString = AWSLambdaHttpUtils.GetQueryString(request); + + Assert.Equal(expectedQueryString, queryString); + } + + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("name=value1", "?name=value1")] + [InlineData("sdckj9_+", "?sdckj9_+")] + public void GetQueryString_APIGatewayHttpApiV2ProxyRequest_CorrectQueryString(string rawQueryString, string expectedQueryString) + { + var request = new APIGatewayHttpApiV2ProxyRequest + { + RawQueryString = rawQueryString, + }; + + var queryString = AWSLambdaHttpUtils.GetQueryString(request); + + Assert.Equal(expectedQueryString, queryString); + } + + private static void AssertTags(IReadOnlyDictionary expectedTags, IEnumerable> actualTags) + where TActualValue : class + { + Assert.NotNull(actualTags); + Assert.Equal(expectedTags.Count, actualTags.Count()); + foreach (var tag in expectedTags) + { + Assert.Contains(new KeyValuePair(tag.Key, tag.Value as TActualValue), actualTags); + } + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/Implementation/CommonExtensionsTests.cs b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/Implementation/CommonExtensionsTests.cs new file mode 100644 index 0000000000..7cecb57cb5 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AWSLambda.Tests/Implementation/CommonExtensionsTests.cs @@ -0,0 +1,49 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License 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. +// + +using System.Collections.Generic; +using System.Linq; +using OpenTelemetry.Instrumentation.AWSLambda.Implementation; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AWSLambda.Tests.Implementation +{ + public class CommonExtensionsTests + { + [Theory] + [InlineData("test")] + [InlineData(443)] + [InlineData(null)] + public void AddTagIfNotNull_Tag_CorrectTagsList(object tag) + { + var tags = new List>(); + + tags.AddTagIfNotNull("tagName", tag); + + if (tag != null) + { + Assert.Single(tags); + var actualTag = tags.First(); + Assert.Equal("tagName", actualTag.Key); + Assert.Equal(tag, actualTag.Value); + } + else + { + Assert.Empty(tags); + } + } + } +}