Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions src/WebJobs.Extensions.DurableTask/Correlation/Schema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,29 @@ internal static class Status
internal static class SpanNames
{
internal static string CallOrSignalEntity(string name, string operation)
=> $"{TraceActivityConstants.Entity}:{name}:{operation}";

internal static string EntityStartsAnOrchestration(string name)
=> $"{TraceActivityConstants.Entity}:{name}:{operation}";

internal static string CallOrSignalEntity(string name, string operation, string? instanceId, bool includeInstanceId)
=> includeInstanceId && !string.IsNullOrEmpty(instanceId)
? $"{TraceActivityConstants.Entity}:{name}:{operation} ({instanceId})"
: CallOrSignalEntity(name, operation);

internal static string EntityStartsAnOrchestration(string name)
=> $"{name}:{TraceActivityConstants.CreateOrchestration}";

internal static string EntityStartsAnOrchestration(string name, string? instanceId, bool includeInstanceId)
=> includeInstanceId && !string.IsNullOrEmpty(instanceId)
? $"{name}:{TraceActivityConstants.CreateOrchestration} ({instanceId})"
: EntityStartsAnOrchestration(name);

internal static string CreateOrchestration(string name, string? version)
=> FormatName(TraceActivityConstants.CreateOrchestration, name, version);

internal static string CreateOrchestration(string name, string? version, string? instanceId, bool includeInstanceId)
=> includeInstanceId && !string.IsNullOrEmpty(instanceId)
? $"{FormatName(TraceActivityConstants.CreateOrchestration, name, version)} ({instanceId})"
: CreateOrchestration(name, version);

private static string FormatName(string prefix, string name, string? version)
=> string.IsNullOrEmpty(version) ? $"{prefix}:{name}" : $"{prefix}:{name}@{version}";
}
Expand Down
20 changes: 11 additions & 9 deletions src/WebJobs.Extensions.DurableTask/Correlation/TraceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ internal class TraceHelper
private const string Source = "WebJobs.Extensions.DurableTask";

private static readonly ActivitySource ActivityTraceSource = new ActivitySource(Source);

internal static Activity? StartActivityForNewOrchestration(ExecutionStartedEvent startEvent, ActivityContext parentTraceContext, DateTimeOffset? startTime = default)
internal static Activity? StartActivityForNewOrchestration(ExecutionStartedEvent startEvent, ActivityContext parentTraceContext, DateTimeOffset? startTime = default, bool includeInstanceIdInOperationName = false)
{
string instanceId = startEvent.OrchestrationInstance.InstanceId;

// Start the new activity to represent scheduling the orchestration
Activity? newActivity = ActivityTraceSource.StartActivity(
Schema.SpanNames.CreateOrchestration(startEvent.Name, startEvent.Version),
kind: ActivityKind.Producer,
Schema.SpanNames.CreateOrchestration(startEvent.Name, startEvent.Version, instanceId, includeInstanceIdInOperationName),
kind: ActivityKind.Producer,
parentContext: parentTraceContext,
startTime: startTime ?? default);
startTime: startTime ?? default);

if (newActivity == null)
{
Expand All @@ -47,8 +49,8 @@ internal class TraceHelper

return newActivity;
}

internal static Activity? StartActivityForCallingOrSignalingEntity(string targetEntityId, string entityName, string operationName, bool signalEntity, DateTime? scheduledTime, ActivityContext? parentTraceContext, DateTimeOffset? startTime = default, string? entityId = null)
internal static Activity? StartActivityForCallingOrSignalingEntity(string targetEntityId, string entityName, string operationName, bool signalEntity, DateTime? scheduledTime, ActivityContext? parentTraceContext, DateTimeOffset? startTime = default, string? entityId = null)
{
// We only want to create a trace activity for calling or signaling an entity in the case that we can successfully get the parent trace context of the request.
// Otherwise, we will create an unlinked trace activity with no parent.
Expand All @@ -60,8 +62,8 @@ internal class TraceHelper
Activity? newActivity = ActivityTraceSource.StartActivity(
Schema.SpanNames.CallOrSignalEntity(entityName, operationName),
kind: signalEntity ? ActivityKind.Producer : ActivityKind.Client,
parentContext: parentTraceContext.Value,
startTime: startTime ?? default);
parentContext: parentTraceContext.Value,
startTime: startTime ?? default);

if (newActivity == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ private static T CreateTelemetryCore<T>(Activity activity)
};

telemetry.Context.Operation.Id = activity.RootId;

// Set Operation.Name for proper grouping in Application Insights.
// The display name may include instance ID if IncludeInstanceIdInOperationName is enabled.
telemetry.Context.Operation.Name = activity.DisplayName;

ActivitySpanId parentId = activity.ParentSpanId;
if (parentId != default)
{
Expand Down
4 changes: 2 additions & 2 deletions src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -904,11 +904,11 @@ private async Task<HttpResponseMessage> HandleStartOrchestratorRequestAsync(
if (traceParent != null)
{
ActivityContext.TryParse(traceParent, traceState, out ActivityContext parentActivityContext);
using Activity scheduleOrchestrationActivity = TraceHelper.StartActivityForNewOrchestration(executionStartedEvent, parentActivityContext);
using Activity scheduleOrchestrationActivity = TraceHelper.StartActivityForNewOrchestration(executionStartedEvent, parentActivityContext, includeInstanceIdInOperationName: this.durableTaskOptions.Tracing.IncludeInstanceIdInOperationName);
}
else
{
using Activity scheduleOrchestrationActivity = TraceHelper.StartActivityForNewOrchestration(executionStartedEvent, default);
using Activity scheduleOrchestrationActivity = TraceHelper.StartActivityForNewOrchestration(executionStartedEvent, default, includeInstanceIdInOperationName: this.durableTaskOptions.Tracing.IncludeInstanceIdInOperationName);
}

await durableClient.DurabilityProvider.CreateTaskOrchestrationAsync(
Expand Down
15 changes: 15 additions & 0 deletions src/WebJobs.Extensions.DurableTask/Options/TraceOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ public class TraceOptions
/// </summary>
public DurableDistributedTracingVersion Version { get; set; } = DurableDistributedTracingVersion.V1;

/// <summary>
/// Gets or sets a value indicating whether to include the orchestration instance ID in the
/// Application Insights operation name (operation_Name).
/// </summary>
/// <remarks>
/// When set to <c>true</c>, the orchestration instance ID will be appended to the operation name
/// in the format "{FunctionName} ({InstanceId})". This allows for better grouping and filtering
/// of failures in Application Insights by individual orchestration instance.
/// The default value is <c>false</c> to maintain backward compatibility.
/// </remarks>
/// <value>
/// <c>true</c> to include the instance ID in the operation name; <c>false</c> otherwise.
/// </value>
public bool IncludeInstanceIdInOperationName { get; set; } = false;

internal void AddToDebugString(StringBuilder builder)
{
builder.Append(nameof(this.TraceReplayEvents)).Append(": ").Append(this.TraceReplayEvents).Append(", ");
Expand Down
8 changes: 4 additions & 4 deletions src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public override Task<Empty> Hello(Empty request, ServerCallContext context)

// Create a new activity with the parent context
ActivityContext.TryParse(traceParent, traceState, out ActivityContext parentActivityContext);
using Activity? scheduleOrchestrationActivity = TraceHelper.StartActivityForNewOrchestration(executionStartedEvent, parentActivityContext, request.RequestTime?.ToDateTimeOffset());
using Activity? scheduleOrchestrationActivity = TraceHelper.StartActivityForNewOrchestration(executionStartedEvent, parentActivityContext, request.RequestTime?.ToDateTimeOffset(), this.extension.Options.Tracing.IncludeInstanceIdInOperationName);

// Schedule the orchestration
await this.GetDurabilityProvider(context).CreateTaskOrchestrationAsync(
Expand Down Expand Up @@ -301,11 +301,11 @@ private OrchestrationStatus[] GetStatusesNotToOverride()

public async override Task<P.RewindInstanceResponse> RewindInstance(P.RewindInstanceRequest request, ServerCallContext context)
{
try
{
try
{
#pragma warning disable CS0618 // Type or member is obsolete
await this.GetClient(context).RewindAsync(request.InstanceId, request.Reason);
#pragma warning restore CS0618 // Type or member is obsolete
#pragma warning restore CS0618 // Type or member is obsolete
}
catch (ArgumentException ex)
{
Expand Down
217 changes: 217 additions & 0 deletions test/Common/SchemaSpanNamesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using Microsoft.Azure.WebJobs.Extensions.DurableTask.Correlation;
using Xunit;

namespace Microsoft.Azure.WebJobs.Extensions.DurableTask.Tests
{
/// <summary>
/// Unit tests for the Schema.SpanNames class.
/// </summary>
public class SchemaSpanNamesTests
{
[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CreateOrchestration_WithoutInstanceId_ReturnsExpectedFormat()
{
// Arrange
string name = "MyOrchestration";
string version = null;

// Act
string result = Schema.SpanNames.CreateOrchestration(name, version);

// Assert
Assert.Equal($"{TraceActivityConstants.CreateOrchestration}:{name}", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CreateOrchestration_WithVersion_ReturnsExpectedFormat()
{
// Arrange
string name = "MyOrchestration";
string version = "1.0";

// Act
string result = Schema.SpanNames.CreateOrchestration(name, version);

// Assert
Assert.Equal($"{TraceActivityConstants.CreateOrchestration}:{name}@{version}", result);
}

[Theory]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
[InlineData(true, "test-instance-123")]
[InlineData(true, "abc-def-ghi")]
public void CreateOrchestration_WithIncludeInstanceIdTrue_IncludesInstanceId(bool includeInstanceId, string instanceId)
{
// Arrange
string name = "MyOrchestration";
string version = null;

// Act
string result = Schema.SpanNames.CreateOrchestration(name, version, instanceId, includeInstanceId);

// Assert
Assert.Equal($"{TraceActivityConstants.CreateOrchestration}:{name} ({instanceId})", result);
}

[Theory]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
[InlineData(false, "test-instance-123")]
[InlineData(false, "abc-def-ghi")]
public void CreateOrchestration_WithIncludeInstanceIdFalse_ExcludesInstanceId(bool includeInstanceId, string instanceId)
{
// Arrange
string name = "MyOrchestration";
string version = null;

// Act
string result = Schema.SpanNames.CreateOrchestration(name, version, instanceId, includeInstanceId);

// Assert
Assert.Equal($"{TraceActivityConstants.CreateOrchestration}:{name}", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CreateOrchestration_WithNullInstanceId_ExcludesInstanceId()
{
// Arrange
string name = "MyOrchestration";
string version = null;
string instanceId = null;

// Act
string result = Schema.SpanNames.CreateOrchestration(name, version, instanceId, includeInstanceId: true);

// Assert
Assert.Equal($"{TraceActivityConstants.CreateOrchestration}:{name}", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CreateOrchestration_WithEmptyInstanceId_ExcludesInstanceId()
{
// Arrange
string name = "MyOrchestration";
string version = null;
string instanceId = string.Empty;

// Act
string result = Schema.SpanNames.CreateOrchestration(name, version, instanceId, includeInstanceId: true);

// Assert
Assert.Equal($"{TraceActivityConstants.CreateOrchestration}:{name}", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CreateOrchestration_WithVersionAndInstanceId_ReturnsExpectedFormat()
{
// Arrange
string name = "MyOrchestration";
string version = "1.0";
string instanceId = "test-instance-123";

// Act
string result = Schema.SpanNames.CreateOrchestration(name, version, instanceId, includeInstanceId: true);

// Assert
Assert.Equal($"{TraceActivityConstants.CreateOrchestration}:{name}@{version} ({instanceId})", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CallOrSignalEntity_WithoutInstanceId_ReturnsExpectedFormat()
{
// Arrange
string name = "Counter";
string operation = "Add";

// Act
string result = Schema.SpanNames.CallOrSignalEntity(name, operation);

// Assert
Assert.Equal($"{TraceActivityConstants.Entity}:{name}:{operation}", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CallOrSignalEntity_WithIncludeInstanceIdTrue_IncludesInstanceId()
{
// Arrange
string name = "Counter";
string operation = "Add";
string instanceId = "entity-instance-123";

// Act
string result = Schema.SpanNames.CallOrSignalEntity(name, operation, instanceId, includeInstanceId: true);

// Assert
Assert.Equal($"{TraceActivityConstants.Entity}:{name}:{operation} ({instanceId})", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void CallOrSignalEntity_WithIncludeInstanceIdFalse_ExcludesInstanceId()
{
// Arrange
string name = "Counter";
string operation = "Add";
string instanceId = "entity-instance-123";

// Act
string result = Schema.SpanNames.CallOrSignalEntity(name, operation, instanceId, includeInstanceId: false);

// Assert
Assert.Equal($"{TraceActivityConstants.Entity}:{name}:{operation}", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void EntityStartsAnOrchestration_WithoutInstanceId_ReturnsExpectedFormat()
{
// Arrange
string name = "Counter";

// Act
string result = Schema.SpanNames.EntityStartsAnOrchestration(name);

// Assert
Assert.Equal($"{name}:{TraceActivityConstants.CreateOrchestration}", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void EntityStartsAnOrchestration_WithIncludeInstanceIdTrue_IncludesInstanceId()
{
// Arrange
string name = "Counter";
string instanceId = "orch-instance-123";

// Act
string result = Schema.SpanNames.EntityStartsAnOrchestration(name, instanceId, includeInstanceId: true);

// Assert
Assert.Equal($"{name}:{TraceActivityConstants.CreateOrchestration} ({instanceId})", result);
}

[Fact]
[Trait("Category", PlatformSpecificHelpers.TestCategory)]
public void EntityStartsAnOrchestration_WithIncludeInstanceIdFalse_ExcludesInstanceId()
{
// Arrange
string name = "Counter";
string instanceId = "orch-instance-123";

// Act
string result = Schema.SpanNames.EntityStartsAnOrchestration(name, instanceId, includeInstanceId: false);

// Assert
Assert.Equal($"{name}:{TraceActivityConstants.CreateOrchestration}", result);
}
}
}
Loading