diff --git a/.gitignore b/.gitignore index be686e726..d95806039 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ Icon? ehthumbs.db Thumbs.db .vs/ + +# Publish Web output # +###################### +PublishProfiles/ \ No newline at end of file diff --git a/DurableTask.sln b/DurableTask.sln index 632e1ec4b..8a74f5b98 100644 --- a/DurableTask.sln +++ b/DurableTask.sln @@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.SqlServer", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DurableTask.SqlServer.Tests", "test\DurableTask.SqlServer.Tests\DurableTask.SqlServer.Tests.csproj", "{B835BFA6-D9BB-47C4-8594-38EAE0157BBA}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Correlation.Samples", "samples\Correlation.Samples\Correlation.Samples.csproj", "{5F88FF6A-E908-4341-89D6-FA530793077A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -239,6 +241,14 @@ Global {B835BFA6-D9BB-47C4-8594-38EAE0157BBA}.Release|Any CPU.Build.0 = Release|Any CPU {B835BFA6-D9BB-47C4-8594-38EAE0157BBA}.Release|x64.ActiveCfg = Release|Any CPU {B835BFA6-D9BB-47C4-8594-38EAE0157BBA}.Release|x64.Build.0 = Release|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Debug|x64.Build.0 = Debug|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Release|Any CPU.Build.0 = Release|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Release|x64.ActiveCfg = Release|Any CPU + {5F88FF6A-E908-4341-89D6-FA530793077A}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -266,9 +276,10 @@ Global {1D52F94E-933C-411F-96C4-4960B423586F} = {C53918E6-667A-4930-837C-0018C5D6E374} {C9F84589-3F2B-4D58-A995-BC169D16217F} = {DBCD161C-D409-48E5-924E-9B7FA1C36B84} {B835BFA6-D9BB-47C4-8594-38EAE0157BBA} = {95C69A06-7F62-4652-A480-207B614C2869} + {5F88FF6A-E908-4341-89D6-FA530793077A} = {AF4E71A6-B16E-4488-B22D-2761101A601A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution - EnterpriseLibraryConfigurationToolBinariesPath = packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4 SolutionGuid = {2D63A120-9394-48D9-8CA9-1184364FB854} + EnterpriseLibraryConfigurationToolBinariesPath = packages\TransientFaultHandling.Core.5.1.1209.1\lib\NET4 EndGlobalSection EndGlobal diff --git a/samples/Correlation.Samples/ContinueAsNewOrchestration.cs b/samples/Correlation.Samples/ContinueAsNewOrchestration.cs new file mode 100644 index 000000000..3d85e89a3 --- /dev/null +++ b/samples/Correlation.Samples/ContinueAsNewOrchestration.cs @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + + [KnownType(typeof(HelloActivity))] + internal class ContinueAsNewOrchestration : TaskOrchestration + { + static int counter = 0; + + public override async Task RunTask(OrchestrationContext context, string input) + { + string result = await context.ScheduleTask(typeof(HelloActivity), input); + result = input + ":" + result; + if (counter < 3) + { + counter++; + context.ContinueAsNew(result); + } + + return result; + } + } + + internal class HelloActivity : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentNullException(nameof(input)); + } + + Console.WriteLine($"Activity: Hello {input}"); + return $"Hello, {input}!"; + } + } +} diff --git a/samples/Correlation.Samples/CorrelatedExceptionExtensions.cs b/samples/Correlation.Samples/CorrelatedExceptionExtensions.cs new file mode 100644 index 000000000..5a45c673d --- /dev/null +++ b/samples/Correlation.Samples/CorrelatedExceptionExtensions.cs @@ -0,0 +1,32 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Collections.Generic; + using System.Text; + using DurableTask.Core; + using Microsoft.ApplicationInsights.DataContracts; + + public static class CorrelatedExceptionExtensions + { + public static ExceptionTelemetry CreateExceptionTelemetry(this CorrelatedExceptionDetails e) + { + var exceptionTelemetry = new ExceptionTelemetry(e.Exception); + exceptionTelemetry.Context.Operation.Id = e.OperationId; + exceptionTelemetry.Context.Operation.ParentId = e.ParentId; + return exceptionTelemetry; + } + } +} diff --git a/samples/Correlation.Samples/Correlation.Samples.csproj b/samples/Correlation.Samples/Correlation.Samples.csproj new file mode 100644 index 000000000..bcd0aeb16 --- /dev/null +++ b/samples/Correlation.Samples/Correlation.Samples.csproj @@ -0,0 +1,31 @@ + + + + Exe + netcoreapp2.1 + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/samples/Correlation.Samples/DurableTaskCorrelationTelemetryInitializer.cs b/samples/Correlation.Samples/DurableTaskCorrelationTelemetryInitializer.cs new file mode 100644 index 000000000..b7ee5ca42 --- /dev/null +++ b/samples/Correlation.Samples/DurableTaskCorrelationTelemetryInitializer.cs @@ -0,0 +1,328 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using DurableTask.Core; + using DurableTask.Core.Settings; + using ImpromptuInterface; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.Common; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + + /// + /// Telemetry Initializer that sets correlation ids for W3C. + /// This source is based on W3COperationCorrelationTelemetryInitializer.cs + /// 1. Modified with CorrelationTraceContext.Current + /// 2. Avoid to be overriden when it is RequestTelemetry + /// Original Source is here + /// + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEPENDENCY_COLLECTOR + public +#else + internal +#endif + class DurableTaskCorrelationTelemetryInitializer : ITelemetryInitializer + { + private const string RddDiagnosticSourcePrefix = "rdddsc"; + private const string SqlRemoteDependencyType = "SQL"; + + /// These internal property is copied from W3CConstants + /// Trace-Id tag name. + internal const string TraceIdTag = "w3c_traceId"; + + /// Span-Id tag name. + internal const string SpanIdTag = "w3c_spanId"; + + /// Parent span-Id tag name. + internal const string ParentSpanIdTag = "w3c_parentSpanId"; + + /// Version tag name. + internal const string VersionTag = "w3c_version"; + + /// Sampled tag name. + internal const string SampledTag = "w3c_sampled"; + + /// Tracestate tag name. + internal const string TracestateTag = "w3c_tracestate"; + + /// Default version value. + internal const string DefaultVersion = "00"; + + /// + /// Default sampled flag value: may be recorded, not requested + /// + internal const string TraceFlagRecordedAndNotRequested = "02"; + + /// Recorded and requested sampled flag value + internal const string TraceFlagRecordedAndRequested = "03"; + + /// Requested trace flag + internal const byte RequestedTraceFlag = 1; + + /// Legacy root Id tag name. + internal const string LegacyRootIdProperty = "ai_legacyRootId"; + + /// Legacy root Id tag name. + internal const string LegacyRequestIdProperty = "ai_legacyRequestId"; + + /// + /// Set of suppress telemetry tracking if you add Host name on this. + /// + public HashSet ExcludeComponentCorrelationHttpHeadersOnDomains { get; set; } + + /// + /// Constructor + /// + public DurableTaskCorrelationTelemetryInitializer() + { + ExcludeComponentCorrelationHttpHeadersOnDomains = new HashSet(); + } + + /// + /// Initializes telemety item. + /// + /// Telemetry item. + public void Initialize(ITelemetry telemetry) + { + if (IsSuppressedTelemetry(telemetry)) + { + SuppressTelemetry(telemetry); + return; + } + + if (!(telemetry is RequestTelemetry)) + { + Activity currentActivity = Activity.Current; + if (telemetry is ExceptionTelemetry) + { + Console.WriteLine("exception!"); + } + + if (currentActivity == null) + { + if (CorrelationTraceContext.Current != null) + { + UpdateTelemetry(telemetry, CorrelationTraceContext.Current); + } + } + else + { + if (CorrelationTraceContext.Current != null) + { + UpdateTelemetry(telemetry, CorrelationTraceContext.Current); + } + else if (CorrelationSettings.Current.Protocol == Protocol.W3CTraceContext) + { + UpdateTelemetry(telemetry, currentActivity, false); + } + else if (CorrelationSettings.Current.Protocol == Protocol.HttpCorrelationProtocol + && telemetry is ExceptionTelemetry) + { + UpdateTelemetryExceptionForHTTPCorrelationProtocol((ExceptionTelemetry)telemetry, currentActivity); + } + } + } + } + + internal static void UpdateTelemetry(ITelemetry telemetry, TraceContextBase contextBase) + { + switch (contextBase) + { + case NullObjectTraceContext nullObjectContext: + return; + case W3CTraceContext w3cContext: + UpdateTelemetryW3C(telemetry, w3cContext); + break; + case HttpCorrelationProtocolTraceContext httpCorrelationProtocolTraceContext: + UpdateTelemetryHttpCorrelationProtocol(telemetry, httpCorrelationProtocolTraceContext); + break; + default: + return; + } + } + + internal static void UpdateTelemetryHttpCorrelationProtocol(ITelemetry telemetry, HttpCorrelationProtocolTraceContext context) + { + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + + bool initializeFromCurrent = opTelemetry != null; + + if (initializeFromCurrent) + { + initializeFromCurrent &= !(opTelemetry is DependencyTelemetry dependency && + dependency.Type == SqlRemoteDependencyType && + dependency.Context.GetInternalContext().SdkVersion + .StartsWith(RddDiagnosticSourcePrefix, StringComparison.Ordinal)); + } + + if (initializeFromCurrent) + { + opTelemetry.Id = !string.IsNullOrEmpty(opTelemetry.Id) ? opTelemetry.Id : context.TelemetryId; + telemetry.Context.Operation.ParentId = !string.IsNullOrEmpty(telemetry.Context.Operation.ParentId) ? telemetry.Context.Operation.ParentId : context.TelemetryContextOperationParentId; + } + else + { + telemetry.Context.Operation.Id = !string.IsNullOrEmpty(telemetry.Context.Operation.Id) ? telemetry.Context.Operation.Id : context.TelemetryContextOperationId; + if (telemetry is ExceptionTelemetry) + { + telemetry.Context.Operation.ParentId = context.TelemetryId; + } + else + { + telemetry.Context.Operation.ParentId = !string.IsNullOrEmpty(telemetry.Context.Operation.ParentId) ? telemetry.Context.Operation.ParentId : context.TelemetryContextOperationParentId; + } + } + } + + internal static void UpdateTelemetryW3C(ITelemetry telemetry, W3CTraceContext context) + { + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + + bool initializeFromCurrent = opTelemetry != null; + + if (initializeFromCurrent) + { + initializeFromCurrent &= !(opTelemetry is DependencyTelemetry dependency && + dependency.Type == SqlRemoteDependencyType && + dependency.Context.GetInternalContext().SdkVersion + .StartsWith(RddDiagnosticSourcePrefix, StringComparison.Ordinal)); + } + + if (!string.IsNullOrEmpty(context.TraceState)) + { + opTelemetry.Properties["w3c_tracestate"] = context.TraceState; + } + + TraceParent traceParent = context.TraceParent.ToTraceParent(); + + if (initializeFromCurrent) + { + if (string.IsNullOrEmpty(opTelemetry.Id)) + opTelemetry.Id = traceParent.SpanId; + + if (string.IsNullOrEmpty(context.ParentSpanId)) + { + telemetry.Context.Operation.ParentId = telemetry.Context.Operation.Id; + } + } + else + { + if (telemetry.Context.Operation.Id == null) + { + telemetry.Context.Operation.Id = traceParent.TraceId; + } + + if (telemetry.Context.Operation.ParentId == null) + { + telemetry.Context.Operation.ParentId = traceParent.SpanId; + } + } + } + + internal void SuppressTelemetry(ITelemetry telemetry) + { + // TODO change the strategy. + telemetry.Context.Operation.Id = "suppressed"; + telemetry.Context.Operation.ParentId = "suppressed"; + // Context. Properties. ai_legacyRequestId , ai_legacyRequestId + foreach (var key in telemetry.Context.Properties.Keys) + { + if (key == "ai_legacyRootId" || + key == "ai_legacyRequestId") + { + telemetry.Context.Properties[key] = "suppressed"; + } + } + + ((OperationTelemetry)telemetry).Id = "suppressed"; + } + + internal bool IsSuppressedTelemetry(ITelemetry telemetry) + { + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + if (telemetry is DependencyTelemetry) + { + DependencyTelemetry dTelemetry = telemetry as DependencyTelemetry; + + if (!string.IsNullOrEmpty(dTelemetry.CommandName)) + { + var host = new Uri(dTelemetry.CommandName).Host; + if (ExcludeComponentCorrelationHttpHeadersOnDomains.Contains(host)) return true; + } + } + + return false; + } + + internal static void UpdateTelemetryExceptionForHTTPCorrelationProtocol(ExceptionTelemetry telemetry, Activity activity) + { + telemetry.Context.Operation.ParentId = activity.Id; + telemetry.Context.Operation.Id = activity.RootId; + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "This method has different code for Net45/NetCore")] + internal static void UpdateTelemetry(ITelemetry telemetry, Activity activity, bool forceUpdate) + { + if (activity == null) + { + return; + } + + // Requests and dependnecies are initialized from the current Activity + // (i.e. telemetry.Id = current.Id). Activity is created for such requests specifically + // Traces, exceptions, events on the other side are children of current activity + // There is one exception - SQL DiagnosticSource where current Activity is a parent + // for dependency calls. + + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + bool initializeFromCurrent = opTelemetry != null; + + if (initializeFromCurrent) + { + initializeFromCurrent &= !(opTelemetry is DependencyTelemetry dependency && + dependency.Type == SqlRemoteDependencyType && + dependency.Context.GetInternalContext().SdkVersion + .StartsWith(RddDiagnosticSourcePrefix, StringComparison.Ordinal)); + } + + if (telemetry is OperationTelemetry operation) + { + operation.Properties[TracestateTag] = activity.TraceStateString; + } + + if (initializeFromCurrent) + { + opTelemetry.Id = activity.SpanId.ToHexString(); + if (activity.ParentSpanId != null) + { + opTelemetry.Context.Operation.ParentId = activity.ParentSpanId.ToHexString(); + } + } + else + { + telemetry.Context.Operation.ParentId = activity.SpanId.ToHexString(); + } + } + } +} diff --git a/samples/Correlation.Samples/FanOutFanInOrchestrator.cs b/samples/Correlation.Samples/FanOutFanInOrchestrator.cs new file mode 100644 index 000000000..157e19499 --- /dev/null +++ b/samples/Correlation.Samples/FanOutFanInOrchestrator.cs @@ -0,0 +1,58 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Runtime.Serialization; + using System.Text; + using System.Threading.Tasks; + using DurableTask.Core; + + [KnownType(typeof(ParallelHello))] + internal class FanOutFanInOrchestrator : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + var tasks = new Task[3]; + for (int i = 0; i < 3; i++) + { + tasks[i] = context.ScheduleTask(typeof(ParallelHello), $"world({i})"); + } + + await Task.WhenAll(tasks); + var buffer = new StringBuilder(); + foreach(var task in tasks) + { + buffer.Append(task.Result); + buffer.Append(":"); + } + + return buffer.ToString(); + } + } + + internal class ParallelHello : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentNullException(nameof(input)); + } + + Console.WriteLine($"Activity: Parallel Hello {input}"); + return $"Parallel Hello, {input}!"; + } + } +} diff --git a/samples/Correlation.Samples/HelloOrchestrator.cs b/samples/Correlation.Samples/HelloOrchestrator.cs new file mode 100644 index 000000000..225e5b084 --- /dev/null +++ b/samples/Correlation.Samples/HelloOrchestrator.cs @@ -0,0 +1,45 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + + [KnownType(typeof(Hello))] + internal class HelloOrchestrator : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + // await contextBase.ScheduleTask(typeof(Hello), "world"); + // if you pass an empty string it throws an error + return await context.ScheduleTask(typeof(Hello), "world"); + } + } + + internal class Hello : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentNullException(nameof(input)); + } + + Console.WriteLine($"Activity: Hello {input}"); + return $"Hello, {input}!"; + } + } +} diff --git a/samples/Correlation.Samples/MultiLayeredOrchestrationWithRetry.cs b/samples/Correlation.Samples/MultiLayeredOrchestrationWithRetry.cs new file mode 100644 index 000000000..56ef88848 --- /dev/null +++ b/samples/Correlation.Samples/MultiLayeredOrchestrationWithRetry.cs @@ -0,0 +1,76 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + using System; + + [KnownType(typeof(MultiLayeredOrchestrationChildWithRetry))] + [KnownType(typeof(NeedToExecuteTwice01))] + [KnownType(typeof(NeedToExecuteTwice02))] + internal class MultiLayeredOrchestrationWithRetryOrchestrator : TaskOrchestration + { + public override Task RunTask(OrchestrationContext context, string input) + { + var retryOption = new RetryOptions(TimeSpan.FromMilliseconds(10), 3); + return context.CreateSubOrchestrationInstanceWithRetry(typeof(MultiLayeredOrchestrationChildWithRetry), retryOption, input); + } + } + + [KnownType(typeof(NeedToExecuteTwice01))] + [KnownType(typeof(NeedToExecuteTwice02))] + internal class MultiLayeredOrchestrationChildWithRetry : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + var result01 = await context.ScheduleTask(typeof(NeedToExecuteTwice01), input); + var result02 = await context.ScheduleTask(typeof(NeedToExecuteTwice02), input); + return $"{result01}:{result02}"; + } + } + + internal class NeedToExecuteTwice01 : TaskActivity + { + static int Counter = 0; + + protected override string Execute(TaskContext context, string input) + { + if (Counter == 0) + { + Counter++; + throw new Exception("Something happens"); + } + + return $"Hello {input} with retry"; + } + } + + internal class NeedToExecuteTwice02 : TaskActivity + { + static int Counter = 0; + + protected override string Execute(TaskContext context, string input) + { + if (Counter == 0) + { + Counter++; + throw new Exception("Something happens"); + } + + return $"Hello {input} with retry"; + } + } +} diff --git a/samples/Correlation.Samples/Program.cs b/samples/Correlation.Samples/Program.cs new file mode 100644 index 000000000..4175e30ee --- /dev/null +++ b/samples/Correlation.Samples/Program.cs @@ -0,0 +1,41 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using DurableTask.Core.Settings; + + public class Program + { + static void Main(string[] args) + { + CorrelationSettings.Current.EnableDistributedTracing = true; + // InvokeScenario(typeof(HelloOrchestrator), "50", 50); // HelloWorldScenario.cs; + // InvokeScenario(typeof(SubOrchestratorOrchestration), "SubOrchestrationWorld", 50); // SubOrchestratorScenario.cs; + // InvokeScenario(typeof(RetryOrchestration), "Retry Scenario", 50); // RetryScenario.cs; + InvokeScenario(typeof(MultiLayeredOrchestrationWithRetryOrchestrator), "world", 50); // MultiLayerOrchestrationWithRetryScenario.cs; + // InvokeScenario(typeof(FanOutFanInOrchestrator), "50", 50); // FanOutFanInScenario.cs; + // InvokeScenario(typeof(ContinueAsNewOrchestration), "50", 50); // ContinueAsNewScenario.cs; + // InvokeScenario(typeof(TerminatedOrchestration), "50", 50); // TerminationScenario.cs; + + Console.WriteLine("Orchestration is successfully finished."); + Console.ReadLine(); + } + + static void InvokeScenario(Type orchestratorType, object orchestratorInput, int timeoutSec) + { + new ScenarioInvoker().ExecuteAsync(orchestratorType, orchestratorInput, timeoutSec).GetAwaiter().GetResult(); + } + } +} diff --git a/samples/Correlation.Samples/Properties/launchSettings.json b/samples/Correlation.Samples/Properties/launchSettings.json new file mode 100644 index 000000000..0bf6935aa --- /dev/null +++ b/samples/Correlation.Samples/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Correlation.Samples": { + "commandName": "Project", + "environmentVariables": { + "CorrelationProtocol": "W3C" + } + } + } +} \ No newline at end of file diff --git a/samples/Correlation.Samples/Readme.md b/samples/Correlation.Samples/Readme.md new file mode 100644 index 000000000..f2920c9ae --- /dev/null +++ b/samples/Correlation.Samples/Readme.md @@ -0,0 +1,22 @@ +# Distributed Tracing for Durable Task + +Distributed Tracing for Durable Task is a feature for enabling correlation propagation among orchestrations and activities. +The key features of Distributed Tracing for Durable Task are: + +- **End to End Tracing with Application Insights**: Support Complex orchestration scenario. Multi-Layered Sub Orchestration, Fan-out Fan-in, retry, Timer, and more. +- **Support Protocol**: [W3C TraceContext](https://w3c.github.io/trace-context/) and [Http Correlation Protocol](https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/HttpCorrelationProtocol.md) +- **Suppress Distributed Tracing**: No breaking change for the current implementation + +Currently, we support [DurableTask.AzureStorage](https://w3c.github.io/trace-context/). + +![Overview](docs/images/overview.png) + +# Getting Started + +If you want to try Distributed Tracing with DurableTask.AzureStorage, you can find a document with a Handful of examples. + + - [Intro](docs/getting-started.md) + +# Developing Provider + +If you want to implement Distributed Tracing for other DurableTask providers, Read [Develop Distributed Tracing](docs/overview.md). \ No newline at end of file diff --git a/samples/Correlation.Samples/RetryOrchestration.cs b/samples/Correlation.Samples/RetryOrchestration.cs new file mode 100644 index 000000000..8731b01e3 --- /dev/null +++ b/samples/Correlation.Samples/RetryOrchestration.cs @@ -0,0 +1,55 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + + [KnownType(typeof(RetryActivity))] + [KnownType(typeof(NonRetryActivity))] + internal class RetryOrchestration : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + await context.ScheduleTask(typeof(NonRetryActivity), input); + var retryOption = new RetryOptions(TimeSpan.FromMilliseconds(10), 3); + return await context.ScheduleWithRetry(typeof(RetryActivity), retryOption, input); + } + } + + internal class RetryActivity : TaskActivity + { + private static int counter = 0; + + protected override string Execute(TaskContext context, string input) + { + counter++; + if (counter == 1) throw new InvalidOperationException($"Counter = {counter}"); + + Console.WriteLine($"Retry with Activity: Hello {input}"); + return $"Retry Hello, {input}!"; + } + } + + internal class NonRetryActivity : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + Console.WriteLine($"Non-Retry with Activity: Hello {input}"); + return $"Works well. Hello, {input}!"; + } + } +} diff --git a/samples/Correlation.Samples/ScenarioInvoker.cs b/samples/Correlation.Samples/ScenarioInvoker.cs new file mode 100644 index 000000000..cf1371c76 --- /dev/null +++ b/samples/Correlation.Samples/ScenarioInvoker.cs @@ -0,0 +1,60 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Diagnostics; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Core.Settings; + using Microsoft.ApplicationInsights.W3C; + + public class ScenarioInvoker + { + public async Task ExecuteAsync(Type orchestratorType, object orchestratorInput, int timeoutSec) + { + new TelemetryActivator().Initialize(); + + using ( + TestOrchestrationHost host = TestHelpers.GetTestOrchestrationHost(false)) + { + await host.StartAsync(); + var activity = new Activity("Start Orchestration"); + SetupActivity(activity); + activity.Start(); + var client = await host.StartOrchestrationAsync(orchestratorType, orchestratorInput); // TODO The parameter null will throw exception. (for the experiment) + var status = await client.WaitForCompletionAsync(TimeSpan.FromSeconds(timeoutSec)); + + await host.StopAsync(); + } + } + + void SetupActivity(Activity activity) + { + var protocol = Environment.GetEnvironmentVariable("CorrelationProtocol"); + switch (protocol) + { + case "HTTP": + CorrelationSettings.Current.Protocol = Protocol.HttpCorrelationProtocol; + activity.SetIdFormat(ActivityIdFormat.Hierarchical); + return; + default: + CorrelationSettings.Current.Protocol = Protocol.W3CTraceContext; + activity.SetIdFormat(ActivityIdFormat.W3C); + return; + } + } + } +} diff --git a/samples/Correlation.Samples/StringExtensions.cs b/samples/Correlation.Samples/StringExtensions.cs new file mode 100644 index 000000000..c4a611c41 --- /dev/null +++ b/samples/Correlation.Samples/StringExtensions.cs @@ -0,0 +1,45 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Collections.Generic; + using System.Text; + + public static class StringExtensions + { + public static TraceParent ToTraceParent(this string traceparent) + { + if (!string.IsNullOrEmpty(traceparent)) + { + var substrings = traceparent.Split('-'); + if (substrings.Length != 4) + { + throw new ArgumentException($"Traceparent doesn't respect the spec. {traceparent}"); + } + + return new TraceParent + { + Version = substrings[0], + TraceId = substrings[1], + SpanId = substrings[2], + TraceFlags = substrings[3] + }; + } + + return null; + } + } + +} diff --git a/samples/Correlation.Samples/SubOrchestratorOrchestration.cs b/samples/Correlation.Samples/SubOrchestratorOrchestration.cs new file mode 100644 index 000000000..546cb526a --- /dev/null +++ b/samples/Correlation.Samples/SubOrchestratorOrchestration.cs @@ -0,0 +1,54 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + + [KnownType(typeof(ChildOrchestration))] + [KnownType(typeof(ChildActivity))] + internal class SubOrchestratorOrchestration : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + return await context.CreateSubOrchestrationInstance(typeof(ChildOrchestration), input); + } + } + + [KnownType(typeof(ChildActivity))] + internal class ChildOrchestration : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + return await context.ScheduleTask(typeof(ChildActivity), input); + } + } + + + internal class ChildActivity : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentNullException(nameof(input)); + } + + Console.WriteLine($"SubOrchestration with Activity: Hello {input}"); + return $"Sub Orchestration Hello, {input}!"; + } + } +} diff --git a/samples/Correlation.Samples/TelemetryActivator.cs b/samples/Correlation.Samples/TelemetryActivator.cs new file mode 100644 index 000000000..176c915d6 --- /dev/null +++ b/samples/Correlation.Samples/TelemetryActivator.cs @@ -0,0 +1,79 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using DurableTask.Core; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.DependencyCollector; + using Microsoft.ApplicationInsights.Extensibility; + + public class TelemetryActivator + { + private TelemetryClient telemetryClient; + + public void Initialize() + { + SetUpTelemetryClient(); + SetUpTelemetryCallbacks(); + } + + void SetUpTelemetryCallbacks() + { + CorrelationTraceClient.SetUp( + (TraceContextBase requestTraceContext) => + { + requestTraceContext.Stop(); + + var requestTelemetry = requestTraceContext.CreateRequestTelemetry(); + telemetryClient.TrackRequest(requestTelemetry); + }, + (TraceContextBase dependencyTraceContext) => + { + dependencyTraceContext.Stop(); + var dependencyTelemetry = dependencyTraceContext.CreateDependencyTelemetry(); + telemetryClient.TrackDependency(dependencyTelemetry); + }, + (Exception e) => + { + telemetryClient.TrackException(e); + } + ); + } + + void SetUpTelemetryClient() + { + var module = new DependencyTrackingTelemetryModule(); + // Currently it seems have a problem https://github.com/microsoft/ApplicationInsights-dotnet-server/issues/536 + module.ExcludeComponentCorrelationHttpHeadersOnDomains.Add("core.windows.net"); + module.ExcludeComponentCorrelationHttpHeadersOnDomains.Add("127.0.0.1"); + + TelemetryConfiguration config = TelemetryConfiguration.CreateDefault(); + +#pragma warning disable CS0618 // Type or member is obsolete + var telemetryInitializer = new DurableTaskCorrelationTelemetryInitializer(); +#pragma warning restore CS0618 // Type or member is obsolete + // TODO It should be suppressed by DependencyTrackingTelemetryModule, however, it doesn't work currently. + // Once the bug is fixed, remove this settings. + telemetryInitializer.ExcludeComponentCorrelationHttpHeadersOnDomains.Add("127.0.0.1"); + config.TelemetryInitializers.Add(telemetryInitializer); + + config.InstrumentationKey = Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY"); + + module.Initialize(config); + + telemetryClient = new TelemetryClient(config); + } + } +} diff --git a/samples/Correlation.Samples/TerminatedOrchestration.cs b/samples/Correlation.Samples/TerminatedOrchestration.cs new file mode 100644 index 000000000..3e26af0e1 --- /dev/null +++ b/samples/Correlation.Samples/TerminatedOrchestration.cs @@ -0,0 +1,45 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + + [KnownType(typeof(WaitActivity))] + internal class TerminatedOrchestration : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + return await context.ScheduleTask(typeof(WaitActivity), ""); + } + } + + internal class WaitActivity : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + return input; + } + + protected override async Task ExecuteAsync(TaskContext context, string input) { + // Wait for 5 min for terminate. + await Task.Delay(TimeSpan.FromMinutes(2)); + + Console.WriteLine($"Activity: Hello {input}"); + return $"Hello, {input}!"; + } + } +} diff --git a/samples/Correlation.Samples/TestHelpers.cs b/samples/Correlation.Samples/TestHelpers.cs new file mode 100644 index 000000000..4a228c21c --- /dev/null +++ b/samples/Correlation.Samples/TestHelpers.cs @@ -0,0 +1,73 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using DurableTask.AzureStorage; + using System.Configuration; + using System.IO; + using Microsoft.Extensions.Configuration; + + public static class TestHelpers + { + public static IConfigurationRoot Configuration { get; set; } + + static TestHelpers() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json"); + Configuration = builder.Build(); + } + + internal static TestOrchestrationHost GetTestOrchestrationHost( + bool enableExtendedSessions, + int extendedSessionTimeoutInSeconds = 30) + { + string storageConnectionString = GetTestStorageAccountConnectionString(); + + var settings = new AzureStorageOrchestrationServiceSettings + { + StorageConnectionString = storageConnectionString, + TaskHubName = Configuration["taskHubName"], + ExtendedSessionsEnabled = enableExtendedSessions, + ExtendedSessionIdleTimeout = TimeSpan.FromSeconds(extendedSessionTimeoutInSeconds), + }; + + return new TestOrchestrationHost(settings); + } + + public static string GetTestStorageAccountConnectionString() + { + string storageConnectionString = GetTestSetting("StorageConnectionString"); + if (string.IsNullOrEmpty(storageConnectionString)) + { + throw new ArgumentNullException("A Storage connection string must be defined in either an environment variable or in appsettings.json."); + } + + return storageConnectionString; + } + + static string GetTestSetting(string name) + { + string value = Environment.GetEnvironmentVariable("DurableTaskTest" + name); + if (string.IsNullOrEmpty(value)) + { + value = Configuration[name]; + } + + return value; + } + } +} diff --git a/samples/Correlation.Samples/TestOrchestrationClient.cs b/samples/Correlation.Samples/TestOrchestrationClient.cs new file mode 100644 index 000000000..05ee2c8ef --- /dev/null +++ b/samples/Correlation.Samples/TestOrchestrationClient.cs @@ -0,0 +1,191 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading.Tasks; + using DurableTask.AzureStorage; + using DurableTask.Core; + using DurableTask.Core.History; + //using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; + + internal class TestOrchestrationClient + { + readonly TaskHubClient client; + readonly Type orchestrationType; + readonly string instanceId; + readonly DateTime instanceCreationTime; + + public TestOrchestrationClient( + TaskHubClient client, + Type orchestrationType, + string instanceId, + DateTime instanceCreationTime) + { + this.client = client; + this.orchestrationType = orchestrationType; + this.instanceId = instanceId; + this.instanceCreationTime = instanceCreationTime; + } + + public string InstanceId => this.instanceId; + + public async Task WaitForCompletionAsync(TimeSpan timeout) + { + timeout = AdjustTimeout(timeout); + + var latestGeneration = new OrchestrationInstance { InstanceId = this.instanceId }; + Stopwatch sw = Stopwatch.StartNew(); + OrchestrationState state = await this.client.WaitForOrchestrationAsync(latestGeneration, timeout); + if (state != null) + { + Trace.TraceInformation( + "{0} (ID = {1}) completed after ~{2}ms. Status = {3}. Output = {4}.", + this.orchestrationType.Name, + state.OrchestrationInstance.InstanceId, + sw.ElapsedMilliseconds, + state.OrchestrationStatus, + state.Output); + } + else + { + Trace.TraceWarning( + "{0} (ID = {1}) failed to complete after {2}ms.", + this.orchestrationType.Name, + this.instanceId, + timeout.TotalMilliseconds); + } + + return state; + } + + internal async Task WaitForStartupAsync(TimeSpan timeout) + { + timeout = AdjustTimeout(timeout); + + Stopwatch sw = Stopwatch.StartNew(); + do + { + OrchestrationState state = await this.GetStatusAsync(); + if (state != null && state.OrchestrationStatus != OrchestrationStatus.Pending) + { + Trace.TraceInformation($"{state.Name} (ID = {state.OrchestrationInstance.InstanceId}) started successfully after ~{sw.ElapsedMilliseconds}ms. Status = {state.OrchestrationStatus}."); + return state; + } + + await Task.Delay(TimeSpan.FromSeconds(1)); + } while (sw.Elapsed < timeout); + + throw new TimeoutException($"Orchestration '{this.orchestrationType.Name}' with instance ID '{this.instanceId}' failed to start."); + } + + public async Task GetStatusAsync() + { + OrchestrationState state = await this.client.GetOrchestrationStateAsync(this.instanceId); + + if (state != null) + { + // Validate the status before returning + //Assert.AreEqual(this.orchestrationType.FullName, state.Name); + //Assert.AreEqual(this.instanceId, state.OrchestrationInstance.InstanceId); + //Assert.IsTrue(state.CreatedTime >= this.instanceCreationTime); + //Assert.IsTrue(state.CreatedTime <= DateTime.UtcNow); + //Assert.IsTrue(state.LastUpdatedTime >= state.CreatedTime); + //Assert.IsTrue(state.LastUpdatedTime <= DateTime.UtcNow); + } + + return state; + } + + public Task RaiseEventAsync(string eventName, object eventData) + { + Trace.TraceInformation($"Raising event to instance {this.instanceId} with name = {eventName}."); + + var instance = new OrchestrationInstance { InstanceId = this.instanceId }; + return this.client.RaiseEventAsync(instance, eventName, eventData); + } + + public Task TerminateAsync(string reason) + { + Trace.TraceInformation($"Terminating instance {this.instanceId} with reason = {reason}."); + + var instance = new OrchestrationInstance { InstanceId = this.instanceId }; + return this.client.TerminateInstanceAsync(instance, reason); + } + + public Task RewindAsync(string reason) + { + Trace.TraceInformation($"Rewinding instance {this.instanceId} with reason = {reason}."); + + // The Rewind API currently only exists in the service object + var service = (AzureStorageOrchestrationService)this.client.ServiceClient; + return service.RewindTaskOrchestrationAsync(this.instanceId, reason); + } + + public Task PurgeInstanceHistory() + { + Trace.TraceInformation($"Purging history for instance with id - {this.instanceId}"); + + // The Purge Instance History API only exists in the service object + var service = (AzureStorageOrchestrationService)this.client.ServiceClient; + return service.PurgeInstanceHistoryAsync(this.instanceId); + } + + public Task PurgeInstanceHistoryByTimePeriod(DateTime createdTimeFrom, DateTime? createdTimeTo, IEnumerable runtimeStatus) + { + Trace.TraceInformation($"Purging history from {createdTimeFrom} to {createdTimeTo}"); + + // The Purge Instance History API only exists in the service object + var service = (AzureStorageOrchestrationService)this.client.ServiceClient; + return service.PurgeInstanceHistoryAsync(createdTimeFrom, createdTimeTo, runtimeStatus); + } + + public async Task> GetOrchestrationHistoryAsync(string instanceId) + { + Trace.TraceInformation($"Getting history for instance with id - {this.instanceId}"); + + // GetOrchestrationHistoryAsync is exposed in the TaskHubClinet but requires execution id. + // However, we need to get all the history records for an instance id not for specific execution. + var service = (AzureStorageOrchestrationService)this.client.ServiceClient; + string historyString = await service.GetOrchestrationHistoryAsync(instanceId, null); + return JsonConvert.DeserializeObject>(historyString); + } + + public async Task> GetStateAsync(string instanceId) + { + Trace.TraceInformation($"Getting orchestration state with instance id - {this.instanceId}"); + // The GetStateAsync only exists in the service object + var service = (AzureStorageOrchestrationService)this.client.ServiceClient; + return await service.GetOrchestrationStateAsync(instanceId, true); + } + + static TimeSpan AdjustTimeout(TimeSpan requestedTimeout) + { + TimeSpan timeout = requestedTimeout; + if (Debugger.IsAttached) + { + TimeSpan debuggingTimeout = TimeSpan.FromMinutes(5); + if (debuggingTimeout > timeout) + { + timeout = debuggingTimeout; + } + } + + return timeout; + } + } +} diff --git a/samples/Correlation.Samples/TestOrchestrationHost.cs b/samples/Correlation.Samples/TestOrchestrationHost.cs new file mode 100644 index 000000000..11124627b --- /dev/null +++ b/samples/Correlation.Samples/TestOrchestrationHost.cs @@ -0,0 +1,121 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.AzureStorage; + using DurableTask.Core; + + internal sealed class TestOrchestrationHost : IDisposable + { + readonly AzureStorageOrchestrationServiceSettings settings; + readonly TaskHubWorker worker; + readonly TaskHubClient client; + readonly HashSet addedOrchestrationTypes; + readonly HashSet addedActivityTypes; + + public TestOrchestrationHost(AzureStorageOrchestrationServiceSettings settings) + { + try + { + var service = new AzureStorageOrchestrationService(settings); + service.CreateAsync().GetAwaiter().GetResult(); // I change Create to CreateIfNotExistsAsync for enabling execute without fail once per twice. + + this.settings = settings; + + this.worker = new TaskHubWorker(service); + this.client = new TaskHubClient(service); + this.addedOrchestrationTypes = new HashSet(); + this.addedActivityTypes = new HashSet(); + } + catch (Exception e) + { + throw e; + } + } + + public string TaskHub => this.settings.TaskHubName; + + public void Dispose() + { + this.worker.Dispose(); + } + + public Task StartAsync() + { + return this.worker.StartAsync(); + } + + public Task StopAsync() + { + return this.worker.StopAsync(isForced: true); + } + + public async Task StartOrchestrationAsync( + Type orchestrationType, + object input, + string instanceId = null) + { + if (!this.addedOrchestrationTypes.Contains(orchestrationType)) + { + this.worker.AddTaskOrchestrations(orchestrationType); + this.addedOrchestrationTypes.Add(orchestrationType); + } + + // Allow orchestration types to declare which activity types they depend on. + // CONSIDER: Make this a supported pattern in DTFx? + KnownTypeAttribute[] knownTypes = + (KnownTypeAttribute[])orchestrationType.GetCustomAttributes(typeof(KnownTypeAttribute), false); + + foreach (KnownTypeAttribute referencedKnownType in knownTypes) + { + bool orch = referencedKnownType.Type.IsSubclassOf(typeof(TaskOrchestration)); + bool activ = referencedKnownType.Type.IsSubclassOf(typeof(TaskActivity)); + if (orch && !this.addedOrchestrationTypes.Contains(referencedKnownType.Type)) + { + this.worker.AddTaskOrchestrations(referencedKnownType.Type); + this.addedOrchestrationTypes.Add(referencedKnownType.Type); + } + + else if (activ && !this.addedActivityTypes.Contains(referencedKnownType.Type)) + { + this.worker.AddTaskActivities(referencedKnownType.Type); + this.addedActivityTypes.Add(referencedKnownType.Type); + } + } + + DateTime creationTime = DateTime.UtcNow; + OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync( + orchestrationType, + instanceId, + input); + + Trace.TraceInformation($"Started {orchestrationType.Name}, Instance ID = {instance.InstanceId}"); + return new TestOrchestrationClient(this.client, orchestrationType, instance.InstanceId, creationTime); + } + + public async Task> GetAllOrchestrationInstancesAsync() + { + // This API currently only exists in the service object and is not yet exposed on the TaskHubClient + var service = (AzureStorageOrchestrationService)this.client.ServiceClient; + IList instances = await service.GetOrchestrationStateAsync(); + Trace.TraceInformation($"Found {instances.Count} in the task hub instance store."); + return instances; + } + } +} diff --git a/samples/Correlation.Samples/TraceContextBaseExtensions.cs b/samples/Correlation.Samples/TraceContextBaseExtensions.cs new file mode 100644 index 000000000..c8e2d6bae --- /dev/null +++ b/samples/Correlation.Samples/TraceContextBaseExtensions.cs @@ -0,0 +1,60 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + using System.Diagnostics; + using DurableTask.Core; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.DataContracts; + + public static class TraceContextBaseExtensions + { + /// + /// Create RequestTelemetry from the TraceContext + /// Currently W3C Trace contextBase is supported. + /// + /// TraceContext + /// + public static RequestTelemetry CreateRequestTelemetry(this TraceContextBase context) + { + var telemetry = new RequestTelemetry { Name = context.OperationName }; + telemetry.Duration = context.Duration; + telemetry.Timestamp = context.StartTime; + telemetry.Id = context.TelemetryId; + telemetry.Context.Operation.Id = context.TelemetryContextOperationId; + telemetry.Context.Operation.ParentId = context.TelemetryContextOperationParentId; + + return telemetry; + } + + /// + /// Create DependencyTelemetry from the Activity. + /// Currently W3C Trace contextBase is supported. + /// + /// TraceContext + /// + public static DependencyTelemetry CreateDependencyTelemetry(this TraceContextBase context) + { + var telemetry = new DependencyTelemetry { Name = context.OperationName }; + telemetry.Start(); // TODO Check if it is necessary. + telemetry.Duration = context.Duration; + telemetry.Timestamp = context.StartTime; // TimeStamp is the time of ending the Activity. + telemetry.Id = context.TelemetryId; + telemetry.Context.Operation.Id = context.TelemetryContextOperationId; + telemetry.Context.Operation.ParentId = context.TelemetryContextOperationParentId; + + return telemetry; + } + } +} diff --git a/samples/Correlation.Samples/Traceparent.cs b/samples/Correlation.Samples/Traceparent.cs new file mode 100644 index 000000000..5543fdee4 --- /dev/null +++ b/samples/Correlation.Samples/Traceparent.cs @@ -0,0 +1,26 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Correlation.Samples +{ + public class TraceParent + { + public string Version { get; set; } + + public string TraceId { get; set; } + + public string SpanId { get; set; } + + public string TraceFlags { get; set; } + } +} \ No newline at end of file diff --git a/samples/Correlation.Samples/appsettings.json b/samples/Correlation.Samples/appsettings.json new file mode 100644 index 000000000..50c3c2427 --- /dev/null +++ b/samples/Correlation.Samples/appsettings.json @@ -0,0 +1,4 @@ +{ + "StorageConnectionString": "UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://127.0.0.1:10002/", + "taskHubName":"SamplesHub" +} \ No newline at end of file diff --git a/samples/Correlation.Samples/docs/getting-started.md b/samples/Correlation.Samples/docs/getting-started.md new file mode 100644 index 000000000..8f5256a83 --- /dev/null +++ b/samples/Correlation.Samples/docs/getting-started.md @@ -0,0 +1,65 @@ +# Getting Started + +In this tutorial, you can configure and execute a distributed tracing sample app with several scenarios. + +## Prerequisite + +The sample application requires these tools. If you don't have it, please click the following link and install it or create it on your Azure subscription. + +- [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/) +- [Storage Emulator 5.9+](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-emulator) +- [Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/create-new-resource) +- [Azure Subscription](https://azure.microsoft.com/en-us/) + +## Configration + +### Get Application Insights InstrumentKey + +Go to Azure Portal. Go to the Application Insights resource, click `Overview.` Copy the `Instrumentation Key` then set the key to the `APPINSIGHTS_INSTRUMENTATIONKEY.` + +### DurableTask Settings + +Start the [Storage Emulator](https://docs.microsoft.com/en-us/azure/storage/common/storage-use-emulator) Then Configure the Environment Variable `DurableTaskTestStorageConnectionString` as `UseDevelopmentStorage=true`. + +### Protocol Settings + +You can choose a correlation protocol. `W3C` for W3C TraceContext. `HTTP` for HTTP Correlation Protocol. Application Insights support the HTTP Correlation Protocol at first. They are now moving toward W3C TraceContext. You can configure `CorrelationProtocol` Environment Variables for the execution sample. + +| Key | Value | +| ---- | ---------| +| APPINSIGHTS_INSTRUMENTATIONKEY | Your Application Insights InstrumentKey | +| DurableTaskTestStorageConnectionString | UseDevelopmentStorage=true | +| CorrelationProtocol | HTTP or W3C | + +### Choose Sample Scenario + +Go to [Program.cs](../Program.cs) You will find a lot of comments for scenario. Enable one then execute `Correlation.Samples` Project. The Scenario starts orchestration and finishes in several minutes. The sample scenario is listed below. For more detail, Find the `.cs` file. It includes code for the orchestration. + +| Scenario | Description | +| -------- | ----------- | +| HelloOrchestrator | A simple orchestration. One Orchestrator with One Activity | +| SubOrchestratorOrchestration | An Orchestrator has a sub orchestrator with an Activity | +| RetryOrchestration | Retry Activity execution | +| MultiLayeredOrchestrationWithRetryOrchestrator | Multilayer sub orchestration scenario with retry with sub orchestrator | +| FanOutFanInOrchestrator | Fan-out, Fan-in scenario | +| ContinueAsNewOrchestration | Continue as new scenario | +| TerminatedOrchestration | Terminated scenario | + +**NOTE:** Termination scenario will fail to correlation. + +### Run scenario + +Run the `Correlation.Samples` project. It executes orchestration. Then you can see the `Orchestration is successfully finished.` message on the console. Then You can stop the application. Wait around 5 minutes. + +### See EndToEnd Tracing + +Go to your Azure Portal, then go to your Application Insights resource. +Click `Search` on your left list. Filter it with `Last 30 minutes` and `Event types: Request.` You can see the `Start Orchestration` request. Click it. + +![Search](images/search.png) + +Then You can see end-to-end tracing. Click and see how it correlates with each other. + +![End To End Tracing](images/end-to-end.png) + +**NOTE:** When you see the correlation breaks, you might wait for a while. The request telemetry of the first orchestrator comes last. diff --git a/samples/Correlation.Samples/docs/images/class-diagram.png b/samples/Correlation.Samples/docs/images/class-diagram.png new file mode 100644 index 000000000..4867f86fc Binary files /dev/null and b/samples/Correlation.Samples/docs/images/class-diagram.png differ diff --git a/samples/Correlation.Samples/docs/images/end-to-end.png b/samples/Correlation.Samples/docs/images/end-to-end.png new file mode 100644 index 000000000..83dd9ce82 Binary files /dev/null and b/samples/Correlation.Samples/docs/images/end-to-end.png differ diff --git a/samples/Correlation.Samples/docs/images/overview.png b/samples/Correlation.Samples/docs/images/overview.png new file mode 100644 index 000000000..707ec24f7 Binary files /dev/null and b/samples/Correlation.Samples/docs/images/overview.png differ diff --git a/samples/Correlation.Samples/docs/images/search.png b/samples/Correlation.Samples/docs/images/search.png new file mode 100644 index 000000000..9534b86c2 Binary files /dev/null and b/samples/Correlation.Samples/docs/images/search.png differ diff --git a/samples/Correlation.Samples/docs/images/telemetry-tracking.png b/samples/Correlation.Samples/docs/images/telemetry-tracking.png new file mode 100644 index 000000000..985d3fd2c Binary files /dev/null and b/samples/Correlation.Samples/docs/images/telemetry-tracking.png differ diff --git a/samples/Correlation.Samples/docs/overview.md b/samples/Correlation.Samples/docs/overview.md new file mode 100644 index 000000000..c234245d2 --- /dev/null +++ b/samples/Correlation.Samples/docs/overview.md @@ -0,0 +1,210 @@ +# Develop Distributed Tracing + +**Distributed Tracing for DurableTask** provides libraries for developers to implement Distributed Tracing for DurableTask providers. Currently, we implement `DurableTask.AzureStorage`. This document helps to understand the implementation and how to implement Distributed Tracing for other providers. + +This document explains: + +* [Prerequisite](#Prerequisite) +* [Architecture](#Architecture) +* [Class library](#Class-library) +* [Configuration](#Configuration) +* [Client implementation](#Client-implementation) +* [Extension points](#Extension-points) +* [Scenario testing](#Scenario-testing) +* [Sample](#Sample) + +## Prerequisite + +If you are new to the Distributed Tracing with Application Insights, you can refer to: + +- [Correlation with Activity with Application Insights (1 - 3)](https://medium.com/@tsuyoshiushio/correlation-with-activity-with-application-insights-1-overview-753a48a645fb) + +It also includes useful links. We assume you have enough knowledge of `DurableTask` repo. + +## Architecture + +`TaskHubClient` receives [Activity](https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md) then tracks the request, then sends `TraceContext` to the queue. The queue has serialized `TraceContext` correlation object. Then track the dependency. The orchestrator `foo` starts. The orchestrator creates a new `TraceContext` that has a parent trace context in the queue. Then the program pushes the trace context to the stack of TraceContext. The sub orchestration starts. It also creates a new `TraceContext` that has parent trace context on the queue, then pushes the trace context to the stack. Once execution of the Activity has finished, Sub orchestrator pop the `TraceContext` from the stack then track Request/Dependency telemetry. Same as the parent orchestrator. +The stack represents the current TraceContext of the orchestrator. + +![Telemetry Tracking and Queue](images/telemetry-tracking.png) + +## Class library + +![Class diagram](images/class-diagram.png) + +### [TraceContextBase](../../../src/DurableTask.Core/TraceContextBase.cs) + +A wrapper of [Activity](https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md) class. The Activity class is in charge of handling the correlation information of Application Insights. Activity is designed for in-memory execution. However, Orchestrator requires a replay. The execution is not in-memory. This class wraps the Activity to adopt orchestration execution. This class has a Stack called `OrchestrationTraceContexts.` It is a stack of request/dependency telemetry of orchestrator. + +This class is serialized to queues. However, the default `Newtonsoft.Json` serializer can't support it. So we have a custom serializer on the `TraceContextBase` class. + +### [CorrelationTraceClient](../../../src/DurableTask.Core/CorrelationTraceClient.cs) + +Client for Tracking Telemetry. The responsibility is to delegate sending Telemetry to Application Insights to `TelemetryActivator.` (See the detail later). The namespace is DurableTask.Core package. It doesn't have a dependency on the Application Insights library. It only depends on the `System.Diagnostic.Activity`. + +### [CorrelationTraceContext](../../../src/DurableTask.Core/CorrelationTraceContext.cs) + +Share the TraceContext on the AsyncLocal scope. It helps to pass the TraceContext to the other classes without passing as a parameter. It works both on .netcore and .netframework. + +### [CorrelationSettings](../../../src/DurableTask.Core/Settings/CorrelationSettings.cs) + +Configuration class for Distributed Tracing for DurableTask + +### [TelemetryActivator](../TelemetryActivator.cs) + +TelemetryActivator has a responsibility to track telemetry. Work with CorrelationTraceClient with giving Lambda to activate Application Insights. This class is a client-side implementation. So this class is **NOT** included in DurableTask namespaces. + +### [DurableTaskCorrelationTelemetryInitializer](../DurableTaskCorrelationTelemetryInitializer.cs) + +This telemetry Initializer tracks Dependency Telemetry automatically. This initializer support TraceContextBase. This class is **NOT** included in DurableTask namespaces. + +## Configuration + +You can configure this feature with the `CorrelationSettings` class. + +| Property | Description | Example | Default | +| -------- | ----------- | ------- | ------- | +| EnableDistributedTracing | Set true if you need this feature | true or false | false | +| Protocol | Correlation Protocol | W3CTraceContext or HttpCorrelation Protocol | W3CTraceContext | + +By default, `EnableDistributedTracing=false` which means this feature is suppressed. No additional code is executed; also, no propagation happens in queues. + +## Client implementation + +If you want to use this feature with your client (for example, Durable Functions), you need to develop `TelemetryActivator` and `DurableTaskDependencyTelemetryInitializer.` However, this feature already includes the sample implementation that you can use. + +## Extension points + +A provider needs to add Correlation code if you want to have this feature. `DurableTask.AzureStorage` Already has an implementation. I'll share the extension points of `DurableTask.AzureStorage` implementation. All implementation is executed as a lambda expression. The Lambda is never executed if you set EnableDistributedTracing as false. You can search for this method to find the additional extension point. + +```csharp +CorrelationTraceClient.Propagate( + () => { + data.SerializableTraceContext = GetSerializableTraceContext(taskMessage); + }); +``` + +### [AzureStorageOrchestrationService](../../../src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs) ([IOrchestrationService](../../../src/DurableTask.Core/IOrchestrationService.cs)) class + +#### LockNextTaskOrchestrationWorkItemAsync +Receive a work item for orchestration and process it. + +Correlation responsibilities: + +- Create Request TraceContext (First time) +- Add the Request TraceContext to the Stack +- Restore Current Orchestration Request TraceContext (Replay) +- Add the Request TraceContext to a WorkItem. +- Track Dependency Telemetry +- Pop Dependency Telemetry once Tracked + + +#### CompleteTaskOrchestrationWorkItemAsync +Receive a work item after the `LockNextTaskOrchestrationWorkItemAsync`, Correlation Library is doing: + +- Crete Dependency TraceContext from a Work Item +- Add the Dependency TraceContext to the Stack +- Set CorrelationTraceContext.Current = Dependency TraceContext +- Track Request Telemetry when the orchestration is finished. +- Track Dependency Telemetry when the orchestration is finished and workItem.isExtendedSession is true. +- Pop Request Telemetry once Tracked + +Why do we need to set the Dependency TraceContext to Collection TraceContext.Current? `TaskHubQueue` get the TraceContext and send it with a queue message. + +#### LockNextTaskActivitiyWorkItemAsync +Receive a work item for Activity. + +The correlation responsibility: + +- Create Request TraceContext + +#### CompleteTaskActivityWorkItemAsync +Receive a work item after `LockNextTaskActivityWorkItemAsync`. + +The correlation responsibilities: + +- Send Request Telemetry to Application Insights +- Set Request TraceContext to `CorrelationTraceContext.Current` to pass the telemetry to the `TaskHubQueue.` + + +### [TaskHubQueue](../../../src/DurableTask.AzureStorage/Messaging/TaskHubQueue.cs) class + +#### AddMessageAsync +Inject TraceContext to the queue message. + +- Get TraceContext from `CorrelationTraceContext.Current` +- If `CorrelationTraceConext.GenerateDependencyTracking` is true, Generate a Dependency TraceContext and push it tht the Stack. +- Add TraceContext to the queue message. + +### [TaskHubClient](../../../src/DurableTask.Core/TaskHubClient.cs) class + +#### InternalCreateOrchestrationInstanceWithRaisedEventAsync +Method for starting orchestration. + +The correlation responsibilities: + +- Create Request from Activity. Current as a parent +- Generate Activity with Request TraceContext if it Activity. Current is not exist +- Create Dependency Telemetry for staring orchestration +- Track Request/Dependency telemetry +- Set the Dependency Telemetry to the `CorrelationTraceContext.Current` to pass the telemetry to `TaskHubQueue.` + +### [TaskActivityDispatcher](../../../src/DurableTask.Core/TaskActivityDispatcher.cs) class + +#### OnProcessWorkItemAsync (2 points) +- Set Activity.Current. From the Activity, users can get the correlation info through Activity. Current +- Track Exception Telemetry + + +### [TaskOrchestrationDispatcher](../../../src/DurableTask.Core/TaskOrchestrationDispatcher.cs) class + +#### OnProcessWorkItemAsync +- Set `CorrelationTraceContext.Current`. + +It enable us to get TraceContext from `CorrelationTraceContext.Current` on it's call graph. + +### OnProcessWorkItemSessionAsync + +- Set WorkItem.isExtendedSession + +## Scenario testing +Find automated scenario testing and testing framework. Read the [CorrelationScenarioTest.cs](../../Test/DurableTask.AzureStorage.Tests/Correlation/CorrelationScenarioTest.cs). + +The following code is an example of the testing framework. It tests both protocols. Then starts `TestCorrelationOrchestrationHost` with `ExecuteOrchestrationAsync` method. The parameter is the type of target orchestrator class, parameter, and timeout). The method starts the orchestrator, then track the telemetry, then sort it according to the parent/child relationship. +Then assert if the telemetry order is correct or not. If you have a scenario of tracking exception, you can use the `ExecuteOrchestrationWithException` method instead. + +```csharp +[DataTestMethod] +[DataRow(Protocol.HttpCorrelationProtocol)] +[DataRow(Protocol.W3CTraceContext)] +public async Task SingleOrchestratorWithSingleActivityAsync(Protocol protocol) +{ + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + List actual = await host.ExecuteOrchestrationAsync(typeof(SayHelloOrchestrator), "world", 360); + Assert.AreEqual(5, actual.Count); + + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} SayHelloOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello") + }, + actual.Select(x => (x.GetType(), x.Name)).ToList() + ); +} +``` + +## Sample + +You can execute the sample. See [Getting Started](getting-started.md). In this sample, you can learn the implementation and configuration of this library and what is the actual End to end tracking looks like on various scenarios using Application Insights. Start reading code from [Program.cs](../Program.cs). + +Recommend to read: + +- Configuration [Program.cs](../Program.cs), [ScenarioInvoker.cs](../ScenarioInvoker.cs) +- Implementation of [TelemetryActivator.cs](../TelemetryActivator.cs) +- Implementation of [DurableTaskCorrelationTelemetryInitializer.cs](../DurableTaskCorrelationTelemetryInitializer.cs) diff --git a/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs b/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs index bbf5826b9..be0cedee3 100644 --- a/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs +++ b/src/DurableTask.AzureStorage/AzureStorageOrchestrationService.cs @@ -16,6 +16,7 @@ namespace DurableTask.AzureStorage using System; using System.Collections.Concurrent; using System.Collections.Generic; + using System.Diagnostics; using System.Diagnostics.Tracing; using System.Linq; using System.Net; @@ -43,6 +44,7 @@ public sealed class AzureStorageOrchestrationService : IDisposable { static readonly HistoryEvent[] EmptyHistoryEventList = new HistoryEvent[0]; + static readonly OrchestrationInstance EmptySourceInstance = new OrchestrationInstance { InstanceId = string.Empty, @@ -667,6 +669,7 @@ static TaskHubInfo GetTaskHubInfo(string taskHub, int partitionCount) session.StartNewLogicalTraceScope(); List outOfOrderMessages = null; + foreach (MessageData message in session.CurrentMessageBatch) { if (session.IsOutOfOrderMessage(message)) @@ -711,6 +714,17 @@ static TaskHubInfo GetTaskHubInfo(string taskHub, int partitionCount) return null; } + // Create or restore Correlation TraceContext + + TraceContextBase currentRequestTraceContext = null; + CorrelationTraceClient.Propagate( + () => + { + var isReplaying = session.RuntimeState.ExecutionStartedEvent?.IsPlayed ?? false; + TraceContextBase parentTraceContext = GetParentTraceContext(session); + currentRequestTraceContext = GetRequestTraceContext(isReplaying, parentTraceContext); + }); + orchestrationWorkItem = new TaskOrchestrationWorkItem { InstanceId = session.Instance.InstanceId, @@ -718,6 +732,7 @@ static TaskHubInfo GetTaskHubInfo(string taskHub, int partitionCount) NewMessages = session.CurrentMessageBatch.Select(m => m.TaskMessage).ToList(), OrchestrationRuntimeState = session.RuntimeState, Session = this.settings.ExtendedSessionsEnabled ? session : null, + TraceContext = currentRequestTraceContext, }; if (!this.IsExecutableInstance(session.RuntimeState, orchestrationWorkItem.NewMessages, out string warningMessage)) @@ -808,6 +823,120 @@ static TaskHubInfo GetTaskHubInfo(string taskHub, int partitionCount) } } + TraceContextBase GetParentTraceContext(OrchestrationSession session) + { + var messages = session.CurrentMessageBatch; + TraceContextBase parentTraceContext = null; + bool foundEventRaised = false; + foreach(var message in messages) + { + if (message.SerializableTraceContext != null) + { + var traceContext = TraceContextBase.Restore(message.SerializableTraceContext); + switch(message.TaskMessage.Event) + { + // Dependency Execution finished. + case TaskCompletedEvent tc: + case TaskFailedEvent tf: + case SubOrchestrationInstanceCompletedEvent sc: + case SubOrchestrationInstanceFailedEvent sf: + if (traceContext.OrchestrationTraceContexts.Count != 0) + { + var orchestrationDependencyTraceContext = traceContext.OrchestrationTraceContexts.Pop(); + CorrelationTraceClient.TrackDepencencyTelemetry(orchestrationDependencyTraceContext); + } + + parentTraceContext = traceContext; + break; + // Retry and Timer that includes Dependency Telemetry needs to remove + case TimerFiredEvent tf: + if (traceContext.OrchestrationTraceContexts.Count != 0) + traceContext.OrchestrationTraceContexts.Pop(); + + parentTraceContext = traceContext; + break; + default: + // When internal error happens, multiple message could come, however, it should not be prioritized. + if (parentTraceContext == null || + parentTraceContext.OrchestrationTraceContexts.Count < traceContext.OrchestrationTraceContexts.Count) + { + parentTraceContext = traceContext; + } + + break; + } + } else + { + + // In this case, we set the parentTraceContext later in this method + if (message.TaskMessage.Event is EventRaisedEvent) + { + foundEventRaised = true; + } + } + } + + // When EventRaisedEvent is present, it will not, out of the box, share the same operation + // identifiers as the rest of the trace events. Thus, we need to explicitely group it with the + // rest of events by using the context string of the ExecutionStartedEvent. + if (parentTraceContext is null && foundEventRaised) + { + // Restore the parent trace context from the correlation state of the execution start event + string traceContextString = session.RuntimeState.ExecutionStartedEvent?.Correlation; + parentTraceContext = TraceContextBase.Restore(traceContextString); + } + return parentTraceContext ?? TraceContextFactory.Empty; + } + + static bool IsActivityOrOrchestrationFailedOrCompleted(IList messages) + { + foreach(var message in messages) + { + if (message.TaskMessage.Event is DurableTask.Core.History.SubOrchestrationInstanceCompletedEvent || + message.TaskMessage.Event is DurableTask.Core.History.SubOrchestrationInstanceFailedEvent || + message.TaskMessage.Event is DurableTask.Core.History.TaskCompletedEvent || + message.TaskMessage.Event is DurableTask.Core.History.TaskFailedEvent || + message.TaskMessage.Event is DurableTask.Core.History.TimerFiredEvent) + { + return true; + } + } + + return false; + } + + static TraceContextBase GetRequestTraceContext(bool isReplaying, TraceContextBase parentTraceContext) + { + TraceContextBase currentRequestTraceContext = TraceContextFactory.Empty; + + if (!isReplaying) + { + var name = $"{TraceConstants.Orchestrator}"; + currentRequestTraceContext = TraceContextFactory.Create(name); + currentRequestTraceContext.SetParentAndStart(parentTraceContext); + currentRequestTraceContext.TelemetryType = TelemetryType.Request; + currentRequestTraceContext.OrchestrationTraceContexts.Push(currentRequestTraceContext); + } + else + { + // TODO Chris said that there is not case in this root. Double check or write test to prove it. + bool noCorrelation = parentTraceContext.OrchestrationTraceContexts.Count == 0; + if (noCorrelation) + { + // Terminate, external events, etc. are examples of messages that not contain any trace context. + // In those cases, we just return an empty trace context and continue on. + return TraceContextFactory.Empty; + } + + currentRequestTraceContext = parentTraceContext.GetCurrentOrchestrationRequestTraceContext(); + currentRequestTraceContext.OrchestrationTraceContexts = parentTraceContext.OrchestrationTraceContexts.Clone(); + currentRequestTraceContext.IsReplay = true; + return currentRequestTraceContext; + } + + return currentRequestTraceContext; + } + internal static Guid StartNewLogicalTraceScope(bool useExisting) { // Starting in DurableTask.Core v2.4.0, a new trace activity will already be @@ -947,10 +1076,34 @@ async Task AbandonAndReleaseSessionAsync(OrchestrationSession session) string instanceId = workItem.InstanceId; string executionId = runtimeState.OrchestrationInstance.ExecutionId; + // Correlation + + CorrelationTraceClient.Propagate(() => + { + // In case of Extended Session, Emit the Dependency Telemetry. + if (workItem.IsExtendedSession) + { + this.TrackExtendedSessionDependencyTelemetry(session); + } + }); + + TraceContextBase currentTraceContextBaseOnComplete = null; + CorrelationTraceClient.Propagate(() => + currentTraceContextBaseOnComplete = CreateOrRestoreRequestTraceContextWithDependencyTrackingSettings( + workItem.TraceContext, + orchestrationState, + DependencyTelemetryStarted( + outboundMessages, + orchestratorMessages, + timerMessages, + continuedAsNewMessage, + orchestrationState))); + // First, add new messages into the queue. If a failure happens after this, duplicate messages will // be written after the retry, but the results of those messages are expected to be de-dup'd later. // This provider needs to ensure that response messages are not processed until the history a few // lines down has been successfully committed. + await this.CommitOutboundQueueMessages( session, outboundMessages, @@ -958,6 +1111,13 @@ async Task AbandonAndReleaseSessionAsync(OrchestrationSession session) timerMessages, continuedAsNewMessage); + // correlation + CorrelationTraceClient.Propagate(() => + this.TrackOrchestrationRequestTelemetry( + currentTraceContextBaseOnComplete, + orchestrationState, + $"{TraceConstants.Orchestrator} {Utils.GetTargetClassName(session.RuntimeState.ExecutionStartedEvent?.Name)}")); + // Next, commit the orchestration history updates. This is the actual "checkpoint". Failures after this // will result in a duplicate replay of the orchestration with no side-effects. try @@ -1004,6 +1164,112 @@ async Task AbandonAndReleaseSessionAsync(OrchestrationSession session) await this.DeleteMessageBatchAsync(session, session.CurrentMessageBatch); } + static bool DependencyTelemetryStarted( + IList outboundMessages, + IList orchestratorMessages, + IList timerMessages, + TaskMessage continuedAsNewMessage, + OrchestrationState orchestrationState) + { + return + (outboundMessages.Count != 0 || orchestratorMessages.Count != 0 || timerMessages.Count != 0) && + (orchestrationState.OrchestrationStatus != OrchestrationStatus.Completed) && + (orchestrationState.OrchestrationStatus != OrchestrationStatus.Failed); + } + + void TrackExtendedSessionDependencyTelemetry(OrchestrationSession session) + { + List messages = session.CurrentMessageBatch; + foreach (MessageData message in messages) + { + if (message.SerializableTraceContext != null) + { + var traceContext = TraceContextBase.Restore(message.SerializableTraceContext); + switch (message.TaskMessage.Event) + { + // Dependency Execution finished. + case TaskCompletedEvent tc: + case TaskFailedEvent tf: + case SubOrchestrationInstanceCompletedEvent sc: + case SubOrchestrationInstanceFailedEvent sf: + if (traceContext.OrchestrationTraceContexts.Count != 0) + { + TraceContextBase orchestrationDependencyTraceContext = traceContext.OrchestrationTraceContexts.Pop(); + CorrelationTraceClient.TrackDepencencyTelemetry(orchestrationDependencyTraceContext); + } + + break; + default: + // When internal error happens, multiple message could come, however, it should not be prioritized. + break; + } + } + } + } + + static TraceContextBase CreateOrRestoreRequestTraceContextWithDependencyTrackingSettings( + TraceContextBase traceContext, + OrchestrationState orchestrationState, + bool dependencyTelemetryStarted) + { + TraceContextBase currentTraceContextBaseOnComplete = null; + + if (dependencyTelemetryStarted) + { + // DependencyTelemetry will be included on an outbound queue + // See TaskHubQueue + CorrelationTraceContext.GenerateDependencyTracking = true; + CorrelationTraceContext.Current = traceContext; + } + else + { + switch(orchestrationState.OrchestrationStatus) + { + case OrchestrationStatus.Completed: + case OrchestrationStatus.Failed: + // Completion of the orchestration. + TraceContextBase parentTraceContext = traceContext; + if (parentTraceContext.OrchestrationTraceContexts.Count >= 1) + { + currentTraceContextBaseOnComplete = parentTraceContext.OrchestrationTraceContexts.Pop(); + CorrelationTraceContext.Current = parentTraceContext; + } + else + { + currentTraceContextBaseOnComplete = TraceContextFactory.Empty; + } + + break; + default: + currentTraceContextBaseOnComplete = TraceContextFactory.Empty; + break; + } + } + + return currentTraceContextBaseOnComplete; + } + + void TrackOrchestrationRequestTelemetry( + TraceContextBase traceContext, + OrchestrationState orchestrationState, + string operationName) + { + switch (orchestrationState.OrchestrationStatus) + { + case OrchestrationStatus.Completed: + case OrchestrationStatus.Failed: + if (traceContext != null) + { + traceContext.OperationName = operationName; + CorrelationTraceClient.TrackRequestTelemetry(traceContext); + } + + break; + default: + break; + } + } + async Task CommitOutboundQueueMessages( OrchestrationSession session, IList outboundMessages, @@ -1168,9 +1434,23 @@ async Task ReleaseSessionAsync(string instanceId) return null; } + Guid traceActivityId = Guid.NewGuid(); var session = new ActivitySession(this.settings, this.storageAccountName, message, traceActivityId); session.StartNewLogicalTraceScope(); + + // correlation + TraceContextBase requestTraceContext = null; + CorrelationTraceClient.Propagate( + () => + { + string name = $"{TraceConstants.Activity} {Utils.GetTargetClassName(((TaskScheduledEvent)session.MessageData.TaskMessage.Event)?.Name)}"; + requestTraceContext = TraceContextFactory.Create(name); + + TraceContextBase parentTraceContextBase = TraceContextBase.Restore(session.MessageData.SerializableTraceContext); + requestTraceContext.SetParentAndStart(parentTraceContextBase); + }); + TraceMessageReceived(this.settings, session.MessageData, this.storageAccountName); session.TraceProcessingMessage(message, isExtendedSession: false); @@ -1192,6 +1472,8 @@ async Task ReleaseSessionAsync(string instanceId) Id = message.Id, TaskMessage = session.MessageData.TaskMessage, LockedUntilUtc = message.OriginalQueueMessage.NextVisibleTime.Value.UtcDateTime, + + TraceContextBase = requestTraceContext }; } } @@ -1211,6 +1493,10 @@ public async Task CompleteTaskActivityWorkItemAsync(TaskActivityWorkItem workIte } session.StartNewLogicalTraceScope(); + + // Correlation + CorrelationTraceClient.Propagate(() => CorrelationTraceContext.Current = workItem.TraceContextBase); + string instanceId = workItem.TaskMessage.OrchestrationInstance.InstanceId; ControlQueue controlQueue = await this.GetControlQueueAsync(instanceId); @@ -1218,6 +1504,13 @@ public async Task CompleteTaskActivityWorkItemAsync(TaskActivityWorkItem workIte // work item message yet (that happens next). await controlQueue.AddMessageAsync(responseTaskMessage, session); + // RequestTelemetryTracking + CorrelationTraceClient.Propagate( + () => + { + CorrelationTraceClient.TrackRequestTelemetry(workItem.TraceContextBase); + }); + // Next, delete the work item queue message. This must come after enqueuing the response message. await this.workItemQueue.DeleteMessageAsync(session.MessageData, session); @@ -1725,6 +2018,7 @@ public QueueMessage(TaskHubQueue queue, TaskMessage message) } public TaskHubQueue Queue { get; } + public TaskMessage Message { get; } } } diff --git a/src/DurableTask.AzureStorage/MessageData.cs b/src/DurableTask.AzureStorage/MessageData.cs index 837313d6e..8ac6ee942 100644 --- a/src/DurableTask.AzureStorage/MessageData.cs +++ b/src/DurableTask.AzureStorage/MessageData.cs @@ -88,6 +88,12 @@ public MessageData() [DataMember(EmitDefaultValue = false)] public OrchestrationInstance Sender { get; private set; } + /// + /// TraceContext for correlation. + /// + [DataMember] + public string SerializableTraceContext { get; set; } + internal string Id => this.OriginalQueueMessage?.Id; internal string QueueName { get; set; } diff --git a/src/DurableTask.AzureStorage/Messaging/TaskHubQueue.cs b/src/DurableTask.AzureStorage/Messaging/TaskHubQueue.cs index 38b6151f4..f6618a33e 100644 --- a/src/DurableTask.AzureStorage/Messaging/TaskHubQueue.cs +++ b/src/DurableTask.AzureStorage/Messaging/TaskHubQueue.cs @@ -14,6 +14,7 @@ namespace DurableTask.AzureStorage.Messaging { using System; + using System.Reflection; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; @@ -106,7 +107,12 @@ async Task AddMessageAsync(TaskMessage taskMessage, OrchestrationIn sourceInstance); data.SequenceNumber = Interlocked.Increment(ref messageSequenceNumber); + // Inject Correlation TraceContext on a queue. + CorrelationTraceClient.Propagate( + () => { data.SerializableTraceContext = GetSerializableTraceContext(taskMessage); }); + string rawContent = await this.messageManager.SerializeMessageDataAsync(data); + CloudQueueMessage queueMessage = new CloudQueueMessage(rawContent); this.settings.Logger.SendingMessage( @@ -158,6 +164,32 @@ async Task AddMessageAsync(TaskMessage taskMessage, OrchestrationIn return data; } + static string GetSerializableTraceContext(TaskMessage taskMessage) + { + TraceContextBase traceContext = CorrelationTraceContext.Current; + if (traceContext != null) + { + if (CorrelationTraceContext.GenerateDependencyTracking) + { + PropertyInfo nameProperty = taskMessage.Event.GetType().GetProperty("Name"); + string name = (nameProperty == null) ? TraceConstants.DependencyDefault : (string)nameProperty.GetValue(taskMessage.Event); + + var dependencyTraceContext = TraceContextFactory.Create($"{TraceConstants.Orchestrator} {name}"); + dependencyTraceContext.TelemetryType = TelemetryType.Dependency; + dependencyTraceContext.SetParentAndStart(traceContext); + dependencyTraceContext.OrchestrationTraceContexts.Push(dependencyTraceContext); + return dependencyTraceContext.SerializableTraceContext; + } + else + { + return traceContext.SerializableTraceContext; + } + } + + // TODO this might not happen, however, in case happen, introduce NullObjectTraceContext. + return null; + } + static TimeSpan? GetVisibilityDelay(TaskMessage taskMessage) { TimeSpan? initialVisibilityDelay = null; diff --git a/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs b/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs index 4fbb5fd76..5884a3df0 100644 --- a/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs +++ b/src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs @@ -56,6 +56,7 @@ class AzureTableTrackingStore : TrackingStoreBase OutputProperty, "Reason", "Details", + "Correlation" }; readonly string storageAccountName; @@ -952,7 +953,7 @@ public override Task StartAsync() ["LastUpdatedTime"] = new EntityProperty(newEvents.Last().Timestamp), } }; - + for (int i = 0; i < newEvents.Count; i++) { bool isFinalEvent = i == newEvents.Count - 1; @@ -986,8 +987,16 @@ public override Task StartAsync() instanceEntity.Properties["Version"] = new EntityProperty(executionStartedEvent.Version); instanceEntity.Properties["CreatedTime"] = new EntityProperty(executionStartedEvent.Timestamp); instanceEntity.Properties["RuntimeStatus"] = new EntityProperty(OrchestrationStatus.Running.ToString()); - if (executionStartedEvent.ScheduledStartTime.HasValue) + if (executionStartedEvent.ScheduledStartTime.HasValue) { instanceEntity.Properties["ScheduledStartTime"] = new EntityProperty(executionStartedEvent.ScheduledStartTime); + } + + CorrelationTraceClient.Propagate(() => + { + historyEntity.Properties["Correlation"] = new EntityProperty(executionStartedEvent.Correlation); + estimatedBytes += Encoding.Unicode.GetByteCount(executionStartedEvent.Correlation); + }); + this.SetInstancesTablePropertyFromHistoryProperty( historyEntity, instanceEntity, diff --git a/src/DurableTask.AzureStorage/Utils.cs b/src/DurableTask.AzureStorage/Utils.cs index 6c2df2de1..3126fdff8 100644 --- a/src/DurableTask.AzureStorage/Utils.cs +++ b/src/DurableTask.AzureStorage/Utils.cs @@ -131,5 +131,22 @@ public static bool TryGetTaskScheduledId(HistoryEvent historyEvent, out int task return false; } } + + /// + /// Get the ClassName part delimited by + + /// e.g. DurableTask.AzureStorage.Tests.Correlation.CorrelationScenarioTest+SayHelloActivity + /// should be "SayHelloActivity" + /// + /// + public static string GetTargetClassName(this string s) + { + if (s == null) + { + return null; + } + + int index = s.IndexOf('+'); + return s.Substring(index + 1, s.Length - index - 1); + } } } diff --git a/src/DurableTask.Core/AssemblyInfo.cs b/src/DurableTask.Core/AssemblyInfo.cs index d4c387e5b..d414ce1e6 100644 --- a/src/DurableTask.Core/AssemblyInfo.cs +++ b/src/DurableTask.Core/AssemblyInfo.cs @@ -14,6 +14,7 @@ using System.Runtime.CompilerServices; #if !SIGN_ASSEMBLY +[assembly: InternalsVisibleTo("DurableTask.Core.Tests")] [assembly: InternalsVisibleTo("DurableTask.Framework.Tests")] [assembly: InternalsVisibleTo("DurableTask.ServiceBus.Tests")] #endif diff --git a/src/DurableTask.Core/CorrelatedExceptionDetails.cs b/src/DurableTask.Core/CorrelatedExceptionDetails.cs new file mode 100644 index 000000000..e4b5d1cd8 --- /dev/null +++ b/src/DurableTask.Core/CorrelatedExceptionDetails.cs @@ -0,0 +1,53 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// A class that includes an exception and correlation information for sending telemetry + /// + public class CorrelatedExceptionDetails + { + /// + /// Exception that is sent to E2E tracing system + /// + public Exception Exception { get; set; } + + /// + /// OperationId is unique id of end to end tracing + /// + public string OperationId { get; set; } + + /// + /// ParentId is an id of an end to end tracing + /// + public string ParentId { get; set; } + + /// + /// A constructor with mandatory parameters. + /// + /// Exception + /// OperationId + /// ParentId + public CorrelatedExceptionDetails(Exception exception, string operationId, string parentId) + { + this.Exception = exception; + this.ParentId = parentId; + this.OperationId = operationId; + } + } +} diff --git a/src/DurableTask.Core/CorrelationTraceClient.cs b/src/DurableTask.Core/CorrelationTraceClient.cs new file mode 100644 index 000000000..99fd91943 --- /dev/null +++ b/src/DurableTask.Core/CorrelationTraceClient.cs @@ -0,0 +1,148 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading.Tasks; + using DurableTask.Core.Settings; + + /// + /// Delegate sending telemetry to the other side. + /// Mainly send telemetry to the Durable Functions TelemetryClient + /// + public static class CorrelationTraceClient + { + const string DiagnosticSourceName = "DurableTask.Core"; + const string RequestTrackEvent = "RequestEvent"; + const string DependencyTrackEvent = "DependencyEvent"; + const string ExceptionEvent = "ExceptionEvent"; + static DiagnosticSource logger = new DiagnosticListener(DiagnosticSourceName); + static IDisposable applicationInsightsSubscription = null; + static IDisposable listenerSubscription = null; + + /// + /// Setup this class uses callbacks to enable send telemetry to the Application Insights. + /// You need to call this method if you want to use this class. + /// + /// Action to send request telemetry using + /// Action to send telemetry for + /// Action to send telemetry for exception + public static void SetUp( + Action trackRequestTelemetryAction, + Action trackDependencyTelemetryAction, + Action trackExceptionAction) + { + listenerSubscription = DiagnosticListener.AllListeners.Subscribe( + delegate (DiagnosticListener listener) + { + if (listener.Name == DiagnosticSourceName) + { + applicationInsightsSubscription?.Dispose(); + + applicationInsightsSubscription = listener.Subscribe((KeyValuePair evt) => + { + if (evt.Key == RequestTrackEvent) + { + var context = (TraceContextBase)evt.Value; + trackRequestTelemetryAction(context); + } + + if (evt.Key == DependencyTrackEvent) + { + // the parameter is DependencyTelemetry which is already stopped. + var context = (TraceContextBase)evt.Value; + trackDependencyTelemetryAction(context); + } + + if (evt.Key == ExceptionEvent) + { + var e = (Exception)evt.Value; + trackExceptionAction(e); + } + }); + } + }); + } + + /// + /// Track the RequestTelemetry + /// + /// + public static void TrackRequestTelemetry(TraceContextBase context) + { + Tracking(() => logger.Write(RequestTrackEvent, context)); + } + + /// + /// Track the DependencyTelemetry + /// + /// + public static void TrackDepencencyTelemetry(TraceContextBase context) + { + Tracking(() => logger.Write(DependencyTrackEvent, context)); + } + + /// + /// Track the Exception + /// + /// + public static void TrackException(Exception e) + { + Tracking(() => logger.Write(ExceptionEvent, e)); + } + + /// + /// Execute Action for Propagate correlation information. + /// It suppresses the execution when .DisablePropagation is true. + /// + /// + public static void Propagate(Action action) + { + Execute(action); + } + + /// + /// Execute Aysnc Function for propagete correlation information + /// It suppresses the execution when .DisablePropagation is true. + /// + /// + /// + public static Task PropagateAsync(Func func) + { + if (CorrelationSettings.Current.EnableDistributedTracing) + { + return func(); + } + else + { + return Task.CompletedTask; + } + } + + static void Tracking(Action tracking) + { + Execute(tracking); + } + + static void Execute(Action action) + { + if (CorrelationSettings.Current.EnableDistributedTracing) + { + action(); + } + } + } +} diff --git a/src/DurableTask.Core/CorrelationTraceContext.cs b/src/DurableTask.Core/CorrelationTraceContext.cs new file mode 100644 index 000000000..c6dfd89ab --- /dev/null +++ b/src/DurableTask.Core/CorrelationTraceContext.cs @@ -0,0 +1,45 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System.Threading; + + /// + /// Manage TraceContext for Dependency. + /// This class share the TraceContext using AsyncLocal. + /// + public class CorrelationTraceContext + { + static readonly AsyncLocal current = new AsyncLocal(); + static readonly AsyncLocal generateDependencyTracking = new AsyncLocal(); + + /// + /// Share the TraceContext on the call graph contextBase. + /// + public static TraceContextBase Current + { + get { return current.Value; } + set { current.Value = value; } + } + + /// + /// Set true if a DependencyTelemetry tracking is generated on the TaskHubQueue. + /// + public static bool GenerateDependencyTracking + { + get { return generateDependencyTracking.Value; } + set { generateDependencyTracking.Value = value; } + } + } +} diff --git a/src/DurableTask.Core/DurableTask.Core.csproj b/src/DurableTask.Core/DurableTask.Core.csproj index 5d4ed6f0d..958e6a48b 100644 --- a/src/DurableTask.Core/DurableTask.Core.csproj +++ b/src/DurableTask.Core/DurableTask.Core.csproj @@ -20,4 +20,9 @@ + + + + + \ No newline at end of file diff --git a/src/DurableTask.Core/History/ExecutionStartedEvent.cs b/src/DurableTask.Core/History/ExecutionStartedEvent.cs index 6b3817b4c..edf1c0d40 100644 --- a/src/DurableTask.Core/History/ExecutionStartedEvent.cs +++ b/src/DurableTask.Core/History/ExecutionStartedEvent.cs @@ -81,11 +81,16 @@ internal ExecutionStartedEvent() [DataMember] public IDictionary Tags { get; set; } + /// + /// Gets or sets the serialized end-to-end correlation state. + /// + [DataMember] + public string Correlation { get; set; } + /// /// Gets or sets date to start the orchestration /// [DataMember] public DateTime? ScheduledStartTime { get; set; } - } } \ No newline at end of file diff --git a/src/DurableTask.Core/History/HistoryEvent.cs b/src/DurableTask.Core/History/HistoryEvent.cs index a6c71c9e3..fab1057b8 100644 --- a/src/DurableTask.Core/History/HistoryEvent.cs +++ b/src/DurableTask.Core/History/HistoryEvent.cs @@ -88,5 +88,6 @@ protected HistoryEvent(int eventId) /// Implementation for . /// public ExtensionDataObject ExtensionData { get; set; } + } } \ No newline at end of file diff --git a/src/DurableTask.Core/HttpCorrelationProtocolTraceContext.cs b/src/DurableTask.Core/HttpCorrelationProtocolTraceContext.cs new file mode 100644 index 000000000..93b5aa4ac --- /dev/null +++ b/src/DurableTask.Core/HttpCorrelationProtocolTraceContext.cs @@ -0,0 +1,93 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + + /// + /// HttpCorrelationProtocolTraceContext keep the correlation value with HTTP Correlation Protocol + /// + public class HttpCorrelationProtocolTraceContext : TraceContextBase + { + /// + /// Default Constructor + /// + public HttpCorrelationProtocolTraceContext() : base() { } + + /// + /// ParentId for backward compatibility + /// + public string ParentId { get; set; } + + /// + /// ParentId for parent + /// + public string ParentParentId { get; set; } + + /// + public override void SetParentAndStart(TraceContextBase parentTraceContext) + { + CurrentActivity = new Activity(this.OperationName); + CurrentActivity.SetIdFormat(ActivityIdFormat.Hierarchical); + + if (parentTraceContext is HttpCorrelationProtocolTraceContext) + { + var context = (HttpCorrelationProtocolTraceContext)parentTraceContext; + CurrentActivity.SetParentId(context.ParentId); // TODO check if it is context.ParentId or context.CurrentActivity.Id + OrchestrationTraceContexts = context.OrchestrationTraceContexts.Clone(); + } + + CurrentActivity.Start(); + + ParentId = CurrentActivity.Id; + StartTime = CurrentActivity.StartTimeUtc; + ParentParentId = CurrentActivity.ParentId; + + CorrelationTraceContext.Current = this; + } + + /// + public override void StartAsNew() + { + CurrentActivity = new Activity(this.OperationName); + CurrentActivity.SetIdFormat(ActivityIdFormat.Hierarchical); + CurrentActivity.Start(); + + ParentId = CurrentActivity.Id; + StartTime = CurrentActivity.StartTimeUtc; + ParentParentId = CurrentActivity.ParentId; + + CorrelationTraceContext.Current = this; + } + + /// + public override TimeSpan Duration => CurrentActivity?.Duration ?? DateTimeOffset.UtcNow - StartTime; + + /// + public override string TelemetryId => CurrentActivity?.Id ?? ParentId; + + /// + public override string TelemetryContextOperationId => CurrentActivity?.RootId ?? GetRootId(ParentId); + + /// + public override string TelemetryContextOperationParentId => CurrentActivity?.ParentId ?? ParentParentId; + + // internal use. Make it internal for testability. + internal string GetRootId(string id) => id?.Split('.').FirstOrDefault()?.Replace("|", ""); + } +} diff --git a/src/DurableTask.Core/NullObjectTraceContext.cs b/src/DurableTask.Core/NullObjectTraceContext.cs new file mode 100644 index 000000000..30549294c --- /dev/null +++ b/src/DurableTask.Core/NullObjectTraceContext.cs @@ -0,0 +1,44 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// NullObjectTraceContext is for the behavior which is not supported the Distributed Tracing + /// + public class NullObjectTraceContext : TraceContextBase + { + /// + public override TimeSpan Duration => TimeSpan.MinValue; + /// + public override string TelemetryId => "NullObjectTraceContextTelemetryId"; + /// + public override string TelemetryContextOperationId => "NullObjectTraceContextOperationId"; + /// + public override string TelemetryContextOperationParentId => "NullObjectTraceContextParentId"; + /// + public override void SetParentAndStart(TraceContextBase parentTraceContext) + { + + } + /// + public override void StartAsNew() + { + + } + } +} diff --git a/src/DurableTask.Core/Settings/CorrelationSettings.cs b/src/DurableTask.Core/Settings/CorrelationSettings.cs new file mode 100644 index 000000000..99ceb9b15 --- /dev/null +++ b/src/DurableTask.Core/Settings/CorrelationSettings.cs @@ -0,0 +1,67 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core.Settings +{ + using System; + using System.Collections.Generic; + using System.Net.Sockets; + using System.Text; + + /// + /// Settings for Distributed Tracing + /// + public class CorrelationSettings + { + /// + /// Create a new instance of the CorrelationSettings with default settings + /// + public CorrelationSettings() + { + Protocol = Protocol.W3CTraceContext; + } + + /// + /// Correlation Protocol + /// + public Protocol Protocol { get; set; } + + /// + /// Suppress Distributed Tracing + /// default: true + /// + public bool EnableDistributedTracing { get; set; } = false; + + /// + /// Current Correlation Settings + /// TODO Need to discuss the design for referencing Settings from DurableTask.Core side. + /// + public static CorrelationSettings Current { get; set; } = new CorrelationSettings(); + } + + /// + /// Distributed Tracing Protocol + /// + public enum Protocol + { + /// + /// W3C TraceContext Protocol + /// + W3CTraceContext, + + /// + /// HttpCorrelationProtocol + /// + HttpCorrelationProtocol + } +} diff --git a/src/DurableTask.Core/StackExtensions.cs b/src/DurableTask.Core/StackExtensions.cs new file mode 100644 index 000000000..dde42698c --- /dev/null +++ b/src/DurableTask.Core/StackExtensions.cs @@ -0,0 +1,35 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System.Collections.Generic; + using System.Linq; + + /// + /// Extension methods for Stack + /// + public static class StackExtensions + { + /// + /// Clone the Stack instance with the right order. + /// + /// Type of the Stack + /// Stack instance + /// + public static Stack Clone(this Stack original) + { + return new Stack(original.Reverse()); + } + } +} diff --git a/src/DurableTask.Core/TaskActivityDispatcher.cs b/src/DurableTask.Core/TaskActivityDispatcher.cs index 56e09eab2..9fe852704 100644 --- a/src/DurableTask.Core/TaskActivityDispatcher.cs +++ b/src/DurableTask.Core/TaskActivityDispatcher.cs @@ -15,6 +15,7 @@ namespace DurableTask.Core { using System; using System.Diagnostics; + using System.Reflection; using System.Threading; using System.Threading.Tasks; using DurableTask.Core.Common; @@ -145,6 +146,9 @@ async Task OnProcessWorkItemAsync(TaskActivityWorkItem workItem) await this.dispatchPipeline.RunAsync(dispatchContext, async _ => { + // correlation + CorrelationTraceClient.Propagate(() => workItem.TraceContextBase?.SetActivityToCurrent()); + try { string output = await taskActivity.RunAsync(context, scheduledEvent.Input); @@ -156,6 +160,7 @@ async Task OnProcessWorkItemAsync(TaskActivityWorkItem workItem) string details = this.IncludeDetails ? e.Details : null; eventToRespond = new TaskFailedEvent(-1, scheduledEvent.EventId, e.Message, details); this.logHelper.TaskActivityFailure(orchestrationInstance, scheduledEvent.Name, (TaskFailedEvent)eventToRespond, e); + CorrelationTraceClient.Propagate(() => CorrelationTraceClient.TrackException(e)); } catch (Exception e) when (!Utils.IsFatal(e) && !Utils.IsExecutionAborting(e)) { diff --git a/src/DurableTask.Core/TaskActivityWorkItem.cs b/src/DurableTask.Core/TaskActivityWorkItem.cs index 70278b189..b73d55d53 100644 --- a/src/DurableTask.Core/TaskActivityWorkItem.cs +++ b/src/DurableTask.Core/TaskActivityWorkItem.cs @@ -14,6 +14,7 @@ namespace DurableTask.Core { using System; + using System.Diagnostics; /// /// An active instance / work item of a task activity @@ -34,5 +35,10 @@ public class TaskActivityWorkItem /// The task message associated with this work item /// public TaskMessage TaskMessage; + + /// + /// The TraceContext which is included on the queue. + /// + public TraceContextBase TraceContextBase; } } \ No newline at end of file diff --git a/src/DurableTask.Core/TaskHubClient.cs b/src/DurableTask.Core/TaskHubClient.cs index 5b796d5a2..e5604cd15 100644 --- a/src/DurableTask.Core/TaskHubClient.cs +++ b/src/DurableTask.Core/TaskHubClient.cs @@ -15,6 +15,7 @@ namespace DurableTask.Core { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -573,6 +574,11 @@ public Task CreateOrchestrationInstanceAsync(string name, object eventData, DateTime? startAt = null) { + TraceContextBase requestTraceContext = null; + + // correlation + CorrelationTraceClient.Propagate(()=> { requestTraceContext = CreateOrExtractRequestTraceContext(eventName); }); + if (string.IsNullOrWhiteSpace(orchestrationInstanceId)) { orchestrationInstanceId = Guid.NewGuid().ToString("N"); @@ -601,6 +607,8 @@ public Task CreateOrchestrationInstanceAsync(string name, }; this.logHelper.SchedulingOrchestration(startedEvent); + + CorrelationTraceClient.Propagate(() => CreateAndTrackDependencyTelemetry(requestTraceContext)); // Raised events and create orchestration calls use different methods so get handled separately await this.ServiceClient.CreateTaskOrchestrationAsync(startMessage, dedupeStatuses); @@ -625,12 +633,43 @@ public Task CreateOrchestrationInstanceAsync(string name, }, Event = eventRaisedEvent, }; + await this.ServiceClient.SendTaskOrchestrationMessageAsync(eventMessage); } + return orchestrationInstance; } + TraceContextBase CreateOrExtractRequestTraceContext(string eventName) + { + TraceContextBase requestTraceContext = null; + if (Activity.Current == null) // It is possible that the caller already has an activity. + { + requestTraceContext = TraceContextFactory.Create($"{TraceConstants.Client}: {eventName}"); + requestTraceContext.StartAsNew(); + } + else + { + requestTraceContext = TraceContextFactory.Create(Activity.Current); + } + + return requestTraceContext; + } + + void CreateAndTrackDependencyTelemetry(TraceContextBase requestTraceContext) + { + TraceContextBase dependencyTraceContext = TraceContextFactory.Create(TraceConstants.Client); + dependencyTraceContext.TelemetryType = TelemetryType.Dependency; + dependencyTraceContext.SetParentAndStart(requestTraceContext); + + CorrelationTraceContext.Current = dependencyTraceContext; + + // Correlation + CorrelationTraceClient.TrackDepencencyTelemetry(dependencyTraceContext); + CorrelationTraceClient.TrackRequestTelemetry(requestTraceContext); + } + /// /// Raises an event in the specified orchestration instance, which eventually causes the OnEvent() method in the /// orchestration to fire. diff --git a/src/DurableTask.Core/TaskOrchestrationContext.cs b/src/DurableTask.Core/TaskOrchestrationContext.cs index c3ed1ff1e..61b00f296 100644 --- a/src/DurableTask.Core/TaskOrchestrationContext.cs +++ b/src/DurableTask.Core/TaskOrchestrationContext.cs @@ -451,6 +451,13 @@ public void FailOrchestration(Exception failure) string reason = failure.Message; string details; + // correlation + CorrelationTraceClient.Propagate( + () => + { + CorrelationTraceClient.TrackException(failure); + }); + if (failure is OrchestrationFailureException orchestrationFailureException) { details = orchestrationFailureException.Details; diff --git a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs index 264de02d3..b09fd4067 100644 --- a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs +++ b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs @@ -125,6 +125,16 @@ async Task OnProcessWorkItemSessionAsync(TaskOrchestrationWorkItem workItem) } var isExtendedSession = false; + + CorrelationTraceClient.Propagate( + () => + { + // Check if it is extended session. + isExtendedSession = this.concurrentSessionLock.Acquire(); + this.concurrentSessionLock.Release(); + workItem.IsExtendedSession = isExtendedSession; + }); + var processCount = 0; try { @@ -205,6 +215,9 @@ protected async Task OnProcessWorkItemAsync(TaskOrchestrationWorkItem work var isCompleted = false; var continuedAsNew = false; var isInterrupted = false; + + // correlation + CorrelationTraceClient.Propagate(() => CorrelationTraceContext.Current = workItem.TraceContext); ExecutionStartedEvent continueAsNewExecutionStarted = null; TaskMessage continuedAsNewMessage = null; @@ -379,6 +392,13 @@ protected async Task OnProcessWorkItemAsync(TaskOrchestrationWorkItem work } } + // correlation + CorrelationTraceClient.Propagate(() => { + if (runtimeState.ExecutionStartedEvent != null) + runtimeState.ExecutionStartedEvent.Correlation = CorrelationTraceContext.Current.SerializableTraceContext; + }); + + // finish up processing of the work item if (!continuedAsNew && runtimeState.Events.Last().EventType != EventType.OrchestratorCompleted) { @@ -403,6 +423,11 @@ protected async Task OnProcessWorkItemAsync(TaskOrchestrationWorkItem work workItem.InstanceId, "Updating state for continuation"); + // correlation + CorrelationTraceClient.Propagate(() => { + continueAsNewExecutionStarted.Correlation = CorrelationTraceContext.Current.SerializableTraceContext; + }); + runtimeState = new OrchestrationRuntimeState(); runtimeState.AddEvent(new OrchestratorStartedEvent(-1)); runtimeState.AddEvent(continueAsNewExecutionStarted); diff --git a/src/DurableTask.Core/TaskOrchestrationWorkItem.cs b/src/DurableTask.Core/TaskOrchestrationWorkItem.cs index 726010acb..20271923f 100644 --- a/src/DurableTask.Core/TaskOrchestrationWorkItem.cs +++ b/src/DurableTask.Core/TaskOrchestrationWorkItem.cs @@ -15,6 +15,7 @@ namespace DurableTask.Core { using System; using System.Collections.Generic; + using System.Diagnostics; /// /// An active instance / work item of an orchestration @@ -47,6 +48,16 @@ public class TaskOrchestrationWorkItem /// public IOrchestrationSession Session; + /// + /// The trace context used for correlation. + /// + public TraceContextBase TraceContext; + + /// + /// The flag of extendedSession. + /// + public bool IsExtendedSession = false; + internal OrchestrationExecutionCursor Cursor; } } \ No newline at end of file diff --git a/src/DurableTask.Core/TraceConstants.cs b/src/DurableTask.Core/TraceConstants.cs new file mode 100644 index 000000000..e155dc8d7 --- /dev/null +++ b/src/DurableTask.Core/TraceConstants.cs @@ -0,0 +1,41 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + /// + /// TraceConstants for Distributed Tracing + /// + public static class TraceConstants + { + /// + /// Client is the Distributed Tracing message for OrchestratorClient. + /// + public const string Client = "DtClient"; + + /// + /// Orchestrator is the Distributed Tracing message for Orchestrator. + /// + public const string Orchestrator = "DtOrchestrator"; + + /// + /// Activity is the Distributed Tracing message for Activity + /// + public const string Activity = "DtActivity"; + + /// + /// DependencyDefault is the Distributed Tracing message default name for Dependency. + /// + public const string DependencyDefault = "outbound"; + } +} diff --git a/src/DurableTask.Core/TraceContextBase.cs b/src/DurableTask.Core/TraceContextBase.cs new file mode 100644 index 000000000..352d0f241 --- /dev/null +++ b/src/DurableTask.Core/TraceContextBase.cs @@ -0,0 +1,212 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + /// + /// TraceContext keep the correlation value. + /// + public abstract class TraceContextBase + { + /// + /// Default constructor + /// + protected TraceContextBase() + { + OrchestrationTraceContexts = new Stack(); + } + + static TraceContextBase() + { + CustomJsonSerializerSettings = new JsonSerializerSettings() + { + TypeNameHandling = TypeNameHandling.Objects, + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + ReferenceLoopHandling = ReferenceLoopHandling.Serialize, + }; + } + + /// + /// Start time of this telemetry + /// + public DateTimeOffset StartTime { get; set; } + + /// + /// Type of this telemetry. + /// Request Telemetry or Dependency Telemetry. + /// Use + /// + /// + public TelemetryType TelemetryType { get; set; } + + /// + /// OrchestrationState save the state of the + /// + public Stack OrchestrationTraceContexts { get; set; } + + /// + /// Keep OperationName in case, don't have an Activity in this context + /// + public string OperationName { get; set; } + + /// + /// Current Activity only managed by this concrete class. + /// This property is not serialized. + /// + [JsonIgnore] + internal Activity CurrentActivity { get; set; } + + /// + /// Return if the orchestration is on replay + /// + /// + [JsonIgnore] + public bool IsReplay { get; set; } = false; + + /// + /// Duration of this context. Valid after call Stop() method. + /// + [JsonIgnore] + public abstract TimeSpan Duration { get; } + + [JsonIgnore] + static JsonSerializerSettings CustomJsonSerializerSettings { get; } + + /// + /// Serializable Json string of TraceContext + /// + [JsonIgnore] + public string SerializableTraceContext => + JsonConvert.SerializeObject(this, CustomJsonSerializerSettings); + + /// + /// Telemetry.Id Used for sending telemetry. refer this URL + /// https://docs.microsoft.com/en-us/dotnet/api/microsoft.applicationinsights.extensibility.implementation.operationtelemetry?view=azure-dotnet + /// + [JsonIgnore] + public abstract string TelemetryId { get; } + + /// + /// Telemetry.Context.Operation.Id Used for sending telemetry refer this URL + /// https://docs.microsoft.com/en-us/dotnet/api/microsoft.applicationinsights.extensibility.implementation.operationtelemetry?view=azure-dotnet + /// + [JsonIgnore] + public abstract string TelemetryContextOperationId { get; } + + /// + /// Get RequestTraceContext of Current Orchestration + /// + /// + public TraceContextBase GetCurrentOrchestrationRequestTraceContext() + { + foreach(TraceContextBase element in OrchestrationTraceContexts) + { + if (TelemetryType.Request == element.TelemetryType) return element; + } + + throw new InvalidOperationException("Can not find RequestTraceContext"); + } + + /// + /// Telemetry.Context.Operation.ParentId Used for sending telemetry refer this URL + /// https://docs.microsoft.com/en-us/dotnet/api/microsoft.applicationinsights.extensibility.implementation.operationtelemetry?view=azure-dotnet + /// + [JsonIgnore] + public abstract string TelemetryContextOperationParentId { get; } + + /// + /// Set Parent TraceContext and Start the context + /// + /// Parent Trace + public abstract void SetParentAndStart(TraceContextBase parentTraceContext); + + /// + /// Start TraceContext as new + /// + public abstract void StartAsNew(); + + /// + /// Stop TraceContext + /// + public void Stop() => CurrentActivity?.Stop(); + + /// + /// Set Activity.Current to CurrentActivity + /// + public void SetActivityToCurrent() + { + Activity.Current = CurrentActivity; + } + + /// + /// Restore TraceContext sub class + /// + /// Serialized json of TraceContext sub classes + /// + public static TraceContextBase Restore(string json) + { + // If the JSON is empty, we assume to have an empty context + if (string.IsNullOrEmpty(json)) + { + return TraceContextFactory.Empty; + } + + // Obtain typename and validate that it is a subclass of `TraceContextBase`. + // If it's not, we throw an exception. + Type traceContextType = null; + Type traceContextBasetype = typeof(TraceContextBase); + + JToken typeName = JObject.Parse(json)["$type"]; + traceContextType = Type.GetType(typeName.Value()); + if (!traceContextType.IsSubclassOf(traceContextBasetype)) + { + string typeNameStr = typeName.ToString(); + string baseNameStr = traceContextBasetype.ToString(); + throw new Exception($"Serialized TraceContext type ${typeNameStr} is not a subclass of ${baseNameStr}." + + "This probably means something went wrong in serializing the TraceContext."); + } + + // De-serialize the object now that we now it's safe + var restored = JsonConvert.DeserializeObject( + json, + traceContextType, + CustomJsonSerializerSettings) as TraceContextBase; + restored.OrchestrationTraceContexts = new Stack(restored.OrchestrationTraceContexts); + return restored; + } + } + + /// + /// Telemetry Type + /// + public enum TelemetryType + { + /// + /// Request Telemetry + /// + Request, + + /// + /// Dependency Telemetry + /// + Dependency, + } +} diff --git a/src/DurableTask.Core/TraceContextFactory.cs b/src/DurableTask.Core/TraceContextFactory.cs new file mode 100644 index 000000000..11ecf6c65 --- /dev/null +++ b/src/DurableTask.Core/TraceContextFactory.cs @@ -0,0 +1,121 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Dynamic; + using System.Text; + using DurableTask.Core.Settings; + + /// + /// Factory of TraceContext + /// + public class TraceContextFactory + { + /// + /// Create an instance of TraceContext + /// + /// Operation name for the TraceContext + /// + public static TraceContextBase Create(string operationName) + { + return CreateFactory().Create(operationName); + } + + /// + /// Create an instance of TraceContext + /// + /// Activity already started + /// + public static TraceContextBase Create(Activity activity) + { + return CreateFactory().Create(activity); + } + + /// + /// Create a default context of TraceContext + /// returns NullObjectTraceContext object + /// + public static TraceContextBase Empty { get; } = new NullObjectTraceContext(); + + static ITraceContextFactory CreateFactory() + { + switch (CorrelationSettings.Current.Protocol) + { + case Protocol.W3CTraceContext: + return new W3CTraceContextFactory(); + case Protocol.HttpCorrelationProtocol: + return new HttpCorrelationProtocolTraceContextFactory(); + default: + throw new NotSupportedException($"{CorrelationSettings.Current.Protocol} is not supported. Check the CorrelationSettings.Current.Protocol"); + } + } + + interface ITraceContextFactory + { + TraceContextBase Create(Activity activity); + + TraceContextBase Create(string operationName); + } + + class W3CTraceContextFactory : ITraceContextFactory + { + public TraceContextBase Create(Activity activity) + { + return new W3CTraceContext() + { + OperationName = activity.OperationName, + StartTime = activity.StartTimeUtc, + TraceParent = activity.Id, + TraceState = activity.TraceStateString, + ParentSpanId = activity.ParentSpanId.ToHexString(), + // ParentId = activity.Id // TODO check if it necessary + CurrentActivity = activity + }; + } + + public TraceContextBase Create(string operationName) + { + return new W3CTraceContext() + { + OperationName = operationName + }; + } + } + + class HttpCorrelationProtocolTraceContextFactory : ITraceContextFactory + { + public TraceContextBase Create(Activity activity) + { + return new HttpCorrelationProtocolTraceContext() + { + OperationName = activity.OperationName, + StartTime = activity.StartTimeUtc, + ParentId = activity.Id, + CurrentActivity = activity + }; + } + + public TraceContextBase Create(string operationName) + { + return new HttpCorrelationProtocolTraceContext() + { + OperationName = operationName + }; + } + } + } +} diff --git a/src/DurableTask.Core/W3CTraceContext.cs b/src/DurableTask.Core/W3CTraceContext.cs new file mode 100644 index 000000000..173ad8e65 --- /dev/null +++ b/src/DurableTask.Core/W3CTraceContext.cs @@ -0,0 +1,163 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + + /// + /// W3CTraceContext keep the correlation value with W3C TraceContext protocol + /// + public class W3CTraceContext : TraceContextBase + { + /// + /// Default constructor + /// + public W3CTraceContext() : base() { } + + /// + /// W3C TraceContext: Traceparent + /// + public string TraceParent { get; set; } + + /// + /// W3C TraceContext: Tracestate + /// + public string TraceState { get; set; } + + /// + /// W3C TraceContext: ParentSpanId + /// + public string ParentSpanId { get; set; } + + /// + public override TimeSpan Duration => CurrentActivity?.Duration ?? DateTimeOffset.UtcNow - StartTime; + + /// + public override string TelemetryId + { + get + { + if (CurrentActivity == null) + { + var traceParent = TraceParentObject.Create(TraceParent); + return traceParent.SpanId; + } + else + { + return CurrentActivity.SpanId.ToHexString(); + } + } + } + + /// + public override string TelemetryContextOperationId => CurrentActivity?.RootId ?? + TraceParentObject.Create(TraceParent).TraceId; + + /// + public override string TelemetryContextOperationParentId { + get + { + if (CurrentActivity == null) + { + return ParentSpanId; + } + else + { + return CurrentActivity.ParentSpanId.ToHexString(); + } + } + } + + /// + public override void SetParentAndStart(TraceContextBase parentTraceContext) + { + if (CurrentActivity == null) + { + CurrentActivity = new Activity(this.OperationName); + CurrentActivity.SetIdFormat(ActivityIdFormat.W3C); + } + + if (parentTraceContext is W3CTraceContext) + { + var context = (W3CTraceContext)parentTraceContext; + CurrentActivity.SetParentId(context.TraceParent); + CurrentActivity.TraceStateString = context.TraceState; + OrchestrationTraceContexts = context.OrchestrationTraceContexts.Clone(); + } + + CurrentActivity.Start(); + + StartTime = CurrentActivity.StartTimeUtc; + TraceParent = CurrentActivity.Id; + TraceState = CurrentActivity.TraceStateString; + ParentSpanId = CurrentActivity.ParentSpanId.ToHexString(); + + CorrelationTraceContext.Current = this; + } + + /// + public override void StartAsNew() + { + CurrentActivity = new Activity(this.OperationName); + CurrentActivity.SetIdFormat(ActivityIdFormat.W3C); + CurrentActivity.Start(); + + StartTime = CurrentActivity.StartTimeUtc; + + TraceParent = CurrentActivity.Id; + + CurrentActivity.TraceStateString = TraceState; + TraceState = CurrentActivity.TraceStateString; + ParentSpanId = CurrentActivity.ParentSpanId.ToHexString(); + + CorrelationTraceContext.Current = this; + } + } + + internal class TraceParentObject + { + public string Version { get; set; } + + public string TraceId { get; set; } + + public string SpanId { get; set; } + + public string TraceFlags { get; set; } + + public static TraceParentObject Create(string traceParent) + { + if (!string.IsNullOrEmpty(traceParent)) + { + var substrings = traceParent.Split('-'); + if (substrings.Length != 4) + { + throw new ArgumentException($"Traceparent doesn't respect the spec. {traceParent}"); + } + + return new TraceParentObject + { + Version = substrings[0], + TraceId = substrings[1], + SpanId = substrings[2], + TraceFlags = substrings[3] + }; + } + + return new TraceParentObject(); + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/CorrelationScenarioTest.cs b/test/DurableTask.AzureStorage.Tests/Correlation/CorrelationScenarioTest.cs new file mode 100644 index 000000000..5e96da93f --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/CorrelationScenarioTest.cs @@ -0,0 +1,824 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Runtime.Serialization; + using System.Threading.Tasks; + using DurableTask.Core; + using DurableTask.Core.Settings; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; + + [TestClass] + public class CorrelationScenarioTest + { + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task SingleOrchestratorWithSingleActivityAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + List actual = await host.ExecuteOrchestrationAsync(typeof(SayHelloOrchestrator), "world", 360, enableExtendedSessions); + Assert.AreEqual(5, actual.Count); + + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} SayHelloOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello") + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + } + + [KnownType(typeof(Hello))] + internal class SayHelloOrchestrator : TaskOrchestration + { + public override Task RunTask(OrchestrationContext context, string input) + { + return context.ScheduleTask(typeof(Hello), input); + } + } + + internal class Hello : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentNullException(nameof(input)); + } + + Console.WriteLine($"Activity: Hello {input}"); + return $"Hello, {input}!"; + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task SingleOrchestrationWithThrowingExceptionAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + // parameter = null cause an exception. + Tuple, List> result = await host.ExecuteOrchestrationWithExceptionAsync(typeof(SayHelloOrchestrator), null, 50, enableExtendedSessions); + + List actual = result.Item1; + List actualExceptions = result.Item2; + + Assert.AreEqual(5, actual.Count); + + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} SayHelloOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello") + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + + CollectionAssert.AreEqual( + actualExceptions.Select(x => + x.Context.Operation.ParentId).ToList(), + new string[] { actual[4].Id, actual[2].Id } + ); + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task SingleOrchestratorWithMultipleActivitiesAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + List actual = await host.ExecuteOrchestrationAsync(typeof(SayHelloActivities), "world", 50, enableExtendedSessions); + Assert.AreEqual(7, actual.Count); + + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} SayHelloActivities"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(HelloWait).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} HelloWait"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(HelloWait).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} HelloWait") + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + } + + [KnownType(typeof(HelloWait))] + internal class SayHelloActivities : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + var tasks = new List>(); + tasks.Add(context.ScheduleTask(typeof(HelloWait), input)); + tasks.Add(context.ScheduleTask(typeof(HelloWait), input)); + await Task.WhenAll(tasks); + return $"{tasks[0].Result}:{tasks[1].Result}"; + } + } + + internal class HelloWait : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + throw new NotImplementedException(); + } + + protected override async Task ExecuteAsync(TaskContext context, string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentNullException(nameof(input)); + } + + await Task.Delay(TimeSpan.FromSeconds(2)); + + Console.WriteLine($"Activity: HelloWait {input}"); + return $"Hello, {input}! I wait for 1 sec."; + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task SubOrchestratorAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + List actual = await host.ExecuteOrchestrationAsync(typeof(ParentOrchestrator), "world", 50, enableExtendedSessions); + Assert.AreEqual(7, actual.Count); + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} ParentOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(ChildOrchestrator).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} ChildOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello") + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + } + + [KnownType(typeof(ChildOrchestrator))] + [KnownType(typeof(Hello))] + internal class ParentOrchestrator : TaskOrchestration + { + public override Task RunTask(OrchestrationContext context, string input) + { + return context.CreateSubOrchestrationInstance(typeof(ChildOrchestrator), input); + } + } + + [KnownType(typeof(Hello))] + internal class ChildOrchestrator : TaskOrchestration + { + public override Task RunTask(OrchestrationContext context, string input) + { + return context.ScheduleTask(typeof(Hello), input); + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task MultipleSubOrchestratorAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + List actual = await host.ExecuteOrchestrationAsync(typeof(ParentOrchestratorWithMultiLayeredSubOrchestrator), "world", 50, enableExtendedSessions); + Assert.AreEqual(13, actual.Count); + + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} ParentOrchestratorWithMultiLayeredSubOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(ChildOrchestratorWithMultiSubOrchestrator).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} ChildOrchestratorWithMultiSubOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(ChildOrchestrator).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} ChildOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(ChildOrchestrator).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} ChildOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello"), + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + } + + [KnownType(typeof(ChildOrchestratorWithMultiSubOrchestrator))] + [KnownType(typeof(ChildOrchestrator))] + [KnownType(typeof(Hello))] + internal class ParentOrchestratorWithMultiLayeredSubOrchestrator : TaskOrchestration + { + public override Task RunTask(OrchestrationContext context, string input) + { + return context.CreateSubOrchestrationInstance(typeof(ChildOrchestratorWithMultiSubOrchestrator), input); + } + } + + [KnownType(typeof(ChildOrchestrator))] + [KnownType(typeof(Hello))] + internal class ChildOrchestratorWithMultiSubOrchestrator : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + var tasks = new List>(); + tasks.Add(context.CreateSubOrchestrationInstance(typeof(ChildOrchestrator), "foo")); + tasks.Add(context.CreateSubOrchestrationInstance(typeof(ChildOrchestrator), "bar")); + await Task.WhenAll(tasks); + return $"{tasks[0].Result}:{tasks[1].Result}"; + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task SingleOrchestratorWithRetryAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + SingleOrchestrationWithRetry.ResetCounter(); + Tuple, List> resultTuple = await host.ExecuteOrchestrationWithExceptionAsync(typeof(SingleOrchestrationWithRetry), "world", 50, enableExtendedSessions); + List actual = resultTuple.Item1; + List actualExceptions = resultTuple.Item2; + + Assert.AreEqual(7, actual.Count); + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} SingleOrchestrationWithRetry"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(NeedToExecuteTwice).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} NeedToExecuteTwice"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(NeedToExecuteTwice).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} NeedToExecuteTwice") + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + + CollectionAssert.AreEqual( + actualExceptions.Select(x => x.Context.Operation.ParentId).ToList(), + new string[] { actual[4].Id }); + } + + [KnownType(typeof(NeedToExecuteTwice))] + internal class SingleOrchestrationWithRetry : TaskOrchestration + { + public override Task RunTask(OrchestrationContext context, string input) + { + var retryOption = new RetryOptions(TimeSpan.FromMilliseconds(10), 2); + return context.ScheduleWithRetry(typeof(NeedToExecuteTwice), retryOption, input); + } + + internal static void ResetCounter() + { + NeedToExecuteTwice.Counter = 0; + } + } + + internal class NeedToExecuteTwice : TaskActivity + { + internal static int Counter = 0; + + protected override string Execute(TaskContext context, string input) + { + if (Counter == 0) + { + Counter++; + throw new Exception("Something happens"); + } + + return $"Hello {input} with retry"; + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task MultiLayeredOrchestrationWithRetryAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + MultiLayeredOrchestrationWithRetry.Reset(); + var host = new TestCorrelationOrchestrationHost(); + Tuple, List> resultTuple = await host.ExecuteOrchestrationWithExceptionAsync(typeof(MultiLayeredOrchestrationWithRetry), "world", 50, enableExtendedSessions); + List actual = resultTuple.Item1; + List actualExceptions = resultTuple.Item2; + Assert.AreEqual(19, actual.Count); + + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiLayeredOrchestrationWithRetry"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(MultiLayeredOrchestrationChildWithRetry).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiLayeredOrchestrationChildWithRetry"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(NeedToExecuteTwice01).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} NeedToExecuteTwice01"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(MultiLayeredOrchestrationChildWithRetry).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiLayeredOrchestrationChildWithRetry"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(NeedToExecuteTwice01).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} NeedToExecuteTwice01"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(NeedToExecuteTwice02).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} NeedToExecuteTwice02"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(MultiLayeredOrchestrationChildWithRetry).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiLayeredOrchestrationChildWithRetry"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(NeedToExecuteTwice01).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} NeedToExecuteTwice01"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(NeedToExecuteTwice02).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} NeedToExecuteTwice02"), + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + + CollectionAssert.AreEqual( + actualExceptions.Select(x => x.Context.Operation.ParentId).ToList(), + new string[] { actual[6].Id , actual[4].Id, actual[12].Id, actual[8].Id}); + } + + [KnownType(typeof(MultiLayeredOrchestrationChildWithRetry))] + [KnownType(typeof(NeedToExecuteTwice01))] + [KnownType(typeof(NeedToExecuteTwice02))] + internal class MultiLayeredOrchestrationWithRetry : TaskOrchestration + { + public override Task RunTask(OrchestrationContext context, string input) + { + var retryOption = new RetryOptions(TimeSpan.FromMilliseconds(10), 3); + return context.CreateSubOrchestrationInstanceWithRetry(typeof(MultiLayeredOrchestrationChildWithRetry), retryOption, input); + } + + internal static void Reset() + { + NeedToExecuteTwice01.Counter = 0; + NeedToExecuteTwice02.Counter = 0; + } + } + + [KnownType(typeof(NeedToExecuteTwice01))] + [KnownType(typeof(NeedToExecuteTwice02))] + internal class MultiLayeredOrchestrationChildWithRetry : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + var result01 = await context.ScheduleTask(typeof(NeedToExecuteTwice01), input); + var result02 = await context.ScheduleTask(typeof(NeedToExecuteTwice02), input); + return $"{result01}:{result02}"; + } + } + + internal class NeedToExecuteTwice01 : TaskActivity + { + internal static int Counter = 0; + + protected override string Execute(TaskContext context, string input) + { + if (Counter == 0) + { + Counter++; + throw new Exception("Something happens"); + } + + return $"Hello {input} with retry"; + } + } + + internal class NeedToExecuteTwice02 : TaskActivity + { + internal static int Counter = 0; + + protected override string Execute(TaskContext context, string input) + { + if (Counter == 0) + { + Counter++; + throw new Exception("Something happens"); + } + + return $"Hello {input} with retry"; + } + } + + //[TestMethod] ContinueAsNew + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task ContinueAsNewAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + ContinueAsNewOrchestration.Reset(); + var host = new TestCorrelationOrchestrationHost(); + List actual = await host.ExecuteOrchestrationAsync(typeof(ContinueAsNewOrchestration), "world", 50, enableExtendedSessions); + Assert.AreEqual(11, actual.Count); + + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} ContinueAsNewOrchestration"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello"), + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + } + + [KnownType(typeof(Hello))] + internal class ContinueAsNewOrchestration : TaskOrchestration + { + static int counter = 0; + + public override async Task RunTask(OrchestrationContext context, string input) + { + string result = await context.ScheduleTask(typeof(Hello), input); + result = input + ":" + result; + if (counter < 3) + { + counter++; + context.ContinueAsNew(result); + } + + return result; + } + + internal static void Reset() + { + counter = 0; + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task MultipleParentScenarioAsync(Protocol protocol, bool enableExtendedSessions) + { + MultiParentOrchestrator.Reset(); + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + var tasks = new List(); + tasks.Add(host.ExecuteOrchestrationAsync(typeof(MultiParentOrchestrator), "world", 30, enableExtendedSessions)); + + while (IsNotReadyForRaiseEvent(host.Client)) + { + await Task.Delay(TimeSpan.FromMilliseconds(100)); + } + + await Task.Delay(TimeSpan.FromSeconds(1)); + tasks.Add(host.Client.RaiseEventAsync("someEvent", "hi")); + await Task.WhenAll(tasks); + + List actual = Convert(tasks[0]); + + Assert.AreEqual(5, actual.Count); + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiParentOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello") + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + } + + bool IsNotReadyForRaiseEvent(TestOrchestrationClient client) + { + return client == null && !MultiParentOrchestrator.IsWaitForExternalEvent; + } + + List Convert(Task task) + { + return (task as Task>)?.Result; + } + + [KnownType(typeof(Hello))] + internal class MultiParentOrchestrator : TaskOrchestration + { + public static bool IsWaitForExternalEvent { get; set; } = false; + + readonly TaskCompletionSource receiveEvent = new TaskCompletionSource(); + + public async override Task RunTask(OrchestrationContext context, string input) + { + IsWaitForExternalEvent = true; + await this.receiveEvent.Task; + await context.ScheduleTask(typeof(Hello), input); + return "done"; + } + + public override void OnEvent(OrchestrationContext context, string name, string input) + { + this.receiveEvent.SetResult(null); + } + + internal static void Reset() + { + IsWaitForExternalEvent = false; + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task MultipleParentMultiLayerScenarioAsync(Protocol protocol, bool enableExtendedSessions) + { + MultiParentOrchestrator.Reset(); + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = true; + var host = new TestCorrelationOrchestrationHost(); + var tasks = new List(); + tasks.Add(host.ExecuteOrchestrationAsync(typeof(MultiParentMultiLayeredOrchestrator), "world", 30, enableExtendedSessions)); + + while (IsNotReadyForTwoRaiseEvents(host.Client)) + { + await Task.Delay(TimeSpan.FromMilliseconds(100)); + } + + await Task.Delay(TimeSpan.FromSeconds(1)); + foreach(string instanceId in MultiParentChildOrchestrator.InstanceIds) + { + tasks.Add(host.Client.RaiseEventAsync(instanceId, "someEvent", "hi")); + } + await Task.WhenAll(tasks); + + List actual = Convert(tasks[0]); + + Assert.AreEqual(11, actual.Count); + CollectionAssert.AreEqual( + new (Type, string)[] + { + (typeof(RequestTelemetry), TraceConstants.Client), + (typeof(DependencyTelemetry), TraceConstants.Client), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiParentMultiLayeredOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(MultiParentChildOrchestrator).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiParentChildOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(MultiParentChildOrchestrator).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Orchestrator} MultiParentChildOrchestrator"), + (typeof(DependencyTelemetry), $"{TraceConstants.Orchestrator} {typeof(Hello).FullName}"), + (typeof(RequestTelemetry), $"{TraceConstants.Activity} Hello") + }, actual.Select(x => (x.GetType(), x.Name)).ToList()); + MultiParentChildOrchestrator.Reset(); + + } + + bool IsNotReadyForTwoRaiseEvents(TestOrchestrationClient client) + { + return client == null || !(MultiParentChildOrchestrator.ReadyForExternalEvent == 2); + } + + [KnownType(typeof(MultiParentChildOrchestrator))] + [KnownType(typeof(Hello))] + internal class MultiParentMultiLayeredOrchestrator : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + var tasks = new List>(); + tasks.Add(context.CreateSubOrchestrationInstance(typeof(MultiParentChildOrchestrator), "foo")); + tasks.Add(context.CreateSubOrchestrationInstance(typeof(MultiParentChildOrchestrator), "bar")); + await Task.WhenAll(tasks); + return $"{tasks[0].Result}:{tasks[1].Result}"; + } + } + [KnownType(typeof(Hello))] + internal class MultiParentChildOrchestrator : TaskOrchestration + { + private static object lockExternalEvent = new object(); + private static object lockId = new object(); + private static int readyCountForExternalEvent = 0; + private static List orchestrationIds = new List(); + public static int ReadyForExternalEvent + { + get + { + return readyCountForExternalEvent; + } + + set + { + lock (lockExternalEvent) + { + readyCountForExternalEvent = value; + } + } + } + public static IEnumerable InstanceIds + { + get + { + IEnumerable result; + lock(lockId) + { + result = orchestrationIds.ToList(); + } + return result; + } + } + + public static void AddOrchestrationId(string orchestrationId) + { + lock(lockId) + { + orchestrationIds.Add(orchestrationId); + } + } + + public static void IncrementReadyForExternalEvent() + { + lock (lockExternalEvent) + { + readyCountForExternalEvent++; + } + } + + readonly TaskCompletionSource receiveEvent = new TaskCompletionSource(); + + public async override Task RunTask(OrchestrationContext context, string input) + { + AddOrchestrationId(context.OrchestrationInstance.InstanceId); + IncrementReadyForExternalEvent(); + await this.receiveEvent.Task; + await context.ScheduleTask(typeof(Hello), input); + return "done"; + } + + public override void OnEvent(OrchestrationContext context, string name, string input) + { + this.receiveEvent.SetResult(null); + } + + internal static void Reset() + { + ReadyForExternalEvent = 0; + orchestrationIds = new List(); + } + } + + [DataTestMethod] + [DataRow(Protocol.W3CTraceContext, false)] + [DataRow(Protocol.HttpCorrelationProtocol, false)] + [DataRow(Protocol.W3CTraceContext, true)] + [DataRow(Protocol.HttpCorrelationProtocol, true)] + public async Task SuppressTelemetryAsync(Protocol protocol, bool enableExtendedSessions) + { + CorrelationSettings.Current.Protocol = protocol; + CorrelationSettings.Current.EnableDistributedTracing = false; + MultiLayeredOrchestrationWithRetry.Reset(); + var host = new TestCorrelationOrchestrationHost(); + Tuple, List> resultTuple = await host.ExecuteOrchestrationWithExceptionAsync(typeof(MultiLayeredOrchestrationWithRetry), "world", 50, enableExtendedSessions); + List actual = resultTuple.Item1; + List actualExceptions = resultTuple.Item2; + Assert.AreEqual(0, actual.Count); + Assert.AreEqual(0, actualExceptions.Count); + } + + //[TestMethod] terminate + + class TestCorrelationOrchestrationHost + { + internal TestOrchestrationClient Client { get; set; } + + internal async Task, List>> ExecuteOrchestrationWithExceptionAsync(Type orchestrationType, string parameter, int timeout, bool enableExtendedSessions) + { + var sendItems = new ConcurrentQueue(); + await ExtractTelemetry(orchestrationType, parameter, timeout, sendItems, enableExtendedSessions); + + var sendItemList = ConvertTo(sendItems); + var operationTelemetryList = sendItemList.OfType(); + var exceptionTelemetryList = sendItemList.OfType().ToList(); + + List operationTelemetries = FilterOperationTelemetry(operationTelemetryList).ToList().CorrelationSort(); + + return new Tuple, List>(operationTelemetries, exceptionTelemetryList); + } + + internal async Task> ExecuteOrchestrationAsync(Type orchestrationType, string parameter, int timeout, bool enableExtendedSessions) + { + var sendItems = new ConcurrentQueue(); + await ExtractTelemetry(orchestrationType, parameter, timeout, sendItems, enableExtendedSessions); + + var sendItemList = ConvertTo(sendItems); + var operationTelemetryList = sendItemList.OfType(); + + var result = FilterOperationTelemetry(operationTelemetryList).ToList(); + Debug.WriteLine( + JsonConvert.SerializeObject( + result.Select( + x => new + { + Type = x.GetType().Name, + OperationName = x.Name, + Id = x.Id, + OperationId = x.Context.Operation.Id, + OperationParentId = x.Context.Operation.ParentId, + }))); + + return result.CorrelationSort(); + } + + IEnumerable FilterOperationTelemetry(IEnumerable operationTelemetries) + { + return operationTelemetries.Where( + p => p.Name.Contains(TraceConstants.Activity) || p.Name.Contains(TraceConstants.Orchestrator) || p.Name.Contains(TraceConstants.Client) || p.Name.Contains("Operation")); + } + + async Task ExtractTelemetry(Type orchestrationType, string parameter, int timeout, ConcurrentQueue sendItems, bool enableExtendedSessions) + { + var sendAction = new Action( + delegate(ITelemetry telemetry) { sendItems.Enqueue(telemetry); }); + new TelemetryActivator().Initialize(sendAction, Guid.NewGuid().ToString()); + // new TelemetryActivator().Initialize(item => sendItems.Enqueue(item), Guid.NewGuid().ToString()); + using (TestOrchestrationHost host = TestHelpers.GetTestOrchestrationHost(enableExtendedSessions)) + { + await host.StartAsync(); + var activity = new Activity(TraceConstants.Client); + + var idFormat = CorrelationSettings.Current.Protocol == Protocol.W3CTraceContext ? ActivityIdFormat.W3C : ActivityIdFormat.Hierarchical; + activity.SetIdFormat(idFormat); + + activity.Start(); + Client = await host.StartOrchestrationAsync(orchestrationType, parameter); + await Client.WaitForCompletionAsync(TimeSpan.FromSeconds(timeout)); + + await host.StopAsync(); + } + } + + List ConvertTo(ConcurrentQueue queue) + { + var converted = new List(); + while (!queue.IsEmpty) + { + ITelemetry x; + if (queue.TryDequeue(out x)) + { + converted.Add(x); + } + } + + return converted; + } + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/DurableTaskCorrelationTelemetryInitializer.cs b/test/DurableTask.AzureStorage.Tests/Correlation/DurableTaskCorrelationTelemetryInitializer.cs new file mode 100644 index 000000000..35d6c1215 --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/DurableTaskCorrelationTelemetryInitializer.cs @@ -0,0 +1,325 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using DurableTask.Core; + using DurableTask.Core.Settings; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.Common; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + + /// + /// Telemetry Initializer that sets correlation ids for W3C. + /// This source is based on W3COperationCorrelationTelemetryInitializer.cs + /// 1. Modified with CorrelationTraceContext.Current + /// 2. Avoid to be overriden when it is RequestTelemetry + /// Original Source is here + /// + [Obsolete("Not ready for public consumption.")] + [EditorBrowsable(EditorBrowsableState.Never)] +#if DEPENDENCY_COLLECTOR + public +#else + internal +#endif + class DurableTaskCorrelationTelemetryInitializer : ITelemetryInitializer + { + private const string RddDiagnosticSourcePrefix = "rdddsc"; + private const string SqlRemoteDependencyType = "SQL"; + + /// These internal property is copied from W3CConstants + /// Trace-Id tag name. + internal const string TraceIdTag = "w3c_traceId"; + + /// Span-Id tag name. + internal const string SpanIdTag = "w3c_spanId"; + + /// Parent span-Id tag name. + internal const string ParentSpanIdTag = "w3c_parentSpanId"; + + /// Version tag name. + internal const string VersionTag = "w3c_version"; + + /// Sampled tag name. + internal const string SampledTag = "w3c_sampled"; + + /// Tracestate tag name. + internal const string TracestateTag = "w3c_tracestate"; + + /// Default version value. + internal const string DefaultVersion = "00"; + + /// + /// Default sampled flag value: may be recorded, not requested + /// + internal const string TraceFlagRecordedAndNotRequested = "02"; + + /// Recorded and requested sampled flag value + internal const string TraceFlagRecordedAndRequested = "03"; + + /// Requested trace flag + internal const byte RequestedTraceFlag = 1; + + /// Legacy root Id tag name. + internal const string LegacyRootIdProperty = "ai_legacyRootId"; + + /// Legacy root Id tag name. + internal const string LegacyRequestIdProperty = "ai_legacyRequestId"; + + /// + /// Set of suppress telemetry tracking if you add Host name on this. + /// + public HashSet ExcludeComponentCorrelationHttpHeadersOnDomains { get; set; } + + /// + /// Constructor + /// + public DurableTaskCorrelationTelemetryInitializer() + { + ExcludeComponentCorrelationHttpHeadersOnDomains = new HashSet(); + } + + /// + /// Initializes telemety item. + /// + /// Telemetry item. + public void Initialize(ITelemetry telemetry) + { + if (IsSuppressedTelemetry(telemetry)) + { + SuppressTelemetry(telemetry); + return; + } + + if (!(telemetry is RequestTelemetry)) + { + Activity currentActivity = Activity.Current; + if (telemetry is ExceptionTelemetry) + { + Console.WriteLine("exception!"); + } + + if (currentActivity == null) + { + if (CorrelationTraceContext.Current != null) + { + UpdateTelemetry(telemetry, CorrelationTraceContext.Current); + } + } + else + { + if (CorrelationTraceContext.Current != null) + { + UpdateTelemetry(telemetry, CorrelationTraceContext.Current); + } + else if (CorrelationSettings.Current.Protocol == Protocol.W3CTraceContext) + { + UpdateTelemetry(telemetry, currentActivity, false); + } else if (CorrelationSettings.Current.Protocol == Protocol.HttpCorrelationProtocol + && telemetry is ExceptionTelemetry) + { + UpdateTelemetryExceptionForHTTPCorrelationProtocol((ExceptionTelemetry)telemetry, currentActivity); + } + } + } + } + + internal static void UpdateTelemetry(ITelemetry telemetry, TraceContextBase contextBase) + { + switch (contextBase) + { + case NullObjectTraceContext nullObjectContext: + return; + case W3CTraceContext w3cContext: + UpdateTelemetryW3C(telemetry, w3cContext); + break; + case HttpCorrelationProtocolTraceContext httpCorrelationProtocolTraceContext: + UpdateTelemetryHttpCorrelationProtocol(telemetry, httpCorrelationProtocolTraceContext); + break; + default: + return; + } + } + + internal static void UpdateTelemetryHttpCorrelationProtocol(ITelemetry telemetry, HttpCorrelationProtocolTraceContext context) + { + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + + bool initializeFromCurrent = opTelemetry != null; + + if (initializeFromCurrent) + { + initializeFromCurrent &= !(opTelemetry is DependencyTelemetry dependency && + dependency.Type == SqlRemoteDependencyType && + dependency.Context.GetInternalContext().SdkVersion + .StartsWith(RddDiagnosticSourcePrefix, StringComparison.Ordinal)); + } + + if (initializeFromCurrent) + { + opTelemetry.Id = !string.IsNullOrEmpty(opTelemetry.Id) ? opTelemetry.Id : context.TelemetryId; + telemetry.Context.Operation.ParentId = !string.IsNullOrEmpty(telemetry.Context.Operation.ParentId) ? telemetry.Context.Operation.ParentId : context.TelemetryContextOperationParentId; + } + else + { + telemetry.Context.Operation.Id = !string.IsNullOrEmpty(telemetry.Context.Operation.Id) ? telemetry.Context.Operation.Id : context.TelemetryContextOperationId; + if (telemetry is ExceptionTelemetry) + { + telemetry.Context.Operation.ParentId = context.TelemetryId; + } else + { + telemetry.Context.Operation.ParentId = !string.IsNullOrEmpty(telemetry.Context.Operation.ParentId) ? telemetry.Context.Operation.ParentId : context.TelemetryContextOperationParentId; + } + } + } + + internal static void UpdateTelemetryW3C(ITelemetry telemetry, W3CTraceContext context) + { + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + + bool initializeFromCurrent = opTelemetry != null; + + if (initializeFromCurrent) + { + initializeFromCurrent &= !(opTelemetry is DependencyTelemetry dependency && + dependency.Type == SqlRemoteDependencyType && + dependency.Context.GetInternalContext().SdkVersion + .StartsWith(RddDiagnosticSourcePrefix, StringComparison.Ordinal)); + } + + if (!string.IsNullOrEmpty(context.TraceState)) + { + opTelemetry.Properties["w3c_tracestate"] = context.TraceState; + } + + TraceParent traceParent = context.TraceParent.ToTraceParent(); + + if (initializeFromCurrent) + { + if (string.IsNullOrEmpty(opTelemetry.Id)) + opTelemetry.Id = traceParent.SpanId; + + if (string.IsNullOrEmpty(context.ParentSpanId)) + { + telemetry.Context.Operation.ParentId = telemetry.Context.Operation.Id; + } + } + else + { + if (telemetry.Context.Operation.Id == null) + { + telemetry.Context.Operation.Id = traceParent.TraceId; + } + + if (telemetry.Context.Operation.ParentId == null) + { + telemetry.Context.Operation.ParentId = traceParent.SpanId; + } + } + } + + internal void SuppressTelemetry(ITelemetry telemetry) + { + // TODO change the strategy. + telemetry.Context.Operation.Id = "suppressed"; + telemetry.Context.Operation.ParentId = "suppressed"; + // Context. Properties. ai_legacyRequestId , ai_legacyRequestId + foreach (var key in telemetry.Context.Properties.Keys) + { + if (key == "ai_legacyRootId" || + key == "ai_legacyRequestId") + { + telemetry.Context.Properties[key] = "suppressed"; + } + } + + ((OperationTelemetry)telemetry).Id = "suppressed"; + } + + internal bool IsSuppressedTelemetry(ITelemetry telemetry) + { + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + if (telemetry is DependencyTelemetry) + { + DependencyTelemetry dTelemetry = telemetry as DependencyTelemetry; + + if (!string.IsNullOrEmpty(dTelemetry.CommandName)) + { + var host = new Uri(dTelemetry.CommandName).Host; + if (ExcludeComponentCorrelationHttpHeadersOnDomains.Contains(host)) return true; + } + } + + return false; + } + + internal static void UpdateTelemetryExceptionForHTTPCorrelationProtocol(ExceptionTelemetry telemetry, Activity activity) + { + telemetry.Context.Operation.ParentId = activity.Id; + telemetry.Context.Operation.Id = activity.RootId; + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "This method has different code for Net45/NetCore")] + internal static void UpdateTelemetry(ITelemetry telemetry, Activity activity, bool forceUpdate) + { + if (activity == null) + { + return; + } + + // Requests and dependnecies are initialized from the current Activity + // (i.e. telemetry.Id = current.Id). Activity is created for such requests specifically + // Traces, exceptions, events on the other side are children of current activity + // There is one exception - SQL DiagnosticSource where current Activity is a parent + // for dependency calls. + + OperationTelemetry opTelemetry = telemetry as OperationTelemetry; + bool initializeFromCurrent = opTelemetry != null; + + if (initializeFromCurrent) + { + initializeFromCurrent &= !(opTelemetry is DependencyTelemetry dependency && + dependency.Type == SqlRemoteDependencyType && + dependency.Context.GetInternalContext().SdkVersion + .StartsWith(RddDiagnosticSourcePrefix, StringComparison.Ordinal)); + } + + if (telemetry is OperationTelemetry operation) + { + operation.Properties[TracestateTag] = activity.TraceStateString; + } + + if (initializeFromCurrent) + { + opTelemetry.Id = activity.SpanId.ToHexString(); + if (activity.ParentSpanId != null) + { + opTelemetry.Context.Operation.ParentId = activity.ParentSpanId.ToHexString(); + } + } + else + { + telemetry.Context.Operation.ParentId = activity.SpanId.ToHexString(); + } + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/LIstExtensionsTest.cs b/test/DurableTask.AzureStorage.Tests/Correlation/LIstExtensionsTest.cs new file mode 100644 index 000000000..b04b18170 --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/LIstExtensionsTest.cs @@ -0,0 +1,113 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class ListExtensionsTest + { + [TestMethod] + public async Task CorrelationSortAsync() + { + var operations = new List(); + var timeStamps = await GetDateTimeOffsetsAsync(6); + + operations.Add(CreateDependencyTelemetry(id: "02", parentId: "01", timeStamps[3])); + operations.Add(CreateRequestTelemetry(id: "01", parentId: null, timeStamps[0])); + operations.Add(CreateRequestTelemetry(id:"04", parentId: "03", timeStamps[2])); + operations.Add(CreateDependencyTelemetry(id:"05", parentId: "04", timeStamps[4])); + operations.Add(CreateDependencyTelemetry(id: "06", parentId: "05", timeStamps[5])); + operations.Add(CreateRequestTelemetry(id: "03", parentId: "02", timeStamps[1])); + + var actual = operations.CorrelationSort(); + Assert.AreEqual(6, actual.Count); + Assert.AreEqual("01", actual[0].Id); + Assert.AreEqual("02", actual[1].Id); + Assert.AreEqual("03", actual[2].Id); + Assert.AreEqual("04", actual[3].Id); + Assert.AreEqual("05", actual[4].Id); + Assert.AreEqual("06", actual[5].Id); + } + + [TestMethod] + public async Task CorrelationSortWithTwoChildrenAsync() + { + var operations = new List(); + var timeStamps = await GetDateTimeOffsetsAsync(8); + operations.Add(CreateRequestTelemetry(id: "01", parentId: null, timeStamps[0])); + operations.Add(CreateDependencyTelemetry(id: "02", parentId: "01", timeStamps[1])); + operations.Add(CreateRequestTelemetry(id: "05", parentId: "03", timeStamps[3])); + operations.Add(CreateRequestTelemetry(id: "04", parentId: "03", timeStamps[2])); + operations.Add(CreateDependencyTelemetry(id: "06", parentId: "04", timeStamps[4])); + operations.Add(CreateDependencyTelemetry(id: "08", parentId: "03", timeStamps[6])); + operations.Add(CreateDependencyTelemetry(id: "07", parentId: "05", timeStamps[5])); + operations.Add(CreateRequestTelemetry(id: "03", parentId: "02", timeStamps[7])); + + var actual = operations.CorrelationSort(); + Assert.AreEqual(8, actual.Count); + Assert.AreEqual("01", actual[0].Id); + Assert.AreEqual("02", actual[1].Id); + Assert.AreEqual("03", actual[2].Id); + Assert.AreEqual("04", actual[3].Id); + Assert.AreEqual("06", actual[4].Id); // Since the tree structure, 04 child comes before 05. + Assert.AreEqual("05", actual[5].Id); + Assert.AreEqual("07", actual[6].Id); + Assert.AreEqual("08", actual[7].Id); + } + + [TestMethod] + public void CorrelationSortWithZero() + { + var operations = new List(); + var actual = operations.CorrelationSort(); + Assert.AreEqual(0, actual.Count); + } + + async Task> GetDateTimeOffsetsAsync(int count) + { + List result = new List(); + for (var i = 0; i < count; i++) + { + await Task.Delay(TimeSpan.FromMilliseconds(1)); + result.Add(DateTimeOffset.UtcNow); + } + + return result; + } + + RequestTelemetry CreateRequestTelemetry(string id, string parentId, DateTimeOffset timeStamp) + { + return (RequestTelemetry)SetIdAndParentId(new RequestTelemetry(), id, parentId, timeStamp); + } + + DependencyTelemetry CreateDependencyTelemetry(string id, string parentId, DateTimeOffset timeStamp) + { + return (DependencyTelemetry)SetIdAndParentId(new DependencyTelemetry(), id, parentId, timeStamp); + } + + OperationTelemetry SetIdAndParentId(OperationTelemetry telemetry, string id, string parentId, DateTimeOffset timeStamp) + { + telemetry.Id = id; + telemetry.Context.Operation.ParentId = parentId; + telemetry.Timestamp = timeStamp; + return telemetry; + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/ListExtensions.cs b/test/DurableTask.AzureStorage.Tests/Correlation/ListExtensions.cs new file mode 100644 index 000000000..1cac268c7 --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/ListExtensions.cs @@ -0,0 +1,90 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + + public static class ListExtensions + { + public static List CorrelationSort(this List telemetries) + { + var result = new List(); + if (telemetries.Count == 0) + { + return result; + } + + // Sort by the timestamp + var sortedTelemetries = telemetries.OrderBy(p => p.Timestamp.Ticks).ToList(); + + // pick the first one as the parent. remove it from the list. + var parent = sortedTelemetries.First(); + result.Add(parent); + sortedTelemetries.RemoveOperationTelemetry(parent); + // find the child recursively and remove the child and pass it as a parameter + var sortedList = GetCorrelationSortedList(parent, sortedTelemetries); + result.AddRange(sortedList); + return result; + } + + public static bool RemoveOperationTelemetry(this List telemetries, OperationTelemetry telemetry) + { + int index = -1; + for (var i = 0; i < telemetries.Count; i++) + { + if (telemetries[i].Id == telemetry.Id) + { + index = i; + } + } + + if (index == -1) + { + return false; + } + + telemetries.RemoveAt(index); + return true; + } + + static List GetCorrelationSortedList(OperationTelemetry parent, List current) + { + var result = new List(); + if (current.Count != 0) + { + foreach (var some in current) + { + if (parent.Id == some.Context.Operation.ParentId) + { + Console.WriteLine("match"); + } + } + + IOrderedEnumerable nexts = current.Where(p => p.Context.Operation.ParentId == parent.Id).OrderBy(p => p.Timestamp.Ticks); + foreach (OperationTelemetry next in nexts) + { + current.RemoveOperationTelemetry(next); + result.Add(next); + var childResult = GetCorrelationSortedList(next, current); + result.AddRange(childResult); + } + } + + return result; + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/NoOpTelemetryChannel.cs b/test/DurableTask.AzureStorage.Tests/Correlation/NoOpTelemetryChannel.cs new file mode 100644 index 000000000..4060e4301 --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/NoOpTelemetryChannel.cs @@ -0,0 +1,92 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using Microsoft.ApplicationInsights.Channel; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + /// + /// A stub of . + /// This is the copy of the https://github.com/Microsoft/ApplicationInsights-dotnet/Test/TestFramework/Shared/StubTelemetryClient + /// + public sealed class NoOpTelemetryChannel : ITelemetryChannel + { + /// + /// Initializes a new instance of the class. + /// + public NoOpTelemetryChannel() + { + this.OnSend = telemetry => { }; + this.OnFlush = () => { }; + this.OnDispose = () => { }; + } + + /// + /// Gets or sets a value indicating whether this channel is in developer mode. + /// + public bool? DeveloperMode { get; set; } + + /// + /// Gets or sets a value indicating the channel's URI. To this URI the telemetry is expected to be sent. + /// + public string EndpointAddress { get; set; } + + /// + /// Gets or sets a value indicating whether to throw an error. + /// + public bool ThrowError { get; set; } + + /// + /// Gets or sets the callback invoked by the method. + /// + public Action OnSend { get; set; } + + public Action OnFlush { get; set; } + + public Action OnDispose { get; set; } + + /// + /// Implements the method by invoking the callback. + /// + public void Send(ITelemetry item) + { + if (this.ThrowError) + { + throw new Exception("test error"); + } + + this.OnSend(item); + } + + /// + /// Implements the method. + /// + public void Dispose() + { + this.OnDispose(); + } + + /// + /// Implements the method. + /// + public void Flush() + { + this.OnFlush(); + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/StringExtensions.cs b/test/DurableTask.AzureStorage.Tests/Correlation/StringExtensions.cs new file mode 100644 index 000000000..66f9abc2f --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/StringExtensions.cs @@ -0,0 +1,43 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using System; + + public static class StringExtensions + { + public static TraceParent ToTraceParent(this string traceParent) + { + if (!string.IsNullOrEmpty(traceParent)) + { + var substrings = traceParent.Split('-'); + if (substrings.Length != 4) + { + throw new ArgumentException($"Traceparent doesn't respect the spec. {traceParent}"); + } + + return new TraceParent + { + Version = substrings[0], + TraceId = substrings[1], + SpanId = substrings[2], + TraceFlags = substrings[3] + }; + } + + return null; + } + } + +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/StringExtensionsTest.cs b/test/DurableTask.AzureStorage.Tests/Correlation/StringExtensionsTest.cs new file mode 100644 index 000000000..164e7ebc0 --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/StringExtensionsTest.cs @@ -0,0 +1,50 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DurableTask.AzureStorage.Tests.Correlation +{ + + [TestClass] + public class StringExtensionsTest + { + [TestMethod] + public void TestParseTraceParent() + { + string traceparentString = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + TraceParent traceparent = traceparentString.ToTraceParent(); + Assert.AreEqual("00", traceparent.Version); + Assert.AreEqual("4bf92f3577b34da6a3ce929d0e0e4736", traceparent.TraceId); + Assert.AreEqual("00f067aa0ba902b7", traceparent.SpanId); + Assert.AreEqual("01", traceparent.TraceFlags); + } + + [TestMethod] + public void TestParseTraceParentThrowsException() + { + string wrongTraceparentString = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7"; + Assert.ThrowsException( + () => { wrongTraceparentString.ToTraceParent(); }); + } + + [TestMethod] + public void TestParseTraceParenWithNull() + { + string someString = null; + TraceParent result = someString?.ToTraceParent(); + Assert.IsNull(result); + } + } +} \ No newline at end of file diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/TelemetryActivator.cs b/test/DurableTask.AzureStorage.Tests/Correlation/TelemetryActivator.cs new file mode 100644 index 000000000..eeba4ca83 --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/TelemetryActivator.cs @@ -0,0 +1,92 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using System; + using System.Diagnostics; + using DurableTask.AzureStorage; + using DurableTask.Core; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.DependencyCollector; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Extensibility.Implementation; + + public class TelemetryActivator + { + private TelemetryClient telemetryClient; + + public void Initialize() + { + SetUpTelemetryClient(null, null); + SetUpTelemetryCallbacks(); + } + + public void Initialize(Action onSend, string instrumentationKey) + { + SetUpTelemetryClient(onSend, instrumentationKey); + SetUpTelemetryCallbacks(); + } + + void SetUpTelemetryCallbacks() + { + CorrelationTraceClient.SetUp( + (TraceContextBase requestTraceContext) => + { + requestTraceContext.Stop(); + + var requestTelemetry = requestTraceContext.CreateRequestTelemetry(); + telemetryClient.TrackRequest(requestTelemetry); + }, + (TraceContextBase dependencyTraceContext) => + { + dependencyTraceContext.Stop(); + var dependencyTelemetry = dependencyTraceContext.CreateDependencyTelemetry(); + telemetryClient.TrackDependency(dependencyTelemetry); + }, + (Exception e) => + { + telemetryClient.TrackException(e); + } + ); + } + + void SetUpTelemetryClient(Action onSend, string instrumentationKey) + { + var module = new DependencyTrackingTelemetryModule(); + // Currently it seems have a problem https://github.com/microsoft/ApplicationInsights-dotnet-server/issues/536 + module.ExcludeComponentCorrelationHttpHeadersOnDomains.Add("core.windows.net"); + module.ExcludeComponentCorrelationHttpHeadersOnDomains.Add("127.0.0.1"); + + TelemetryConfiguration config = TelemetryConfiguration.CreateDefault(); + if (onSend != null) + { + config.TelemetryChannel = new NoOpTelemetryChannel { OnSend = onSend }; + } + +#pragma warning disable 618 // DurableTaskCorrelationTelemetryIntializer() requires suppression. It is required for W3C for this System.Diagnostics version. + var telemetryInitializer = new DurableTaskCorrelationTelemetryInitializer(); + // TODO It should be suppressed by DependencyTrackingTelemetryModule, however, it doesn't work currently. + // Once the bug is fixed, remove this settings. + telemetryInitializer.ExcludeComponentCorrelationHttpHeadersOnDomains.Add("127.0.0.1"); + config.TelemetryInitializers.Add(telemetryInitializer); +#pragma warning restore 618 + module.Initialize(config); + instrumentationKey = instrumentationKey ?? Environment.GetEnvironmentVariable("APPINSIGHTS_INSTRUMENTATIONKEY"); + + telemetryClient = new TelemetryClient(config); + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/TraceContextBaseExtensions.cs b/test/DurableTask.AzureStorage.Tests/Correlation/TraceContextBaseExtensions.cs new file mode 100644 index 000000000..911614bee --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/TraceContextBaseExtensions.cs @@ -0,0 +1,59 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + using DurableTask.Core; + using Microsoft.ApplicationInsights; + using Microsoft.ApplicationInsights.DataContracts; + + public static class TraceContextBaseExtensions + { + /// + /// Create RequestTelemetry from the TraceContext + /// Currently W3C Trace contextBase is supported. + /// + /// TraceContext + /// + public static RequestTelemetry CreateRequestTelemetry(this TraceContextBase context) + { + var telemetry = new RequestTelemetry { Name = context.OperationName }; + telemetry.Duration = context.Duration; + telemetry.Timestamp = context.StartTime; + telemetry.Id = context.TelemetryId; + telemetry.Context.Operation.Id = context.TelemetryContextOperationId; + telemetry.Context.Operation.ParentId = context.TelemetryContextOperationParentId; + + return telemetry; + } + + /// + /// Create DependencyTelemetry from the Activity. + /// Currently W3C Trace contextBase is supported. + /// + /// TraceContext + /// + public static DependencyTelemetry CreateDependencyTelemetry(this TraceContextBase context) + { + var telemetry = new DependencyTelemetry { Name = context.OperationName }; + telemetry.Start(); // TODO Check if it is necessary. + telemetry.Duration = context.Duration; + telemetry.Timestamp = context.StartTime; // TimeStamp is the time of ending the Activity. + telemetry.Id = context.TelemetryId; + telemetry.Context.Operation.Id = context.TelemetryContextOperationId; + telemetry.Context.Operation.ParentId = context.TelemetryContextOperationParentId; + + return telemetry; + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/Correlation/Traceparent.cs b/test/DurableTask.AzureStorage.Tests/Correlation/Traceparent.cs new file mode 100644 index 000000000..934dc925e --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/Correlation/Traceparent.cs @@ -0,0 +1,26 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests.Correlation +{ + public class TraceParent + { + public string Version { get; set; } + + public string TraceId { get; set; } + + public string SpanId { get; set; } + + public string TraceFlags { get; set; } + } +} \ No newline at end of file diff --git a/test/DurableTask.AzureStorage.Tests/DurableTask.AzureStorage.Tests.csproj b/test/DurableTask.AzureStorage.Tests/DurableTask.AzureStorage.Tests.csproj index 07c2aa83c..c00a77698 100644 --- a/test/DurableTask.AzureStorage.Tests/DurableTask.AzureStorage.Tests.csproj +++ b/test/DurableTask.AzureStorage.Tests/DurableTask.AzureStorage.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/test/DurableTask.AzureStorage.Tests/StringExtensionsTest.cs b/test/DurableTask.AzureStorage.Tests/StringExtensionsTest.cs new file mode 100644 index 000000000..b31868bfe --- /dev/null +++ b/test/DurableTask.AzureStorage.Tests/StringExtensionsTest.cs @@ -0,0 +1,50 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.AzureStorage.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Extension methods for String Test + /// + [TestClass] + public class StringExtensionsTest + { + [TestMethod] + public void GetTargetClassName() + { + var input = "DurableTask.AzureStorage.Tests.Correlation.CorrelationScenarioTest+SayHelloActivity"; + Assert.AreEqual("SayHelloActivity", input.GetTargetClassName()); + } + + [TestMethod] + public void GetTargetClassNamePlusNotIncluded() + { + var input = "foo"; + Assert.AreEqual("foo", input.GetTargetClassName()); + } + + [TestMethod] + public void GetTargetClassNameEmptyString() + { + var input = ""; + Assert.AreEqual("", input.GetTargetClassName()); + } + } +} diff --git a/test/DurableTask.AzureStorage.Tests/TestOrchestrationClient.cs b/test/DurableTask.AzureStorage.Tests/TestOrchestrationClient.cs index 48f8e78c4..b9ac0ade9 100644 --- a/test/DurableTask.AzureStorage.Tests/TestOrchestrationClient.cs +++ b/test/DurableTask.AzureStorage.Tests/TestOrchestrationClient.cs @@ -121,6 +121,13 @@ public Task RaiseEventAsync(string eventName, object eventData) return this.client.RaiseEventAsync(instance, eventName, eventData); } + public Task RaiseEventAsync(string instanceId, string eventName, object eventData) + { + Trace.TraceInformation($"Raising event to instance {instanceId} with name = {eventName}."); + var instance = new OrchestrationInstance { InstanceId = instanceId }; + return this.client.RaiseEventAsync(instance, eventName, eventData); + } + public Task TerminateAsync(string reason) { Trace.TraceInformation($"Terminating instance {this.instanceId} with reason = {reason}."); diff --git a/test/DurableTask.Core.Tests/HttpCorrelationProtocolTraceContextTest.cs b/test/DurableTask.Core.Tests/HttpCorrelationProtocolTraceContextTest.cs new file mode 100644 index 000000000..092de451d --- /dev/null +++ b/test/DurableTask.Core.Tests/HttpCorrelationProtocolTraceContextTest.cs @@ -0,0 +1,66 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Net; + using System.Text; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class HttpCorrelationProtocolTraceContextTest + { + [TestMethod] + public void GetRootIdNormalCase() + { + var id = "|ea55fd0a-45699198bc3873c3.ea55fd0b_"; + var childId = "|ea55fd0a-45699198bc3873c3.ea55fd0b_ea55fd0c_"; + var expected = "ea55fd0a-45699198bc3873c3"; + + var traceContext = new HttpCorrelationProtocolTraceContext(); + Assert.AreEqual(expected, traceContext.GetRootId(id)); + Assert.AreEqual(expected,traceContext.GetRootId(childId)); + } + + [TestMethod] + public void GetRootIdWithNull() + { + string id = null; + var traceContext = new HttpCorrelationProtocolTraceContext(); + Assert.IsNull(traceContext.GetRootId(id)); + } + + [TestMethod] + public void GetRootIdWithMalformed() + { + // Currently it doesn't fail and doesn't throw exception. + string id = "ea55fd0a-45699198bc3873c3"; + var traceContext = new HttpCorrelationProtocolTraceContext(); + Assert.AreEqual("ea55fd0a-45699198bc3873c3", traceContext.GetRootId(id)); + } + + [TestMethod] + public void SetParentAndStartWithNullObject() + { + var traceContext = new HttpCorrelationProtocolTraceContext(); + var parentTraceContext = new NullObjectTraceContext(); + traceContext.SetParentAndStart(parentTraceContext); + Assert.AreEqual(traceContext.StartTime, traceContext.CurrentActivity.StartTimeUtc); + } + } +} diff --git a/test/DurableTask.Core.Tests/StackExtensionsTest.cs b/test/DurableTask.Core.Tests/StackExtensionsTest.cs new file mode 100644 index 000000000..d5bf0ef7f --- /dev/null +++ b/test/DurableTask.Core.Tests/StackExtensionsTest.cs @@ -0,0 +1,40 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core.Tests +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class StackExtensionsTest + { + [TestMethod] + public void CloneStack() + { + var input = new Stack(); + input.Push("1"); + input.Push("2"); + input.Push("3"); + var result = input.Clone(); + Assert.AreEqual("3", result.Pop()); + Assert.AreEqual("2", result.Pop()); + Assert.AreEqual("1", result.Pop()); + } + } +} diff --git a/test/DurableTask.Core.Tests/TraceContextBaseTest.cs b/test/DurableTask.Core.Tests/TraceContextBaseTest.cs new file mode 100644 index 000000000..be32719e6 --- /dev/null +++ b/test/DurableTask.Core.Tests/TraceContextBaseTest.cs @@ -0,0 +1,168 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class TraceContextBaseTest + { + [TestMethod] + public void SerializeTraceContextBase() + { + Foo context = new Foo(); + context.StartAsNew(); + var expectedStartTime = context.StartTime; + context.Comment = "hello"; + var json = context.SerializableTraceContext; + var result = TraceContextBase.Restore(json); + Assert.AreEqual(expectedStartTime, result.StartTime); + Assert.AreEqual(context.Comment, ((Foo)result).Comment); + } + + [TestMethod] + public void SerializeAndDeserializeTraceContextWithParent() + { + TraceContextBase context = new Foo(); + context.StartAsNew(); + var expectedStartTime = context.StartTime; + context.OrchestrationTraceContexts.Push(context); // Adding Orchestration Context it might include $type + var json = context.SerializableTraceContext; + var result = TraceContextBase.Restore(json); + Assert.AreEqual(expectedStartTime, result.StartTime); + } + + [TestMethod] + public void SerializeAndDeserializeTraceContextWithMultipleOrchestrationTraceContexts() + { + TraceContextBase one = new Foo() { Comment = "one" }; + TraceContextBase two = new Foo() { Comment = "two" }; + TraceContextBase three = new Foo() { Comment = "three" }; + one.OrchestrationTraceContexts.Push(one); + one.OrchestrationTraceContexts.Push(two); + one.OrchestrationTraceContexts.Push(three); + var json = one.SerializableTraceContext; + var restored = TraceContextBase.Restore(json); + Assert.AreEqual("three", ((Foo)restored.OrchestrationTraceContexts.Pop()).Comment); + Assert.AreEqual("two", ((Foo)restored.OrchestrationTraceContexts.Pop()).Comment); + Assert.AreEqual("one", ((Foo)restored.OrchestrationTraceContexts.Pop()).Comment); + } + + [TestMethod] + public void DeserializeScenario() + { + var json = "{ \"$id\":\"1\",\"$type\":\"DurableTask.Core.W3CTraceContext, DurableTask.Core\",\"Traceparent\":\"00-a422532de19d3e4f8f67af06f8f880c7-81354b086ec6fb41-02\",\"Tracestate\":null,\"ParentSpanId\":\"b69bc0f95af84240\",\"StartTime\":\"2019-05-03T23:43:27.6728211+00:00\",\"OrchestrationTraceContexts\":[{\"$id\":\"2\",\"$type\":\"DurableTask.Core.W3CTraceContext, DurableTask.Core\",\"Traceparent\":\"00-a422532de19d3e4f8f67af06f8f880c7-f86a8711d7226d42-02\",\"Tracestate\":null,\"ParentSpanId\":\"2ec2a64f22dbb143\",\"StartTime\":\"2019-05-03T23:43:12.7553182+00:00\",\"OrchestrationTraceContexts\":[{\"$ref\":\"2\"}]}]}"; + TraceContextBase context = TraceContextBase.Restore(json); + Assert.AreEqual(DateTimeOffset.Parse("2019-05-03T23:43:27.6728211+00:00"), context.StartTime); + } + + [TestMethod] + public void DeserializeNullScenario() + { + TraceContextBase context = TraceContextBase.Restore(null); + Assert.AreEqual(typeof(NullObjectTraceContext), context.GetType()); + } + + [TestMethod] + public void DeserializeEmptyScenario() + { + TraceContextBase context = TraceContextBase.Restore(""); + Assert.AreEqual(typeof(NullObjectTraceContext), context.GetType()); + } + + [TestMethod] + public void GetCurrentOrchestrationRequestTraceContextScenario() + { + TraceContextBase currentContext = new Foo(); + + currentContext.OrchestrationTraceContexts.Push(GetNewRequestContext("foo")); + currentContext.OrchestrationTraceContexts.Push(GetNewDependencyContext("bar")); + + var currentRequestContext = currentContext.GetCurrentOrchestrationRequestTraceContext(); + + Assert.AreEqual(TelemetryType.Request, currentRequestContext.TelemetryType); + Assert.AreEqual("foo", ((Foo)currentRequestContext).Comment); + } + + [TestMethod] + public void GetCurrentOrchestrationRequestTraceContextMultiOrchestratorScenario() + { + TraceContextBase currentContext = new Foo(); + + currentContext.OrchestrationTraceContexts.Push(GetNewRequestContext("foo")); + currentContext.OrchestrationTraceContexts.Push(GetNewDependencyContext("bar")); + currentContext.OrchestrationTraceContexts.Push(GetNewRequestContext("baz")); + currentContext.OrchestrationTraceContexts.Push(GetNewDependencyContext("qux")); + + var currentRequestContext = currentContext.GetCurrentOrchestrationRequestTraceContext(); + + Assert.AreEqual(TelemetryType.Request, currentRequestContext.TelemetryType); + Assert.AreEqual("baz", ((Foo)currentRequestContext).Comment); + } + + [TestMethod] + public void GetCurrentOrchestrationRequestTraceContextWithNoRequestTraceContextScenario() + { + TraceContextBase currentContext = new Foo(); + Assert.ThrowsException(() => currentContext.GetCurrentOrchestrationRequestTraceContext()); + } + + private static Foo GetNewRequestContext(string comment) + { + var requestContext = new Foo() { Comment = comment }; + requestContext.TelemetryType = TelemetryType.Request; + return requestContext; + } + + private static Foo GetNewDependencyContext(string comment) + { + var dependencyContext = new Foo() { Comment = comment }; + dependencyContext.TelemetryType = TelemetryType.Dependency; + return dependencyContext; + } + + class Foo : TraceContextBase + { + public Foo() : base() { } + + public string Comment { get; set; } + + public override TimeSpan Duration => TimeSpan.FromMilliseconds(10); + + public override string TelemetryId => "foo"; + + public override string TelemetryContextOperationId { get; } + + public override string TelemetryContextOperationParentId { get; } + + public override void SetParentAndStart(TraceContextBase parentTraceContext) + { + throw new NotImplementedException(); + } + + public override void StartAsNew() + { + CurrentActivity = new Activity(this.OperationName); + CurrentActivity.Start(); + StartTime = CurrentActivity.StartTimeUtc; + } + } + } +} diff --git a/test/DurableTask.Core.Tests/W3CTraceContextTest.cs b/test/DurableTask.Core.Tests/W3CTraceContextTest.cs new file mode 100644 index 000000000..04fca8b4d --- /dev/null +++ b/test/DurableTask.Core.Tests/W3CTraceContextTest.cs @@ -0,0 +1,90 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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 DurableTask.Core.Tests +{ + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Xml.Serialization; + + [TestClass] + public class W3CTraceContextTest + { + [TestMethod] + public void SetParentNormalCase() + { + var ExpectedTraceState = "congo=t61rcWkgMzE"; + var parentContext = new W3CTraceContext() + { + OperationName = "Foo", + TraceState = ExpectedTraceState + }; + parentContext.StartAsNew(); + Assert.AreEqual(ExpectedTraceState, parentContext.TraceState); + Assert.AreEqual(parentContext.CurrentActivity.Id, parentContext.TraceParent); + Assert.AreEqual(parentContext.CurrentActivity.SpanId.ToHexString(), parentContext.TelemetryId); + Assert.AreEqual(parentContext.CurrentActivity.RootId, parentContext.TelemetryContextOperationId); + Assert.AreEqual(parentContext.CurrentActivity.ParentSpanId.ToHexString(), parentContext.TelemetryContextOperationParentId); + + var childContext = new W3CTraceContext() + { + OperationName = "Bar" + }; + childContext.SetParentAndStart(parentContext); + Assert.AreEqual(ExpectedTraceState, childContext.TraceState); + Assert.AreEqual(childContext.CurrentActivity.Id, childContext.TraceParent); + Assert.AreEqual(childContext.CurrentActivity.SpanId.ToHexString(),childContext.TelemetryId); + Assert.AreEqual(childContext.CurrentActivity.RootId, childContext.TelemetryContextOperationId); + Assert.AreEqual(parentContext.CurrentActivity.SpanId.ToHexString(), childContext.ParentSpanId); + Assert.AreEqual(parentContext.CurrentActivity.SpanId.ToHexString(), childContext.TelemetryContextOperationParentId); + } + + [TestMethod] + public void RestoredSoThatNoCurrentActivity() + { + var ExpectedTraceState = "congo=t61rcWkgMzE"; + var ExpectedSpanId = "b7ad6b7169203331"; + var ExpectedRootId = "0af7651916cd43dd8448eb211c80319c"; + var ExpectedTraceParent = $"00-{ExpectedRootId}-{ExpectedSpanId}-01"; + var ExpectedParentSpanId = "00f067aa0ba902b7"; + var context = new W3CTraceContext() + { + OperationName = "Foo", + TraceState = ExpectedTraceState, + TraceParent = ExpectedTraceParent, + ParentSpanId = ExpectedParentSpanId, + TelemetryType = TelemetryType.Request + }; + Assert.AreEqual(ExpectedTraceState, context.TraceState); + Assert.AreEqual(ExpectedTraceParent, context.TraceParent); + Assert.AreEqual(ExpectedSpanId, context.TelemetryId); + Assert.AreEqual(ExpectedRootId, context.TelemetryContextOperationId); + Assert.AreEqual(ExpectedParentSpanId, context.TelemetryContextOperationParentId); + } + + // SetParent sometimes accept NullObject + [TestMethod] + public void SetParentWithNullObject() + { + var traceContext = new W3CTraceContext(); + var parentTraceContext = new NullObjectTraceContext(); + traceContext.SetParentAndStart(parentTraceContext); + Assert.AreEqual(traceContext.StartTime, traceContext.CurrentActivity.StartTimeUtc); + } + } +}