// // 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. // namespace OpenTelemetry.Instrumentation.AspNet.Tests { using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using System.Web; using OpenTelemetry.Context.Propagation; using Xunit; public class ActivityHelperTest : IDisposable { private const string TraceParentHeaderName = "traceparent"; private const string TraceStateHeaderName = "tracestate"; private const string BaggageHeaderName = "baggage"; private const string BaggageInHeader = "TestKey1=123,TestKey2=456,TestKey1=789"; private const string TestActivityName = "Activity.Test"; private readonly TextMapPropagator noopTextMapPropagator = new NoopTextMapPropagator(); private ActivityListener activitySourceListener; public void Dispose() { this.activitySourceListener?.Dispose(); } [Fact] public void Has_Started_Returns_Correctly() { var context = HttpContextHelper.GetFakeHttpContext(); bool result = ActivityHelper.HasStarted(context, out Activity aspNetActivity); Assert.False(result); Assert.Null(aspNetActivity); context.Items[ActivityHelper.ContextKey] = ActivityHelper.StartedButNotSampledObj; result = ActivityHelper.HasStarted(context, out aspNetActivity); Assert.True(result); Assert.Null(aspNetActivity); Activity activity = new Activity(TestActivityName); context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = activity }; result = ActivityHelper.HasStarted(context, out aspNetActivity); Assert.True(result); Assert.NotNull(aspNetActivity); Assert.Equal(activity, aspNetActivity); } [Fact] public async Task Can_Restore_Activity() { this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); rootActivity.AddTag("k1", "v1"); rootActivity.AddTag("k2", "v2"); Task testTask; using (ExecutionContext.SuppressFlow()) { testTask = Task.Run(() => { Task.Yield(); Assert.Null(Activity.Current); ActivityHelper.RestoreContextIfNeeded(context); Assert.Same(Activity.Current, rootActivity); }); } await testTask.ConfigureAwait(false); } [Fact(Skip = "Temporarily disable until stable.")] public async Task Can_Restore_Baggage() { this.EnableListener(); var requestHeaders = new Dictionary { { BaggageHeaderName, BaggageInHeader }, }; var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); using var rootActivity = ActivityHelper.StartAspNetActivity(new CompositeTextMapPropagator(new TextMapPropagator[] { new TraceContextPropagator(), new BaggagePropagator() }), context, null); rootActivity.AddTag("k1", "v1"); rootActivity.AddTag("k2", "v2"); Task testTask; using (ExecutionContext.SuppressFlow()) { testTask = Task.Run(() => { Task.Yield(); Assert.Null(Activity.Current); Assert.Equal(0, Baggage.Current.Count); ActivityHelper.RestoreContextIfNeeded(context); Assert.Same(Activity.Current, rootActivity); Assert.Empty(rootActivity.Baggage); Assert.Equal(2, Baggage.Current.Count); Assert.Equal("789", Baggage.Current.GetBaggage("TestKey1")); Assert.Equal("456", Baggage.Current.GetBaggage("TestKey2")); }); } await testTask.ConfigureAwait(false); } [Fact] public void Can_Stop_Lost_Activity() { this.EnableListener(a => { Assert.NotNull(Activity.Current); Assert.Equal(Activity.Current, a); Assert.Equal(TelemetryHttpModule.AspNetActivityName, Activity.Current.OperationName); }); var context = HttpContextHelper.GetFakeHttpContext(); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); rootActivity.AddTag("k1", "v1"); rootActivity.AddTag("k2", "v2"); Activity.Current = null; ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.Null(Activity.Current); Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Do_Not_Restore_Activity_When_There_Is_No_Activity_In_Context() { this.EnableListener(); ActivityHelper.RestoreContextIfNeeded(HttpContextHelper.GetFakeHttpContext()); Assert.Null(Activity.Current); } [Fact] public void Do_Not_Restore_Activity_When_It_Is_Not_Lost() { this.EnableListener(); var root = new Activity("root").Start(); var context = HttpContextHelper.GetFakeHttpContext(); context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = root }; ActivityHelper.RestoreContextIfNeeded(context); Assert.Equal(root, Activity.Current); } [Fact] public void Can_Stop_Activity_Without_AspNetListener_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); var rootActivity = new Activity(TestActivityName); rootActivity.Start(); context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = rootActivity }; Thread.Sleep(100); ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.Null(rootActivity.Parent); Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Stop_Activity_With_AspNetListener_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); var rootActivity = new Activity(TestActivityName); rootActivity.Start(); context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = rootActivity }; Thread.Sleep(100); this.EnableListener(); ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.Null(rootActivity.Parent); Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Stop_Root_Activity_With_All_Children() { this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); var child = new Activity("child").Start(); new Activity("grandchild").Start(); ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.True(child.Duration == TimeSpan.Zero); Assert.Null(rootActivity.Parent); Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Stop_Root_While_Child_Is_Current() { this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); var child = new Activity("child").Start(); ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(child.Duration == TimeSpan.Zero); Assert.NotNull(Activity.Current); Assert.Equal(Activity.Current, child); Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public async Task Can_Stop_Root_Activity_If_It_Is_Broken() { this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); using var root = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); new Activity("child").Start(); for (int i = 0; i < 2; i++) { await Task.Run(() => { // when we enter this method, Current is 'child' activity Activity.Current.Stop(); // here Current is 'parent', but only in this execution context }); } // when we return back here, in the 'parent' execution context // Current is still 'child' activity - changes in child context (inside Task.Run) // do not affect 'parent' context in which Task.Run is called. // But 'child' Activity is stopped, thus consequent calls to Stop will // not update Current ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, root, context, null); Assert.True(root.Duration != TimeSpan.Zero); Assert.Null(context.Items[ActivityHelper.ContextKey]); Assert.Null(Activity.Current); } [Fact] public void Stop_Root_Activity_With_129_Nesting_Depth() { this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); using var root = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); for (int i = 0; i < 129; i++) { new Activity("child" + i).Start(); } // can stop any activity regardless of the stack depth ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, root, context, null); Assert.True(root.Duration != TimeSpan.Zero); Assert.Null(context.Items[ActivityHelper.ContextKey]); Assert.NotNull(Activity.Current); } [Fact] public void Should_Not_Create_RootActivity_If_AspNetListener_Not_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.Null(rootActivity); Assert.Equal(ActivityHelper.StartedButNotSampledObj, context.Items[ActivityHelper.ContextKey]); ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Should_Not_Create_RootActivity_If_AspNetActivity_Not_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); this.EnableListener(onSample: (context) => ActivitySamplingResult.None); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.Null(rootActivity); Assert.Equal(ActivityHelper.StartedButNotSampledObj, context.Items[ActivityHelper.ContextKey]); ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Create_RootActivity_From_W3C_Traceparent() { this.EnableListener(); var requestHeaders = new Dictionary { { TraceParentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-00" }, }; var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); using var rootActivity = ActivityHelper.StartAspNetActivity(new TraceContextPropagator(), context, null); Assert.NotNull(rootActivity); Assert.Equal(ActivityIdFormat.W3C, rootActivity.IdFormat); Assert.Equal("00-0123456789abcdef0123456789abcdef-0123456789abcdef-00", rootActivity.ParentId); Assert.Equal("0123456789abcdef0123456789abcdef", rootActivity.TraceId.ToHexString()); Assert.Equal("0123456789abcdef", rootActivity.ParentSpanId.ToHexString()); Assert.True(rootActivity.Recorded); // note: We're not using a parent-based sampler in this test so the recorded flag of traceparent is ignored. Assert.Null(rootActivity.TraceStateString); Assert.Empty(rootActivity.Baggage); Assert.Equal(0, Baggage.Current.Count); } [Fact] public void Can_Create_RootActivityWithTraceState_From_W3C_TraceContext() { this.EnableListener(); var requestHeaders = new Dictionary { { TraceParentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01" }, { TraceStateHeaderName, "ts1=v1,ts2=v2" }, }; var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); using var rootActivity = ActivityHelper.StartAspNetActivity(new TraceContextPropagator(), context, null); Assert.NotNull(rootActivity); Assert.Equal(ActivityIdFormat.W3C, rootActivity.IdFormat); Assert.Equal("00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", rootActivity.ParentId); Assert.Equal("0123456789abcdef0123456789abcdef", rootActivity.TraceId.ToHexString()); Assert.Equal("0123456789abcdef", rootActivity.ParentSpanId.ToHexString()); Assert.True(rootActivity.Recorded); Assert.Equal("ts1=v1,ts2=v2", rootActivity.TraceStateString); Assert.Empty(rootActivity.Baggage); Assert.Equal(0, Baggage.Current.Count); } [Fact] public void Can_Create_RootActivity_From_W3C_Traceparent_With_Baggage() { this.EnableListener(); var requestHeaders = new Dictionary { { TraceParentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-00" }, { BaggageHeaderName, BaggageInHeader }, }; var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); using var rootActivity = ActivityHelper.StartAspNetActivity(new CompositeTextMapPropagator(new TextMapPropagator[] { new TraceContextPropagator(), new BaggagePropagator() }), context, null); Assert.NotNull(rootActivity); Assert.Equal(ActivityIdFormat.W3C, rootActivity.IdFormat); Assert.Equal("00-0123456789abcdef0123456789abcdef-0123456789abcdef-00", rootActivity.ParentId); Assert.Equal("0123456789abcdef0123456789abcdef", rootActivity.TraceId.ToHexString()); Assert.Equal("0123456789abcdef", rootActivity.ParentSpanId.ToHexString()); Assert.True(rootActivity.Recorded); // note: We're not using a parent-based sampler in this test so the recorded flag of traceparent is ignored. Assert.Null(rootActivity.TraceStateString); Assert.Empty(rootActivity.Baggage); Assert.Equal(2, Baggage.Current.Count); Assert.Equal("789", Baggage.Current.GetBaggage("TestKey1")); Assert.Equal("456", Baggage.Current.GetBaggage("TestKey2")); ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.Equal(0, Baggage.Current.Count); } [Fact] public void Can_Create_RootActivity_And_Start_Activity() { this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.NotNull(rootActivity); Assert.True(!string.IsNullOrEmpty(rootActivity.Id)); } [Fact] public void Can_Create_RootActivity_And_Saved_In_HttContext() { this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.NotNull(rootActivity); Assert.Same(rootActivity, ((ActivityHelper.ContextHolder)context.Items[ActivityHelper.ContextKey])?.Activity); } [Fact] public void Fire_Exception_Events() { int callbacksFired = 0; var context = HttpContextHelper.GetFakeHttpContext(); Activity activity = new Activity(TestActivityName); ActivityHelper.WriteActivityException(activity, context, new InvalidOperationException(), (a, c, e) => { callbacksFired++; }); ActivityHelper.WriteActivityException(null, context, new InvalidOperationException(), (a, c, e) => { callbacksFired++; }); // Callback should fire only for non-null activity Assert.Equal(1, callbacksFired); } private void EnableListener(Action onStarted = null, Action onStopped = null, Func onSample = null) { Debug.Assert(this.activitySourceListener == null, "Cannot attach multiple listeners in tests."); this.activitySourceListener = new ActivityListener { ShouldListenTo = (activitySource) => activitySource.Name == TelemetryHttpModule.AspNetSourceName, ActivityStarted = (a) => onStarted?.Invoke(a), ActivityStopped = (a) => onStopped?.Invoke(a), Sample = (ref ActivityCreationOptions options) => { if (onSample != null) { return onSample(options.Parent); } return ActivitySamplingResult.AllDataAndRecorded; }, }; ActivitySource.AddActivityListener(this.activitySourceListener); } private class TestHttpRequest : HttpRequestBase { private readonly NameValueCollection headers = new(); public override NameValueCollection Headers => this.headers; public override UnvalidatedRequestValuesBase Unvalidated => new TestUnvalidatedRequestValues(this.headers); } private class TestUnvalidatedRequestValues : UnvalidatedRequestValuesBase { public TestUnvalidatedRequestValues(NameValueCollection headers) { this.Headers = headers; } public override NameValueCollection Headers { get; } } private class TestHttpResponse : HttpResponseBase { } private class TestHttpServerUtility : HttpServerUtilityBase { private readonly HttpContextBase context; public TestHttpServerUtility(HttpContextBase context) { this.context = context; } public override Exception GetLastError() { return this.context.Error; } } private class TestHttpContext : HttpContextBase { private readonly Hashtable items; public TestHttpContext(Exception error = null) { this.Server = new TestHttpServerUtility(this); this.items = new Hashtable(); this.Error = error; } public override HttpRequestBase Request { get; } = new TestHttpRequest(); /// public override IDictionary Items => this.items; public override Exception Error { get; } public override HttpServerUtilityBase Server { get; } } private class NoopTextMapPropagator : TextMapPropagator { private static readonly PropagationContext DefaultPropagationContext = default; public override ISet Fields => null; public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) { return DefaultPropagationContext; } public override void Inject(PropagationContext context, T carrier, Action setter) { } } } }