Skip to content

Commit 4f48371

Browse files
CopilotYunchuWang
andcommitted
Investigate and understand test failure caused by dual-category logging
Co-authored-by: YunchuWang <[email protected]>
1 parent 7b5631c commit 4f48371

File tree

5 files changed

+471
-2
lines changed

5 files changed

+471
-2
lines changed

src/Worker/Core/DurableTaskWorkerOptions.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ public DataConverter DataConverter
151151
[Obsolete("Experimental")]
152152
public IOrchestrationFilter? OrchestrationFilter { get; set; }
153153

154+
/// <summary>
155+
/// Gets options for the Durable Task worker logging.
156+
/// </summary>
157+
/// <remarks>
158+
/// <para>
159+
/// Logging options control how logging categories are assigned to different components of the worker.
160+
/// Starting from a future version, more specific logging categories will be used for better log filtering.
161+
/// </para><para>
162+
/// To maintain backward compatibility, legacy logging categories are emitted by default alongside the new
163+
/// categories. This can be disabled by setting <see cref="LoggingOptions.UseLegacyCategories" /> to false.
164+
/// </para>
165+
/// </remarks>
166+
public LoggingOptions Logging { get; } = new();
167+
154168
/// <summary>
155169
/// Gets a value indicating whether <see cref="DataConverter" /> was explicitly set or not.
156170
/// </summary>
@@ -177,6 +191,7 @@ internal void ApplyTo(DurableTaskWorkerOptions other)
177191
other.EnableEntitySupport = this.EnableEntitySupport;
178192
other.Versioning = this.Versioning;
179193
other.OrchestrationFilter = this.OrchestrationFilter;
194+
other.Logging.UseLegacyCategories = this.Logging.UseLegacyCategories;
180195
}
181196
}
182197

@@ -229,4 +244,51 @@ public class VersioningOptions
229244
/// </remarks>
230245
public VersionFailureStrategy FailureStrategy { get; set; } = VersionFailureStrategy.Reject;
231246
}
247+
248+
/// <summary>
249+
/// Options for the Durable Task worker logging.
250+
/// </summary>
251+
/// <remarks>
252+
/// <para>
253+
/// These options control how logging categories are assigned to different components of the worker.
254+
/// Starting from a future version, more specific logging categories will be used for better log filtering:
255+
/// <list type="bullet">
256+
/// <item><description><c>Microsoft.DurableTask.Worker.Grpc</c> for gRPC worker logs (previously <c>Microsoft.DurableTask</c>)</description></item>
257+
/// <item><description><c>Microsoft.DurableTask.Worker.*</c> for worker-specific logs</description></item>
258+
/// </list>
259+
/// </para><para>
260+
/// To maintain backward compatibility, legacy logging categories are emitted by default alongside the new
261+
/// categories until a future major release. This ensures existing log filters continue to work.
262+
/// </para><para>
263+
/// <b>Migration Path:</b>
264+
/// <list type="number">
265+
/// <item><description>Update your log filters to use the new, more specific categories</description></item>
266+
/// <item><description>Test your application to ensure logs are captured correctly</description></item>
267+
/// <item><description>Once confident, set <see cref="UseLegacyCategories" /> to <c>false</c> to disable legacy category emission</description></item>
268+
/// </list>
269+
/// </para>
270+
/// </remarks>
271+
public class LoggingOptions
272+
{
273+
/// <summary>
274+
/// Gets or sets a value indicating whether to emit logs using legacy logging categories in addition to new categories.
275+
/// </summary>
276+
/// <remarks>
277+
/// <para>
278+
/// When <c>true</c> (default), logs are emitted to both the new specific categories (e.g., <c>Microsoft.DurableTask.Worker.Grpc</c>)
279+
/// and the legacy broad categories (e.g., <c>Microsoft.DurableTask</c>). This ensures backward compatibility with existing
280+
/// log filters and queries.
281+
/// </para><para>
282+
/// When <c>false</c>, logs are only emitted to the new specific categories, which provides better log organization
283+
/// and filtering capabilities.
284+
/// </para><para>
285+
/// <b>Default:</b> <c>true</c> (legacy categories are enabled for backward compatibility)
286+
/// </para><para>
287+
/// <b>Breaking Change Warning:</b> Setting this to <c>false</c> is a breaking change if you have existing log filters,
288+
/// queries, or monitoring rules that depend on the legacy category names. Ensure you update those before disabling
289+
/// legacy categories.
290+
/// </para>
291+
/// </remarks>
292+
public bool UseLegacyCategories { get; set; } = true;
293+
}
232294
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace Microsoft.DurableTask.Worker.Grpc;
7+
8+
/// <summary>
9+
/// A logger wrapper that emits logs to both a primary (new) category and an optional legacy category.
10+
/// </summary>
11+
/// <remarks>
12+
/// This logger is used to maintain backward compatibility while transitioning to more specific logging categories.
13+
/// When legacy categories are enabled, log messages are written to both the new specific category
14+
/// (e.g., "Microsoft.DurableTask.Worker.Grpc") and the legacy broad category (e.g., "Microsoft.DurableTask").
15+
/// </remarks>
16+
sealed class DualCategoryLogger : ILogger
17+
{
18+
readonly ILogger primaryLogger;
19+
readonly ILogger? legacyLogger;
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="DualCategoryLogger"/> class.
23+
/// </summary>
24+
/// <param name="primaryLogger">The primary logger with the new category.</param>
25+
/// <param name="legacyLogger">The optional legacy logger with the old category.</param>
26+
public DualCategoryLogger(ILogger primaryLogger, ILogger? legacyLogger)
27+
{
28+
this.primaryLogger = Check.NotNull(primaryLogger);
29+
this.legacyLogger = legacyLogger;
30+
}
31+
32+
/// <inheritdoc/>
33+
public IDisposable? BeginScope<TState>(TState state)
34+
where TState : notnull
35+
{
36+
IDisposable? primaryScope = this.primaryLogger.BeginScope(state);
37+
IDisposable? legacyScope = this.legacyLogger?.BeginScope(state);
38+
39+
if (primaryScope is not null && legacyScope is not null)
40+
{
41+
return new CompositeDisposable(primaryScope, legacyScope);
42+
}
43+
44+
return primaryScope ?? legacyScope;
45+
}
46+
47+
/// <inheritdoc/>
48+
public bool IsEnabled(LogLevel logLevel)
49+
{
50+
// Return true if either logger is enabled at this level
51+
return this.primaryLogger.IsEnabled(logLevel) ||
52+
(this.legacyLogger?.IsEnabled(logLevel) ?? false);
53+
}
54+
55+
/// <inheritdoc/>
56+
public void Log<TState>(
57+
LogLevel logLevel,
58+
EventId eventId,
59+
TState state,
60+
Exception? exception,
61+
Func<TState, Exception?, string> formatter)
62+
{
63+
// Log to primary logger
64+
if (this.primaryLogger.IsEnabled(logLevel))
65+
{
66+
this.primaryLogger.Log(logLevel, eventId, state, exception, formatter);
67+
}
68+
69+
// Log to legacy logger if enabled
70+
if (this.legacyLogger?.IsEnabled(logLevel) ?? false)
71+
{
72+
this.legacyLogger.Log(logLevel, eventId, state, exception, formatter);
73+
}
74+
}
75+
76+
sealed class CompositeDisposable : IDisposable
77+
{
78+
readonly IDisposable first;
79+
readonly IDisposable second;
80+
81+
public CompositeDisposable(IDisposable first, IDisposable second)
82+
{
83+
this.first = first;
84+
this.second = second;
85+
}
86+
87+
public void Dispose()
88+
{
89+
this.first.Dispose();
90+
this.second.Dispose();
91+
}
92+
}
93+
}

src/Worker/Grpc/GrpcDurableTaskWorker.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public GrpcDurableTaskWorker(
4545
this.workerOptions = Check.NotNull(workerOptions).Get(name);
4646
this.services = Check.NotNull(services);
4747
this.loggerFactory = Check.NotNull(loggerFactory);
48-
this.logger = loggerFactory.CreateLogger("Microsoft.DurableTask"); // TODO: use better category name.
48+
this.logger = CreateLogger(loggerFactory, this.workerOptions);
4949
this.orchestrationFilter = orchestrationFilter;
5050
this.ExceptionPropertiesProvider = exceptionPropertiesProvider;
5151
}
@@ -103,4 +103,19 @@ AsyncDisposable GetCallInvoker(out CallInvoker callInvoker, out string address)
103103
address = c.Target;
104104
return new AsyncDisposable(() => new(c.ShutdownAsync()));
105105
}
106+
107+
static ILogger CreateLogger(ILoggerFactory loggerFactory, DurableTaskWorkerOptions options)
108+
{
109+
// Use the new, more specific category name for gRPC worker logs
110+
ILogger primaryLogger = loggerFactory.CreateLogger("Microsoft.DurableTask.Worker.Grpc");
111+
112+
// If legacy categories are enabled, also emit logs to the old broad category
113+
if (options.Logging.UseLegacyCategories)
114+
{
115+
ILogger legacyLogger = loggerFactory.CreateLogger("Microsoft.DurableTask");
116+
return new DualCategoryLogger(primaryLogger, legacyLogger);
117+
}
118+
119+
return primaryLogger;
120+
}
106121
}

0 commit comments

Comments
 (0)