diff --git a/examples/AspNetCore/Startup.cs b/examples/AspNetCore/Startup.cs index 625c7c16615..416f02de043 100644 --- a/examples/AspNetCore/Startup.cs +++ b/examples/AspNetCore/Startup.cs @@ -23,8 +23,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Instrumentation.AspNetCore; +using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -32,6 +34,8 @@ namespace Examples.AspNetCore { public class Startup { + private MeterProvider meterProvider; + public Startup(IConfiguration configuration) { this.Configuration = configuration; @@ -111,6 +115,16 @@ public void ConfigureServices(IServiceCollection services) break; } + + // TODO: Add IServiceCollection.AddOpenTelemetryMetrics extension method + var providerBuilder = Sdk.CreateMeterProviderBuilder() + .AddAspNetCoreInstrumentation(); + + // TODO: Add configuration switch for Prometheus and OTLP export + providerBuilder + .AddConsoleExporter(); + + this.meterProvider = providerBuilder.Build(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs index d47be1c1ff9..f5bb6dc9ff2 100644 --- a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs @@ -29,6 +29,16 @@ protected MeterProviderBuilder() { } + /// + /// Adds instrumentation to the provider. + /// + /// Type of instrumentation class. + /// Function that builds instrumentation. + /// Returns for chaining. + public abstract MeterProviderBuilder AddInstrumentation( + Func instrumentationFactory) + where TInstrumentation : class; + /// /// Adds given Meter names to the list of subscribed sources. /// diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.cs new file mode 100644 index 00000000000..7f4c2292091 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreMetrics.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.AspNetCore.Implementation; + +namespace OpenTelemetry.Instrumentation.AspNetCore +{ + /// + /// Asp.Net Core Requests instrumentation. + /// + internal class AspNetCoreMetrics : IDisposable + { + internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); + internal static readonly string InstrumentationName = AssemblyName.Name; + internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString(); + + private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + private readonly Meter meter; + + /// + /// Initializes a new instance of the class. + /// + public AspNetCoreMetrics() + { + this.meter = new Meter(InstrumentationName, InstrumentationVersion); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpInMetricsListener("Microsoft.AspNetCore", this.meter), null); + this.diagnosticSourceSubscriber.Subscribe(); + } + + /// + public void Dispose() + { + this.diagnosticSourceSubscriber?.Dispose(); + this.meter?.Dispose(); + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs new file mode 100644 index 00000000000..3856299dd2a --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -0,0 +1,78 @@ +// +// 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.Diagnostics.Metrics; +using Microsoft.AspNetCore.Http; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation +{ + internal class HttpInMetricsListener : ListenerHandler + { + private readonly PropertyFetcher stopContextFetcher = new PropertyFetcher("HttpContext"); + private readonly Meter meter; + + private Counter httpServerRequestCount; + + public HttpInMetricsListener(string name, Meter meter) + : base(name) + { + this.meter = meter; + + // TODO: + // In the future, this instrumentation should produce the http.server.duration metric which will likely be represented as a histogram. + // See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server + // + // Histograms are not yet supported by the SDK. + // + // For now we produce a count metric called http.server.request_count just for demonstration purposes. + // This metric is not defined by the in the semantic conventions. + this.httpServerRequestCount = meter.CreateCounter("http.server.request_count", null, "The number of HTTP requests processed."); + } + + public override void OnStopActivity(Activity activity, object payload) + { + HttpContext context = this.stopContextFetcher.Fetch(payload); + if (context == null) + { + AspNetCoreInstrumentationEventSource.Log.NullPayload(nameof(HttpInMetricsListener), nameof(this.OnStopActivity)); + return; + } + + // TODO: Prometheus pulls metrics by invoking the /metrics endpoint. Decide if it makes sense to suppress this. + // Below is just a temporary way of achieving this suppression for metrics (we should consider suppressing traces too). + // If we want to suppress activity from Prometheus then we should use SuppressInstrumentationScope. + if (context.Request.Path.HasValue && context.Request.Path.Value.Contains("metrics")) + { + return; + } + + // 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 KeyValuePair[] + { + new KeyValuePair(SemanticConventions.AttributeHttpMethod, context.Request.Method), + new KeyValuePair(SemanticConventions.AttributeHttpScheme, context.Request.Scheme), + new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, context.Response.StatusCode), + new KeyValuePair(SemanticConventions.AttributeHttpFlavor, context.Request.Protocol), + }; + + this.httpServerRequestCount.Add(1, tags); + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs new file mode 100644 index 00000000000..8f32fea696b --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.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 OpenTelemetry.Instrumentation.AspNetCore; + +namespace OpenTelemetry.Metrics +{ + /// + /// Extension methods to simplify registering of ASP.NET Core request instrumentation. + /// + public static class MeterProviderBuilderExtensions + { + /// + /// Enables the incoming requests automatic data collection for ASP.NET Core. + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddAspNetCoreInstrumentation( + this MeterProviderBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + // TODO: Implement an IDeferredMeterProviderBuilder + + // TODO: Handle AspNetCoreInstrumentationOptions + // Filter - makes sense for metric instrumentation + // Enrich - do we want a similar kind of functionality for metrics? + // RecordException - probably doesn't make sense for metric instrumentation + // EnableGrpcAspNetCoreSupport - this instrumentation will also need to also handle gRPC requests + + var instrumentation = new AspNetCoreMetrics(); + builder.AddSource(AspNetCoreMetrics.InstrumentationName); + return builder.AddInstrumentation(() => instrumentation); + } + } +} diff --git a/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs index d6725972101..eb35983fed3 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs @@ -22,6 +22,7 @@ namespace OpenTelemetry.Metrics { internal class MeterProviderBuilderSdk : MeterProviderBuilder { + private readonly List instrumentationFactories = new List(); private readonly List meterSources = new List(); private ResourceBuilder resourceBuilder = ResourceBuilder.CreateDefault(); @@ -33,6 +34,22 @@ internal MeterProviderBuilderSdk() internal List MetricProcessors { get; } = new List(); + public override MeterProviderBuilder AddInstrumentation(Func instrumentationFactory) + { + if (instrumentationFactory == null) + { + throw new ArgumentNullException(nameof(instrumentationFactory)); + } + + this.instrumentationFactories.Add( + new InstrumentationFactory( + typeof(TInstrumentation).Name, + "semver:" + typeof(TInstrumentation).Assembly.GetName().Version, + instrumentationFactory)); + + return this; + } + public override MeterProviderBuilder AddSource(params string[] names) { if (names == null) @@ -76,8 +93,24 @@ internal MeterProvider Build() return new MeterProviderSdk( this.resourceBuilder.Build(), this.meterSources, + this.instrumentationFactories, this.MeasurementProcessors.ToArray(), this.MetricProcessors.ToArray()); } + + // TODO: This is copied from TracerProviderBuilderSdk. Move to common location. + internal readonly struct InstrumentationFactory + { + public readonly string Name; + public readonly string Version; + public readonly Func Factory; + + internal InstrumentationFactory(string name, string version, Func factory) + { + this.Name = name; + this.Version = version; + this.Factory = factory; + } + } } } diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 04f97d00350..a6edbf58652 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -18,6 +18,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Metrics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using OpenTelemetry.Resources; @@ -29,6 +30,7 @@ public class MeterProviderSdk { internal readonly ConcurrentDictionary AggregatorStores = new ConcurrentDictionary(); + private readonly List instrumentations = new List(); private readonly object collectLock = new object(); private readonly CancellationTokenSource cts = new CancellationTokenSource(); private readonly List collectorTasks = new List(); @@ -39,6 +41,7 @@ public class MeterProviderSdk internal MeterProviderSdk( Resource resource, IEnumerable meterSources, + List instrumentationFactories, MeasurementProcessor[] measurementProcessors, MetricProcessor[] metricProcessors) { @@ -52,6 +55,14 @@ internal MeterProviderSdk( processor.SetParentProvider(this); } + if (instrumentationFactories.Any()) + { + foreach (var instrumentationFactory in instrumentationFactories) + { + this.instrumentations.Add(instrumentationFactory.Factory()); + } + } + // Setup Listener var meterSourcesToSubscribe = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var name in meterSources) @@ -121,6 +132,16 @@ internal void MeasurementRecorded(Instrument instrument, T value, ReadOnlySpa protected override void Dispose(bool disposing) { + if (this.instrumentations != null) + { + foreach (var item in this.instrumentations) + { + (item as IDisposable)?.Dispose(); + } + + this.instrumentations.Clear(); + } + this.listener.Dispose(); this.cts.Cancel();