Skip to content

Commit 745bf47

Browse files
authored
Basic orchestration and activity execution logs (#405)
1 parent 067bc45 commit 745bf47

File tree

9 files changed

+995
-807
lines changed

9 files changed

+995
-807
lines changed

src/Worker/Core/Logs.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,48 @@ namespace Microsoft.DurableTask
99
/// <summary>
1010
/// Log messages.
1111
/// </summary>
12-
/// <remarks>
13-
/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs.
14-
/// </remarks>
1512
static partial class Logs
1613
{
1714
[LoggerMessage(EventId = 15, Level = LogLevel.Error, Message = "Unhandled exception in entity operation {entityInstanceId}/{operationName}.")]
1815
public static partial void OperationError(this ILogger logger, Exception ex, EntityInstanceId entityInstanceId, string operationName);
1916

2017
[LoggerMessage(EventId = 55, Level = LogLevel.Information, Message = "{instanceId}: Evaluating custom retry handler for failed '{name}' task. Attempt = {attempt}.")]
2118
public static partial void RetryingTask(this ILogger logger, string instanceId, string name, int attempt);
19+
20+
[LoggerMessage(EventId = 600, Level = LogLevel.Information, Message = "'{Name}' orchestration with ID '{InstanceId}' started.")]
21+
public static partial void OrchestrationStarted(this ILogger logger, string instanceId, string name);
22+
23+
[LoggerMessage(EventId = 601, Level = LogLevel.Information, Message = "'{Name}' orchestration with ID '{InstanceId}' completed.")]
24+
public static partial void OrchestrationCompleted(this ILogger logger, string instanceId, string name);
25+
26+
[LoggerMessage(EventId = 602, Level = LogLevel.Information, Message = "'{Name}' orchestration with ID '{InstanceId}' failed.")]
27+
public static partial void OrchestrationFailed(this ILogger logger, Exception ex, string instanceId, string name);
28+
29+
[LoggerMessage(EventId = 603, Level = LogLevel.Information, Message = "'{Name}' activity of orchestration ID '{InstanceId}' started.")]
30+
public static partial void ActivityStarted(this ILogger logger, string instanceId, string name);
31+
32+
[LoggerMessage(EventId = 604, Level = LogLevel.Information, Message = "'{Name}' activity of orchestration ID '{InstanceId}' completed.")]
33+
public static partial void ActivityCompleted(this ILogger logger, string instanceId, string name);
34+
35+
[LoggerMessage(EventId = 605, Level = LogLevel.Information, Message = "'{Name}' activity of orchestration ID '{InstanceId}' failed.")]
36+
public static partial void ActivityFailed(this ILogger logger, Exception ex, string instanceId, string name);
37+
38+
/// <summary>
39+
/// Creates a logger named "Microsoft.DurableTask.Worker" with an optional subcategory.
40+
/// </summary>
41+
/// <param name="loggerFactory">The logger factory to use to create the logger.</param>
42+
/// <param name="subcategory">The subcategory of the logger. For example, "Activities" or "Orchestrations".
43+
/// </param>
44+
/// <returns>The generated <see cref="ILogger"/>.</returns>
45+
internal static ILogger CreateWorkerLogger(ILoggerFactory loggerFactory, string? subcategory = null)
46+
{
47+
string categoryName = "Microsoft.DurableTask.Worker";
48+
if (!string.IsNullOrEmpty(subcategory))
49+
{
50+
categoryName += "." + subcategory;
51+
}
52+
53+
return loggerFactory.CreateLogger(categoryName);
54+
}
2255
}
2356
}

src/Worker/Core/Shims/DurableTaskShimFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public TaskActivity CreateActivity(TaskName name, ITaskActivity activity)
5050
{
5151
Check.NotDefault(name);
5252
Check.NotNull(activity);
53-
return new TaskActivityShim(this.options.DataConverter, name, activity);
53+
return new TaskActivityShim(this.loggerFactory, this.options.DataConverter, name, activity);
5454
}
5555

5656
/// <summary>

src/Worker/Core/Shims/TaskActivityShim.cs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using DurableTask.Core;
5+
using Microsoft.Extensions.Logging;
56

67
namespace Microsoft.DurableTask.Worker.Shims;
78

@@ -11,17 +12,24 @@ namespace Microsoft.DurableTask.Worker.Shims;
1112
class TaskActivityShim : TaskActivity
1213
{
1314
readonly ITaskActivity implementation;
15+
readonly ILogger logger;
1416
readonly DataConverter dataConverter;
1517
readonly TaskName name;
1618

1719
/// <summary>
1820
/// Initializes a new instance of the <see cref="TaskActivityShim"/> class.
1921
/// </summary>
22+
/// <param name="loggerFactory">The logger factory.</param>
2023
/// <param name="dataConverter">The data converter.</param>
2124
/// <param name="name">The name of the activity.</param>
2225
/// <param name="implementation">The activity implementation to wrap.</param>
23-
public TaskActivityShim(DataConverter dataConverter, TaskName name, ITaskActivity implementation)
26+
public TaskActivityShim(
27+
ILoggerFactory loggerFactory,
28+
DataConverter dataConverter,
29+
TaskName name,
30+
ITaskActivity implementation)
2431
{
32+
this.logger = Logs.CreateWorkerLogger(Check.NotNull(loggerFactory), "Activities");
2533
this.dataConverter = Check.NotNull(dataConverter);
2634
this.name = Check.NotDefault(name);
2735
this.implementation = Check.NotNull(implementation);
@@ -34,11 +42,25 @@ public TaskActivityShim(DataConverter dataConverter, TaskName name, ITaskActivit
3442
string? strippedRawInput = StripArrayCharacters(rawInput);
3543
object? deserializedInput = this.dataConverter.Deserialize(strippedRawInput, this.implementation.InputType);
3644
TaskActivityContextWrapper contextWrapper = new(coreContext, this.name);
37-
object? output = await this.implementation.RunAsync(contextWrapper, deserializedInput);
3845

39-
// Return the output (if any) as a serialized string.
40-
string? serializedOutput = this.dataConverter.Serialize(output);
41-
return serializedOutput;
46+
string instanceId = coreContext.OrchestrationInstance.InstanceId;
47+
this.logger.ActivityStarted(instanceId, this.name);
48+
49+
try
50+
{
51+
object? output = await this.implementation.RunAsync(contextWrapper, deserializedInput);
52+
53+
// Return the output (if any) as a serialized string.
54+
string? serializedOutput = this.dataConverter.Serialize(output);
55+
this.logger.ActivityCompleted(instanceId, this.name);
56+
57+
return serializedOutput;
58+
}
59+
catch (Exception e)
60+
{
61+
this.logger.ActivityFailed(e, instanceId, this.name);
62+
throw;
63+
}
4264
}
4365

4466
/// <inheritdoc/>

src/Worker/Core/Shims/TaskOrchestrationShim.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ partial class TaskOrchestrationShim : TaskOrchestration
1818
{
1919
readonly ITaskOrchestrator implementation;
2020
readonly OrchestrationInvocationContext invocationContext;
21+
readonly ILogger logger;
22+
2123
TaskOrchestrationContextWrapper? wrapperContext;
2224

2325
/// <summary>
@@ -31,12 +33,12 @@ public TaskOrchestrationShim(
3133
{
3234
this.invocationContext = Check.NotNull(invocationContext);
3335
this.implementation = Check.NotNull(implementation);
36+
37+
this.logger = Logs.CreateWorkerLogger(this.invocationContext.LoggerFactory, "Orchestrations");
3438
}
3539

3640
DataConverter DataConverter => this.invocationContext.Options.DataConverter;
3741

38-
ILoggerFactory LoggerFactory => this.invocationContext.LoggerFactory;
39-
4042
/// <inheritdoc/>
4143
public override async Task<string?> Execute(OrchestrationContext innerContext, string rawInput)
4244
{
@@ -48,15 +50,31 @@ public TaskOrchestrationShim(
4850
object? input = this.DataConverter.Deserialize(rawInput, this.implementation.InputType);
4951
this.wrapperContext = new(innerContext, this.invocationContext, input);
5052

53+
string instanceId = innerContext.OrchestrationInstance.InstanceId;
54+
if (!innerContext.IsReplaying)
55+
{
56+
this.logger.OrchestrationStarted(instanceId, this.invocationContext.Name);
57+
}
58+
5159
try
5260
{
5361
object? output = await this.implementation.RunAsync(this.wrapperContext, input);
5462

63+
if (!innerContext.IsReplaying)
64+
{
65+
this.logger.OrchestrationCompleted(instanceId, this.invocationContext.Name);
66+
}
67+
5568
// Return the output (if any) as a serialized string.
5669
return this.DataConverter.Serialize(output);
5770
}
5871
catch (TaskFailedException e)
5972
{
73+
if (!innerContext.IsReplaying)
74+
{
75+
this.logger.OrchestrationFailed(e, instanceId, this.invocationContext.Name);
76+
}
77+
6078
// Convert back to something the Durable Task Framework natively understands so that
6179
// failure details are correctly propagated.
6280
throw new CoreTaskFailedException(e.Message, e.InnerException)

src/Worker/Grpc/Logs.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ namespace Microsoft.DurableTask.Worker.Grpc
88
/// <summary>
99
/// Log messages.
1010
/// </summary>
11-
/// <remarks>
12-
/// NOTE: Trying to make logs consistent with https://github.com/Azure/durabletask/blob/main/src/DurableTask.Core/Logging/LogEvents.cs.
13-
/// </remarks>
1411
static partial class Logs
1512
{
1613
[LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "Durable Task gRPC worker starting and connecting to {endpoint}.")]

test/Grpc.IntegrationTests/IntegrationTestBase.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,61 @@ protected IReadOnlyCollection<LogEntry> GetLogs()
9090
return logs;
9191
}
9292

93+
protected IReadOnlyCollection<LogEntry> GetLogs(string category)
94+
{
95+
this.logProvider.TryGetLogs(category, out IReadOnlyCollection<LogEntry> logs);
96+
return logs ?? [];
97+
}
98+
99+
protected static bool MatchLog(
100+
LogEntry log,
101+
string logEventName,
102+
(Type exceptionType, string exceptionMessage)? exception,
103+
params (string key, string value)[] metadataPairs)
104+
{
105+
if (log.EventId.Name != logEventName)
106+
{
107+
return false;
108+
}
109+
110+
if (log.Exception is null != exception is null)
111+
{
112+
return false;
113+
}
114+
else if (exception is not null)
115+
{
116+
if (log.Exception?.GetType() != exception.Value.exceptionType)
117+
{
118+
return false;
119+
}
120+
if (log.Exception?.Message != exception.Value.exceptionMessage)
121+
{
122+
return false;
123+
}
124+
}
125+
126+
if (log.State is not IReadOnlyCollection<KeyValuePair<string, object>> metadataList)
127+
{
128+
return false;
129+
}
130+
131+
Dictionary<string, object> state = new(metadataList);
132+
foreach ((string key, string value) in metadataPairs)
133+
{
134+
if (!state.TryGetValue(key, out object? stateValue))
135+
{
136+
return false;
137+
}
138+
139+
if (stateValue is not string stateString || stateString != value)
140+
{
141+
return false;
142+
}
143+
}
144+
145+
return true;
146+
}
147+
93148
protected struct HostTestLifetime : IAsyncDisposable
94149
{
95150
readonly IHost host;

test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Runtime.Serialization;
55
using Microsoft.DurableTask.Client;
6+
using Microsoft.DurableTask.Tests.Logging;
67
using Microsoft.DurableTask.Worker;
78
using Xunit.Abstractions;
89
using Xunit.Sdk;
@@ -28,8 +29,11 @@ public async Task UnhandledActivityException()
2829
TaskName activityName = "FaultyActivity";
2930

3031
// Use local function definitions to simplify the validation of the call stacks
31-
async Task MyOrchestrationImpl(TaskOrchestrationContext ctx) => await ctx.CallActivityAsync(activityName);
32-
void MyActivityImpl(TaskActivityContext ctx) => throw new Exception(errorMessage, new CustomException("Inner!"));
32+
async Task MyOrchestrationImpl(TaskOrchestrationContext ctx) =>
33+
await ctx.CallActivityAsync(activityName);
34+
35+
void MyActivityImpl(TaskActivityContext ctx) =>
36+
throw new InvalidOperationException(errorMessage, new CustomException("Inner!"));
3337

3438
await using HostTestLifetime server = await this.StartWorkerAsync(b =>
3539
{
@@ -64,13 +68,45 @@ public async Task UnhandledActivityException()
6468

6569
// Check that the inner exception - i.e. the exact exception that failed the orchestration - was populated correctly
6670
Assert.NotNull(failureDetails.InnerFailure);
67-
Assert.Equal(typeof(Exception).FullName, failureDetails.InnerFailure!.ErrorType);
71+
Assert.Equal(typeof(InvalidOperationException).FullName, failureDetails.InnerFailure!.ErrorType);
6872
Assert.Equal(errorMessage, failureDetails.InnerFailure.ErrorMessage);
6973

7074
// Check that the inner-most exception was populated correctly too (the custom exception type)
7175
Assert.NotNull(failureDetails.InnerFailure.InnerFailure);
7276
Assert.Equal(typeof(CustomException).FullName, failureDetails.InnerFailure.InnerFailure!.ErrorType);
7377
Assert.Equal("Inner!", failureDetails.InnerFailure.InnerFailure.ErrorMessage);
78+
79+
IReadOnlyCollection<LogEntry> workerLogs = this.GetLogs(category: "Microsoft.DurableTask.Worker");
80+
Assert.NotEmpty(workerLogs);
81+
82+
// Check that the orchestrator and activity logs are present
83+
Assert.Single(workerLogs, log => MatchLog(
84+
log,
85+
logEventName: "OrchestrationStarted",
86+
exception: null,
87+
("InstanceId", instanceId),
88+
("Name", orchestratorName.Name)));
89+
90+
Assert.Single(workerLogs, log => MatchLog(
91+
log,
92+
logEventName: "ActivityStarted",
93+
exception: null,
94+
("InstanceId", instanceId),
95+
("Name", activityName.Name)));
96+
97+
Assert.Single(workerLogs, log => MatchLog(
98+
log,
99+
logEventName: "ActivityFailed",
100+
exception: (typeof(InvalidOperationException), errorMessage),
101+
("InstanceId", instanceId),
102+
("Name", activityName.Name)));
103+
104+
Assert.Single(workerLogs, log => MatchLog(
105+
log,
106+
logEventName: "OrchestrationFailed",
107+
exception: (typeof(TaskFailedException), $"Task '{activityName}' (#0) failed with an unhandled exception: {errorMessage}"),
108+
("InstanceId", instanceId),
109+
("Name", orchestratorName.Name)));
74110
}
75111

76112
/// <summary>

0 commit comments

Comments
 (0)