diff --git a/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt index 35b5f87638f..a9ba7f916db 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt @@ -6,5 +6,7 @@ OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.Filter.get -> OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.Filter.set -> void OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNet/AspNetMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNet/AspNetMetrics.cs new file mode 100644 index 00000000000..373fe8a5534 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNet/AspNetMetrics.cs @@ -0,0 +1,53 @@ +// +// 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.Diagnostics.Metrics; +using System.Reflection; +using OpenTelemetry.Instrumentation.AspNet.Implementation; + +namespace OpenTelemetry.Instrumentation.AspNet +{ + /// + /// Asp.Net Requests instrumentation. + /// + internal class AspNetMetrics : IDisposable + { + internal static readonly AssemblyName AssemblyName = typeof(HttpInMetricsListener).Assembly.GetName(); + internal static readonly string InstrumentationName = AssemblyName.Name; + internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + + private readonly Meter meter; + + private readonly HttpInMetricsListener httpInMetricsListener; + + /// + /// Initializes a new instance of the class. + /// + public AspNetMetrics() + { + this.meter = new Meter(InstrumentationName, InstrumentationVersion); + this.httpInMetricsListener = new HttpInMetricsListener(this.meter); + } + + /// + public void Dispose() + { + this.meter?.Dispose(); + this.httpInMetricsListener?.Dispose(); + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md index 85cc06105e0..0eced13388a 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +* Added ASP.NET metrics instrumentation to collect `http.server.duration`. + ([#2985](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2985)) * Fix: Http server span status is now unset for `400`-`499`. ([#2904](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2904)) diff --git a/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInMetricsListener.cs new file mode 100644 index 00000000000..ece456273ce --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInMetricsListener.cs @@ -0,0 +1,54 @@ +// +// 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.Diagnostics; +using System.Diagnostics.Metrics; +using System.Web; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.AspNet.Implementation +{ + internal sealed class HttpInMetricsListener : IDisposable + { + private readonly Histogram httpServerDuration; + + public HttpInMetricsListener(Meter meter) + { + this.httpServerDuration = meter.CreateHistogram("http.server.duration", "ms", "measures the duration of the inbound HTTP request"); + TelemetryHttpModule.Options.OnRequestStoppedCallback += this.OnStopActivity; + } + + public void Dispose() + { + TelemetryHttpModule.Options.OnRequestStoppedCallback -= this.OnStopActivity; + } + + private void OnStopActivity(Activity activity, HttpContext context) + { + // TODO: This is just a minimal set of attributes. See the spec for additional attributes: + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server + var tags = new TagList + { + { SemanticConventions.AttributeHttpMethod, context.Request.HttpMethod }, + { SemanticConventions.AttributeHttpScheme, context.Request.Url.Scheme }, + { SemanticConventions.AttributeHttpStatusCode, context.Response.StatusCode }, + }; + + this.httpServerDuration.Record(activity.Duration.TotalMilliseconds, tags); + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNet/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNet/MeterProviderBuilderExtensions.cs new file mode 100644 index 00000000000..e92740546af --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNet/MeterProviderBuilderExtensions.cs @@ -0,0 +1,42 @@ +// +// 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 OpenTelemetry.Instrumentation.AspNet; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// Extension methods to simplify registering of ASP.NET request instrumentation. + /// + public static class MeterProviderBuilderExtensions + { + /// + /// Enables the incoming requests automatic data collection for ASP.NET. + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddAspNetInstrumentation( + this MeterProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + + var instrumentation = new AspNetMetrics(); + builder.AddMeter(AspNetMetrics.InstrumentationName); + return builder.AddInstrumentation(() => instrumentation); + } + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInMetricsListenerTests.cs b/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInMetricsListenerTests.cs new file mode 100644 index 00000000000..b9fbbbeebeb --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInMetricsListenerTests.cs @@ -0,0 +1,112 @@ +// +// 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.IO; +using System.Web; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Xunit; + +namespace OpenTelemetry.Instrumentation.AspNet.Tests +{ + public class HttpInMetricsListenerTests + { + [Fact] + public void HttpDurationMetricIsEmitted() + { + 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 traceprovider = 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() + .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(3, metricPoints[0].Tags.Count); + string httpMethod = null; + int httpStatusCode = 0; + string httpScheme = 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; + } + } + + Assert.Equal("GET", httpMethod); + Assert.Equal(200, httpStatusCode); + Assert.Equal("http", httpScheme); + } + } +}