From 248818733c0dad13e5e75314cf7baaf84e820595 Mon Sep 17 00:00:00 2001 From: Zachary Richardson Date: Fri, 3 Nov 2023 17:48:47 -0700 Subject: [PATCH] Tests --- .../HttpInMetricsListenerTests.cs | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) diff --git a/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInMetricsListenerTests.cs b/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInMetricsListenerTests.cs index cd458aad73..3d97c033c5 100644 --- a/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInMetricsListenerTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInMetricsListenerTests.cs @@ -15,6 +15,7 @@ // using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Web; using OpenTelemetry.Context.Propagation; @@ -108,4 +109,287 @@ public void HttpDurationMetricIsEmitted() Assert.Equal(200, httpStatusCode); Assert.Equal("http", httpScheme); } + + [Fact] + public void HttpDurationMetricIsEmittedWithEnrichments() + { + // Custom tag that will be added to the http.server.duration metric's TagList + KeyValuePair testTag = new("testTagKey", "testTagValue"); + + string url = "http://localhost/api/value"; + double duration = 0; + HttpContext.Current = new HttpContext( + new HttpRequest(string.Empty, url, string.Empty), + new HttpResponse(new StringWriter())); + + // This is to enable activity creation + // as it is created using ActivitySource inside TelemetryHttpModule + // TODO: This should not be needed once the dependency on activity is removed from metrics + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetInstrumentation(opts => opts.Enrich + = (activity, eventName, rawObject) => + { + if (eventName.Equals("OnStopActivity")) + { + duration = activity.Duration.TotalMilliseconds; + } + }) + .Build(); + + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetInstrumentation(options => + { + options.Enrich = (string metricName, HttpContext httpContext, ref TagList tags) => + { + // If we're emitting an http.server.duration metric... + if (metricName == "http.server.duration") + { + // Enrich it with the given custom tag. + tags.Add(testTag); + } + }; + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + var activity = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); + + meterProvider.ForceFlush(); + + var metricPoints = new List(); + foreach (var p in exportedItems[0].GetMetricPoints()) + { + metricPoints.Add(p); + } + + Assert.Single(metricPoints); + + var metricPoint = metricPoints[0]; + + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); + + Assert.Equal(MetricType.Histogram, exportedItems[0].MetricType); + Assert.Equal("http.server.duration", exportedItems[0].Name); + Assert.Equal(1L, count); + Assert.Equal(duration, sum); + + Assert.Equal(4, metricPoints[0].Tags.Count); + string? httpMethod = null; + int httpStatusCode = 0; + string? httpScheme = null; + string? testTagVal = null; + + foreach (var tag in metricPoints[0].Tags) + { + if (tag.Key == SemanticConventions.AttributeHttpMethod) + { + httpMethod = (string)tag.Value; + continue; + } + + if (tag.Key == SemanticConventions.AttributeHttpStatusCode) + { + httpStatusCode = (int)tag.Value; + continue; + } + + if (tag.Key == SemanticConventions.AttributeHttpScheme) + { + httpScheme = (string)tag.Value; + continue; + } + + if (tag.Key == testTag.Key) + { + testTagVal = (string)tag.Value; + continue; + } + } + + Assert.Equal("GET", httpMethod); + Assert.Equal(200, httpStatusCode); + Assert.Equal("http", httpScheme); + Assert.Equal(testTag.Value, testTagVal); + } + + [Fact] + public void HttpDurationMetricIsFiltered() + { + // Make two "requests" and filter the http.server.duration to only collect + // metrics for one of the given URLs. + string url1 = "http://localhost/api/filterout"; + string url2 = "http://localhost/api/collect"; + double duration = 0; + HttpContext.Current = new HttpContext( + new HttpRequest(string.Empty, url1, string.Empty), + new HttpResponse(new StringWriter())); + + // This is to enable activity creation + // as it is created using ActivitySource inside TelemetryHttpModule + // TODO: This should not be needed once the dependency on activity is removed from metrics + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetInstrumentation(opts => opts.Enrich + = (activity, eventName, rawObject) => + { + if (eventName.Equals("OnStopActivity")) + { + duration = activity.Duration.TotalMilliseconds; + } + }) + .Build(); + + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetInstrumentation(options => + { + options.Filter = (string metricName, HttpContext httpContext) => + { + // If the request hits a given URL, we shouldn't emit a metric. + return httpContext.Request.Url.AbsoluteUri != url1; + }; + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + var activity1 = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity1, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); + + HttpContext.Current = new HttpContext( + new HttpRequest(string.Empty, url2, string.Empty), + new HttpResponse(new StringWriter())); + + var activity2 = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity2, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); + + meterProvider.ForceFlush(); + + // We should've only collected one metric + Assert.Single(exportedItems); + } + + [Fact] + public void HttpDurationMetricIsEmittedWithEnrichmentsAndFiltered() + { + // Custom tag that will be added to the http.server.duration metric's TagList + KeyValuePair testTag = new("testTagKey", "testTagValue"); + + // Make two "requests" and filter the http.server.duration to only collect + // metrics for one of the given URLs. + string url1 = "http://localhost/api/filterout"; + string url2 = "http://localhost/api/collect"; + double duration = 0; + HttpContext.Current = new HttpContext( + new HttpRequest(string.Empty, url1, string.Empty), + new HttpResponse(new StringWriter())); + + // This is to enable activity creation + // as it is created using ActivitySource inside TelemetryHttpModule + // TODO: This should not be needed once the dependency on activity is removed from metrics + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetInstrumentation(opts => opts.Enrich + = (activity, eventName, rawObject) => + { + if (eventName.Equals("OnStopActivity")) + { + duration = activity.Duration.TotalMilliseconds; + } + }) + .Build(); + + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddAspNetInstrumentation(options => + { + options.Enrich = (string metricName, HttpContext httpContext, ref TagList tags) => + { + // If we're emitting an http.server.duration metric... + if (metricName == "http.server.duration") + { + // Enrich it with the given custom tag. + tags.Add(testTag); + } + }; + options.Filter = (string metricName, HttpContext httpContext) => + { + // If the request hits a given URL, we shouldn't emit a metric. + return httpContext.Request.Url.AbsoluteUri != url1; + }; + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + var activity1 = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity1, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); + + HttpContext.Current = new HttpContext( + new HttpRequest(string.Empty, url2, string.Empty), + new HttpResponse(new StringWriter())); + + var activity2 = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity2, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); + + meterProvider.ForceFlush(); + + // We should've only collected one metric + Assert.Single(exportedItems); + + var metricPoints = new List(); + foreach (var p in exportedItems[0].GetMetricPoints()) + { + metricPoints.Add(p); + } + + Assert.Single(metricPoints); + + var metricPoint = metricPoints[0]; + + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); + + Assert.Equal(MetricType.Histogram, exportedItems[0].MetricType); + Assert.Equal("http.server.duration", exportedItems[0].Name); + Assert.Equal(1L, count); + Assert.Equal(duration, sum); + + Assert.Equal(4, metricPoints[0].Tags.Count); + string? httpMethod = null; + int httpStatusCode = 0; + string? httpScheme = null; + string? testTagVal = null; + + foreach (var tag in metricPoints[0].Tags) + { + if (tag.Key == SemanticConventions.AttributeHttpMethod) + { + httpMethod = (string)tag.Value; + continue; + } + + if (tag.Key == SemanticConventions.AttributeHttpStatusCode) + { + httpStatusCode = (int)tag.Value; + continue; + } + + if (tag.Key == SemanticConventions.AttributeHttpScheme) + { + httpScheme = (string)tag.Value; + continue; + } + + if (tag.Key == testTag.Key) + { + testTagVal = (string)tag.Value; + continue; + } + } + + Assert.Equal("GET", httpMethod); + Assert.Equal(200, httpStatusCode); + Assert.Equal("http", httpScheme); + Assert.Equal(testTag.Value, testTagVal); + } }