Skip to content

Commit 34e8e9b

Browse files
CopilotYunchuWangcgillum
authored
Add specific logging categories for Worker.Grpc and orchestration logs with backward-compatible opt-in (#583)
* Initial plan * Investigate and understand test failure caused by dual-category logging Co-authored-by: YunchuWang <[email protected]> * Fix test failure: update GetLogs() to use specific category to avoid duplicates Co-authored-by: YunchuWang <[email protected]> * Simplify comment in GetLogs method Co-authored-by: YunchuWang <[email protected]> * Improve exception handling in CompositeDisposable.Dispose Co-authored-by: YunchuWang <[email protected]> * Improve integration tests to verify actual worker logger type via reflection Co-authored-by: YunchuWang <[email protected]> * Extract reflection helper methods to reduce code duplication in tests Co-authored-by: YunchuWang <[email protected]> * Consolidate LoggingOptions docs and remove useless test Co-authored-by: cgillum <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: YunchuWang <[email protected]> Co-authored-by: cgillum <[email protected]>
1 parent bf93fe8 commit 34e8e9b

File tree

6 files changed

+455
-4
lines changed

6 files changed

+455
-4
lines changed

src/Worker/Core/DurableTaskWorkerOptions.cs

Lines changed: 57 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,46 @@ public class VersioningOptions
229244
/// </remarks>
230245
public VersionFailureStrategy FailureStrategy { get; set; } = VersionFailureStrategy.Reject;
231246
}
247+
248+
/// <summary>
249+
/// Options for configuring Durable Task worker logging behavior.
250+
/// </summary>
251+
public class LoggingOptions
252+
{
253+
/// <summary>
254+
/// Gets or sets a value indicating whether to emit logs using legacy logging categories in addition to new categories.
255+
/// </summary>
256+
/// <remarks>
257+
/// <para>
258+
/// Starting from a future version, more specific logging categories will be used for better log filtering:
259+
/// <list type="bullet">
260+
/// <item><description><c>Microsoft.DurableTask.Worker.Grpc</c> for gRPC worker logs (previously <c>Microsoft.DurableTask</c>)</description></item>
261+
/// <item><description><c>Microsoft.DurableTask.Worker.*</c> for worker-specific logs</description></item>
262+
/// </list>
263+
/// </para>
264+
/// <para>
265+
/// When <c>true</c> (default), logs are emitted to both the new specific categories (e.g., <c>Microsoft.DurableTask.Worker.Grpc</c>)
266+
/// and the legacy broad categories (e.g., <c>Microsoft.DurableTask</c>). This ensures backward compatibility with existing
267+
/// log filters and queries.
268+
/// </para>
269+
/// <para>
270+
/// When <c>false</c>, logs are only emitted to the new specific categories, which provides better log organization
271+
/// and filtering capabilities.
272+
/// </para>
273+
/// <para>
274+
/// <b>Migration Path:</b>
275+
/// <list type="number">
276+
/// <item><description>Update your log filters to use the new, more specific categories</description></item>
277+
/// <item><description>Test your application to ensure logs are captured correctly</description></item>
278+
/// <item><description>Once confident, set this property to <c>false</c> to disable legacy category emission</description></item>
279+
/// </list>
280+
/// </para>
281+
/// <para>
282+
/// <b>Breaking Change Warning:</b> Setting this to <c>false</c> is a breaking change if you have existing log filters,
283+
/// queries, or monitoring rules that depend on the legacy category names. Ensure you update those before disabling
284+
/// legacy categories.
285+
/// </para>
286+
/// </remarks>
287+
public bool UseLegacyCategories { get; set; } = true;
288+
}
232289
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
Exception? firstException = null;
90+
91+
try
92+
{
93+
this.first.Dispose();
94+
}
95+
catch (Exception ex)
96+
{
97+
firstException = ex;
98+
}
99+
100+
try
101+
{
102+
this.second.Dispose();
103+
}
104+
catch (Exception ex)
105+
{
106+
if (firstException is null)
107+
{
108+
throw;
109+
}
110+
111+
throw new AggregateException(firstException, ex);
112+
}
113+
114+
if (firstException is not null)
115+
{
116+
throw firstException;
117+
}
118+
}
119+
}
120+
}

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
}

test/Grpc.IntegrationTests/IntegrationTestBase.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,11 @@ protected IHostBuilder CreateHostBuilder(
9595

9696
protected IReadOnlyCollection<LogEntry> GetLogs()
9797
{
98-
// NOTE: Renaming the log category is a breaking change!
99-
const string ExpectedCategoryName = "Microsoft.DurableTask";
98+
// Use the specific Worker.Grpc category to get gRPC worker logs.
99+
// TryGetLogs uses StartsWith matching, so "Microsoft.DurableTask" would match both
100+
// the legacy category AND "Microsoft.DurableTask.Worker.Grpc", causing duplicate entries
101+
// when dual-category logging is enabled.
102+
const string ExpectedCategoryName = "Microsoft.DurableTask.Worker.Grpc";
100103
bool foundCategory = this.logProvider.TryGetLogs(ExpectedCategoryName, out IReadOnlyCollection<LogEntry> logs);
101104
Assert.True(foundCategory);
102105
return logs;

0 commit comments

Comments
 (0)