ソースを参照

Add IHttpActivityFeature (#31180)

Sourabh Shirhatti 4 年 前
コミット
f390ff4a6f

+ 22 - 0
src/Hosting/Hosting/src/Internal/ActivityFeature.cs

@@ -0,0 +1,22 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Diagnostics;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.Hosting
+{
+    /// <summary>
+    /// Default implementation for <see cref="IHttpActivityFeature"/>.
+    /// </summary>
+    internal sealed class ActivityFeature : IHttpActivityFeature
+    {
+        internal ActivityFeature(Activity activity)
+        {
+            Activity = activity;
+        }
+
+        /// <inheritdoc />
+        public Activity Activity { get; set; }
+    }
+}

+ 17 - 1
src/Hosting/Hosting/src/Internal/HostingApplication.cs

@@ -122,13 +122,29 @@ namespace Microsoft.AspNetCore.Hosting
         {
             public HttpContext? HttpContext { get; set; }
             public IDisposable? Scope { get; set; }
-            public Activity? Activity { get; set; }
+            public Activity? Activity
+            {
+                get => HttpActivityFeature?.Activity;
+                set
+                {
+                    if (HttpActivityFeature is null)
+                    {
+                        HttpActivityFeature = new ActivityFeature(value!);
+                    }
+                    else
+                    {
+                        HttpActivityFeature.Activity = value!;
+                    }
+                }
+            }
             internal HostingRequestStartingLog? StartLog { get; set; }
 
             public long StartTimestamp { get; set; }
             internal bool HasDiagnosticListener { get; set; }
             public bool EventLogEnabled { get; set; }
 
+            internal IHttpActivityFeature? HttpActivityFeature;
+
             public void Reset()
             {
                 // Not resetting HttpContext here as we pool it on the Context

+ 16 - 2
src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs

@@ -6,6 +6,7 @@ using System.Diagnostics;
 using System.Runtime.CompilerServices;
 using System.Web;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Primitives;
 using Microsoft.Net.Http.Headers;
@@ -16,7 +17,8 @@ namespace Microsoft.AspNetCore.Hosting
     {
         private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
 
-        private const string ActivityName = "Microsoft.AspNetCore.Hosting.HttpRequestIn";
+        // internal so it can be used in tests
+        internal const string ActivityName = "Microsoft.AspNetCore.Hosting.HttpRequestIn";
         private const string ActivityStartKey = ActivityName + ".Start";
         private const string ActivityStopKey = ActivityName + ".Stop";
 
@@ -56,6 +58,18 @@ namespace Microsoft.AspNetCore.Hosting
             {
                 context.Activity = StartActivity(httpContext, loggingEnabled, diagnosticListenerActivityCreationEnabled, out var hasDiagnosticListener);
                 context.HasDiagnosticListener = hasDiagnosticListener;
+
+                if (context.Activity is Activity activity)
+                {
+                    if (httpContext.Features.Get<IHttpActivityFeature>() is IHttpActivityFeature feature)
+                    {
+                        feature.Activity = activity;
+                    }
+                    else
+                    {
+                        httpContext.Features.Set(context.HttpActivityFeature);
+                    }
+                }
             }
 
             if (diagnosticListenerEnabled)
@@ -137,7 +151,7 @@ namespace Microsoft.AspNetCore.Hosting
 
             var activity = context.Activity;
             // Always stop activity if it was started
-            if (activity != null)
+            if (activity is not null)
             {
                 StopActivity(httpContext, activity, context.HasDiagnosticListener);
             }

+ 93 - 2
src/Hosting/Hosting/test/HostingApplicationTests.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
+using System.IO;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Hosting.Server.Abstractions;
 using Microsoft.AspNetCore.Http;
@@ -87,7 +88,97 @@ namespace Microsoft.AspNetCore.Hosting.Tests
             hostingApplication.DisposeContext(context, null);
         }
 
-        private static HostingApplication CreateApplication(IHttpContextFactory httpContextFactory = null, bool useHttpContextAccessor = false)
+        [Fact]
+        public void IHttpActivityFeatureIsPopulated()
+        {
+            var testSource = new ActivitySource(Path.GetRandomFileName());
+            var dummySource = new ActivitySource(Path.GetRandomFileName());
+            using var listener = new ActivityListener
+            {
+                ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource) ||
+                                                    ReferenceEquals(activitySource, dummySource)),
+                Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData
+            };
+            ActivitySource.AddActivityListener(listener);
+
+            var hostingApplication = CreateApplication(activitySource: testSource);
+            var httpContext = new DefaultHttpContext();
+            var context = hostingApplication.CreateContext(httpContext.Features);
+
+            var activityFeature = context.HttpContext.Features.Get<IHttpActivityFeature>();
+            Assert.NotNull(activityFeature);
+            Assert.NotNull(activityFeature.Activity);
+            Assert.Equal(HostingApplicationDiagnostics.ActivityName, activityFeature.Activity.DisplayName);
+            var initialActivity = Activity.Current;
+
+            // Create nested dummy Activity
+            using var _ = dummySource.StartActivity("DummyActivity");
+
+            Assert.Same(initialActivity, activityFeature.Activity);
+            Assert.NotEqual(Activity.Current, activityFeature.Activity);
+
+            // Act/Assert
+            hostingApplication.DisposeContext(context, null);
+        }
+
+        private class TestHttpActivityFeature : IHttpActivityFeature
+        {
+            public Activity Activity { get; set; }
+        }
+
+        [Fact]
+        public void IHttpActivityFeatureIsAssignedToIfItExists()
+        {
+            var testSource = new ActivitySource(Path.GetRandomFileName());
+            var dummySource = new ActivitySource(Path.GetRandomFileName());
+            using var listener = new ActivityListener
+            {
+                ShouldListenTo = activitySource => (ReferenceEquals(activitySource, testSource) ||
+                                                    ReferenceEquals(activitySource, dummySource)),
+                Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData
+            };
+            ActivitySource.AddActivityListener(listener);
+
+            var hostingApplication = CreateApplication(activitySource: testSource);
+            var httpContext = new DefaultHttpContext();
+            httpContext.Features.Set<IHttpActivityFeature>(new TestHttpActivityFeature());
+            var context = hostingApplication.CreateContext(httpContext.Features);
+
+            var activityFeature = context.HttpContext.Features.Get<IHttpActivityFeature>();
+            Assert.NotNull(activityFeature);
+            Assert.IsType<TestHttpActivityFeature>(activityFeature);
+            Assert.NotNull(activityFeature.Activity);
+            Assert.Equal(HostingApplicationDiagnostics.ActivityName, activityFeature.Activity.DisplayName);
+            var initialActivity = Activity.Current;
+
+            // Create nested dummy Activity
+            using var _ = dummySource.StartActivity("DummyActivity");
+
+            Assert.Same(initialActivity, activityFeature.Activity);
+            Assert.NotEqual(Activity.Current, activityFeature.Activity);
+
+            // Act/Assert
+            hostingApplication.DisposeContext(context, null);
+        }
+
+        [Fact]
+        public void IHttpActivityFeatureIsNotPopulatedWithoutAListener()
+        {
+            var hostingApplication = CreateApplication();
+            var httpContext = new DefaultHttpContext();
+            httpContext.Features.Set<IHttpActivityFeature>(new TestHttpActivityFeature());
+            var context = hostingApplication.CreateContext(httpContext.Features);
+
+            var activityFeature = context.HttpContext.Features.Get<IHttpActivityFeature>();
+            Assert.NotNull(activityFeature);
+            Assert.Null(activityFeature.Activity);
+
+            // Act/Assert
+            hostingApplication.DisposeContext(context, null);
+        }
+
+        private static HostingApplication CreateApplication(IHttpContextFactory httpContextFactory = null, bool useHttpContextAccessor = false,
+            ActivitySource activitySource = null)
         {
             var services = new ServiceCollection();
             services.AddOptions();
@@ -102,7 +193,7 @@ namespace Microsoft.AspNetCore.Hosting.Tests
                 ctx => Task.CompletedTask,
                 NullLogger.Instance,
                 new DiagnosticListener("Microsoft.AspNetCore"),
-                new ActivitySource("Microsoft.AspNetCore"),
+                activitySource ?? new ActivitySource("Microsoft.AspNetCore"),
                 httpContextFactory);
 
             return hostingApplication;

+ 18 - 0
src/Http/Http/src/Features/IHttpActivityFeature.cs

@@ -0,0 +1,18 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Diagnostics;
+
+namespace Microsoft.AspNetCore.Http.Features
+{
+    /// <summary>
+    /// Feature to access the <see cref="Activity"/> associated with a request.
+    /// </summary>
+    public interface IHttpActivityFeature
+    {
+        /// <summary>
+        /// Returns the <see cref="Activity"/> associated with the current request.
+        /// </summary>
+        Activity Activity { get; set; }
+    }
+}

+ 3 - 0
src/Http/Http/src/PublicAPI.Unshipped.txt

@@ -6,3 +6,6 @@
 *REMOVED*~Microsoft.AspNetCore.Http.HttpContextFactory.HttpContextFactory(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Http.Features.FormOptions!>! formOptions, Microsoft.AspNetCore.Http.IHttpContextAccessor? httpContextAccessor) -> void
 *REMOVED*~Microsoft.AspNetCore.Http.HttpContextFactory.HttpContextFactory(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Http.Features.FormOptions!>! formOptions, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory! serviceScopeFactory) -> void
 *REMOVED*~Microsoft.AspNetCore.Http.HttpContextFactory.HttpContextFactory(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Http.Features.FormOptions!>! formOptions, Microsoft.Extensions.DependencyInjection.IServiceScopeFactory! serviceScopeFactory, Microsoft.AspNetCore.Http.IHttpContextAccessor? httpContextAccessor) -> void
+Microsoft.AspNetCore.Http.Features.IHttpActivityFeature
+Microsoft.AspNetCore.Http.Features.IHttpActivityFeature.Activity.get -> System.Diagnostics.Activity!
+Microsoft.AspNetCore.Http.Features.IHttpActivityFeature.Activity.set -> void

+ 0 - 3
src/Servers/IIS/IIS/src/Core/IISHttpContext.FeatureCollection.cs

@@ -37,9 +37,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
                                             IHttpResponseTrailersFeature,
                                             IHttpResetFeature
     {
-        // NOTE: When feature interfaces are added to or removed from this HttpProtocol implementation,
-        // then the list of `implementedFeatures` in the generated code project MUST also be updated.
-
         private int _featureRevision;
         private string? _httpProtocolVersion;
         private X509Certificate2? _certificate;

+ 16 - 0
src/Servers/IIS/IIS/src/Core/IISHttpContext.Features.cs

@@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
         private static readonly Type IHttpMaxRequestBodySizeFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
         private static readonly Type IHttpResponseTrailersFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseTrailersFeature);
         private static readonly Type IHttpResetFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResetFeature);
+        private static readonly Type IHttpActivityFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpActivityFeature);
 
         private object? _currentIHttpRequestFeature;
         private object? _currentIHttpRequestBodyDetectionFeature;
@@ -54,6 +55,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
         private object? _currentIHttpMaxRequestBodySizeFeature;
         private object? _currentIHttpResponseTrailersFeature;
         private object? _currentIHttpResetFeature;
+        private object? _currentIHttpActivityFeature;
 
         private void Initialize()
         {
@@ -72,6 +74,8 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
             _currentITlsConnectionFeature = this;
             _currentIHttpResponseTrailersFeature = GetResponseTrailersFeature();
             _currentIHttpResetFeature = GetResetFeature();
+
+            _currentIHttpActivityFeature = null;
         }
 
         internal object? FastFeatureGet(Type key)
@@ -168,6 +172,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
             {
                 return _currentIHttpResetFeature;
             }
+            if (key == IHttpActivityFeature)
+            {
+                return _currentIHttpActivityFeature;
+            }
 
             return ExtraFeatureGet(key);
         }
@@ -283,6 +291,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
             {
                 _currentIHttpResetFeature = feature;
             }
+            if (key == IHttpActivityFeature)
+            {
+                _currentIHttpActivityFeature = feature;
+            }
             if (key == IISHttpContextType)
             {
                 throw new InvalidOperationException("Cannot set IISHttpContext in feature collection");
@@ -380,6 +392,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
             {
                 yield return new KeyValuePair<Type, object>(IHttpResponseTrailersFeature, _currentIHttpResetFeature);
             }
+            if (_currentIHttpActivityFeature != null)
+            {
+                yield return new KeyValuePair<Type, object>(IHttpActivityFeature, _currentIHttpActivityFeature);
+            }
 
             if (MaybeExtra != null)
             {

+ 22 - 0
src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs

@@ -48,6 +48,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
 
         // Other reserved feature slots
         internal protected IServiceProvidersFeature? _currentIServiceProvidersFeature;
+        internal protected IHttpActivityFeature? _currentIHttpActivityFeature;
         internal protected IItemsFeature? _currentIItemsFeature;
         internal protected IQueryFeature? _currentIQueryFeature;
         internal protected IFormFeature? _currentIFormFeature;
@@ -84,6 +85,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             _currentIHttpRequestBodyDetectionFeature = this;
 
             _currentIServiceProvidersFeature = null;
+            _currentIHttpActivityFeature = null;
             _currentIItemsFeature = null;
             _currentIQueryFeature = null;
             _currentIFormFeature = null;
@@ -192,6 +194,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 {
                     feature = _currentIServiceProvidersFeature;
                 }
+                else if (key == typeof(IHttpActivityFeature))
+                {
+                    feature = _currentIHttpActivityFeature;
+                }
                 else if (key == typeof(IItemsFeature))
                 {
                     feature = _currentIItemsFeature;
@@ -316,6 +322,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 {
                     _currentIServiceProvidersFeature = (IServiceProvidersFeature?)value;
                 }
+                else if (key == typeof(IHttpActivityFeature))
+                {
+                    _currentIHttpActivityFeature = (IHttpActivityFeature?)value;
+                }
                 else if (key == typeof(IItemsFeature))
                 {
                     _currentIItemsFeature = (IItemsFeature?)value;
@@ -442,6 +452,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             {
                 feature = Unsafe.As<IServiceProvidersFeature?, TFeature?>(ref _currentIServiceProvidersFeature);
             }
+            else if (typeof(TFeature) == typeof(IHttpActivityFeature))
+            {
+                feature = Unsafe.As<IHttpActivityFeature?, TFeature?>(ref _currentIHttpActivityFeature);
+            }
             else if (typeof(TFeature) == typeof(IItemsFeature))
             {
                 feature = Unsafe.As<IItemsFeature?, TFeature?>(ref _currentIItemsFeature);
@@ -574,6 +588,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             {
                 _currentIServiceProvidersFeature = Unsafe.As<TFeature?, IServiceProvidersFeature?>(ref feature);
             }
+            else if (typeof(TFeature) == typeof(IHttpActivityFeature))
+            {
+                _currentIHttpActivityFeature = Unsafe.As<TFeature?, IHttpActivityFeature?>(ref feature);
+            }
             else if (typeof(TFeature) == typeof(IItemsFeature))
             {
                 _currentIItemsFeature = Unsafe.As<TFeature?, IItemsFeature?>(ref feature);
@@ -694,6 +712,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             {
                 yield return new KeyValuePair<Type, object>(typeof(IServiceProvidersFeature), _currentIServiceProvidersFeature);
             }
+            if (_currentIHttpActivityFeature != null)
+            {
+                yield return new KeyValuePair<Type, object>(typeof(IHttpActivityFeature), _currentIHttpActivityFeature);
+            }
             if (_currentIItemsFeature != null)
             {
                 yield return new KeyValuePair<Type, object>(typeof(IItemsFeature), _currentIItemsFeature);

+ 2 - 1
src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs

@@ -16,7 +16,8 @@ namespace CodeGenerator
                 "IHttpResponseBodyFeature",
                 "IRouteValuesFeature",
                 "IEndpointFeature",
-                "IServiceProvidersFeature"
+                "IServiceProvidersFeature",
+                "IHttpActivityFeature"
             };
 
             var commonFeatures = new[]