Skip to content

Commit 7055e80

Browse files
authored
Merge branch 'main' into copilot/sanitize-method-names
2 parents c87aac0 + cdf3f27 commit 7055e80

17 files changed

+1137
-12
lines changed

src/Analyzers/Activities/FunctionNotFoundAnalyzer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public sealed class FunctionNotFoundAnalyzer : DiagnosticAnalyzer
3737
ActivityNotFoundTitle,
3838
ActivityNotFoundMessageFormat,
3939
AnalyzersCategories.Activity,
40-
DiagnosticSeverity.Warning,
40+
DiagnosticSeverity.Info,
4141
customTags: [WellKnownDiagnosticTags.CompilationEnd],
4242
isEnabledByDefault: true,
4343
helpLinkUri: "https://go.microsoft.com/fwlink/?linkid=2346202");
@@ -47,7 +47,7 @@ public sealed class FunctionNotFoundAnalyzer : DiagnosticAnalyzer
4747
SubOrchestrationNotFoundTitle,
4848
SubOrchestrationNotFoundMessageFormat,
4949
AnalyzersCategories.Orchestration,
50-
DiagnosticSeverity.Warning,
50+
DiagnosticSeverity.Info,
5151
customTags: [WellKnownDiagnosticTags.CompilationEnd],
5252
isEnabledByDefault: true,
5353
helpLinkUri: "https://go.microsoft.com/fwlink/?linkid=2346202");

src/Analyzers/AnalyzerReleases.Shipped.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
Rule ID | Category | Severity | Notes
99
--------|----------|----------|-------
10-
DURABLE2003 | Activity | Warning | **FunctionNotFoundAnalyzer**: Warns when an activity function call references a name that does not match any defined activity in the compilation.
11-
DURABLE2004 | Orchestration | Warning | **FunctionNotFoundAnalyzer**: Warns when a sub-orchestration call references a name that does not match any defined orchestrator in the compilation.
10+
DURABLE2003 | Activity | Info | **FunctionNotFoundAnalyzer**: Reports when an activity function call references a name that does not match any defined activity in the compilation. If the function is defined in another assembly, this diagnostic can be safely ignored. Static resolution across referenced assemblies is not supported.
11+
DURABLE2004 | Orchestration | Info | **FunctionNotFoundAnalyzer**: Reports when a sub-orchestration call references a name that does not match any defined orchestrator in the compilation. If the function is defined in another assembly, this diagnostic can be safely ignored. Static resolution across referenced assemblies is not supported.
1212

1313
## Release 0.1.0
1414

src/Analyzers/AnalyzerReleases.Unshipped.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@
44
### New Rules
55

66
Rule ID | Category | Severity | Notes
7-
--------|----------|----------|-------
7+
--------|----------|----------|-------
8+
DURABLE0009 | Orchestration | Info | **GetInputOrchestrationAnalyzer**: Suggests using input parameter binding instead of ctx.GetInput<T>() in orchestration methods.
9+
DURABLE0010 | Orchestration | Warning | **LoggerOrchestrationAnalyzer**: Warns when a non-contextual ILogger is used in an orchestration method. Orchestrations should use `context.CreateReplaySafeLogger()` instead of injecting ILogger directly.

src/Analyzers/KnownTypeSymbols.Net.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public sealed partial class KnownTypeSymbols
2323
INamedTypeSymbol? cancellationToken;
2424
INamedTypeSymbol? environment;
2525
INamedTypeSymbol? httpClient;
26+
INamedTypeSymbol? iLogger;
2627

2728
/// <summary>
2829
/// Gets a Guid type symbol.
@@ -75,4 +76,9 @@ public sealed partial class KnownTypeSymbols
7576
/// Gets an HttpClient type symbol.
7677
/// </summary>
7778
public INamedTypeSymbol? HttpClient => this.GetOrResolveFullyQualifiedType(typeof(HttpClient).FullName, ref this.httpClient);
79+
80+
/// <summary>
81+
/// Gets an ILogger type symbol.
82+
/// </summary>
83+
public INamedTypeSymbol? ILogger => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Logging.ILogger", ref this.iLogger);
7884
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Immutable;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using Microsoft.CodeAnalysis.Operations;
8+
using static Microsoft.DurableTask.Analyzers.Orchestration.GetInputOrchestrationAnalyzer;
9+
10+
namespace Microsoft.DurableTask.Analyzers.Orchestration;
11+
12+
/// <summary>
13+
/// Analyzer that reports an informational diagnostic when ctx.GetInput() is used in an orchestration method,
14+
/// suggesting the use of input parameter binding instead.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class GetInputOrchestrationAnalyzer : OrchestrationAnalyzer<GetInputOrchestrationVisitor>
18+
{
19+
/// <summary>
20+
/// Diagnostic ID supported for the analyzer.
21+
/// </summary>
22+
public const string DiagnosticId = "DURABLE0009";
23+
24+
static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.GetInputOrchestrationAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
25+
static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.GetInputOrchestrationAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
26+
27+
static readonly DiagnosticDescriptor Rule = new(
28+
DiagnosticId,
29+
Title,
30+
MessageFormat,
31+
AnalyzersCategories.Orchestration,
32+
DiagnosticSeverity.Info,
33+
isEnabledByDefault: true);
34+
35+
/// <inheritdoc/>
36+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
37+
38+
/// <summary>
39+
/// Visitor that inspects the method body for GetInput calls.
40+
/// </summary>
41+
public sealed class GetInputOrchestrationVisitor : MethodProbeOrchestrationVisitor
42+
{
43+
/// <inheritdoc/>
44+
protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode methodSyntax, IMethodSymbol methodSymbol, string orchestrationName, Action<Diagnostic> reportDiagnostic)
45+
{
46+
IOperation? methodOperation = semanticModel.GetOperation(methodSyntax);
47+
if (methodOperation is null)
48+
{
49+
return;
50+
}
51+
52+
foreach (IInvocationOperation operation in methodOperation.Descendants().OfType<IInvocationOperation>())
53+
{
54+
IMethodSymbol? method = operation.TargetMethod;
55+
if (method == null)
56+
{
57+
continue;
58+
}
59+
60+
// Check if this is a call to GetInput<T>() on TaskOrchestrationContext
61+
if (method.Name != "GetInput" || !method.IsGenericMethod)
62+
{
63+
continue;
64+
}
65+
66+
// Verify the containing type is TaskOrchestrationContext
67+
if (!method.ContainingType.Equals(this.KnownTypeSymbols.TaskOrchestrationContext, SymbolEqualityComparer.Default))
68+
{
69+
continue;
70+
}
71+
72+
// e.g.: "Consider using an input parameter instead of 'GetInput<T>()' in orchestration 'MyOrchestrator'"
73+
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation.Syntax, orchestrationName));
74+
}
75+
}
76+
}
77+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Immutable;
5+
using System.Linq;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.CodeAnalysis.Operations;
9+
using static Microsoft.DurableTask.Analyzers.Orchestration.LoggerOrchestrationAnalyzer;
10+
11+
namespace Microsoft.DurableTask.Analyzers.Orchestration;
12+
13+
/// <summary>
14+
/// Analyzer that reports a warning when a non-contextual ILogger is used in an orchestration method.
15+
/// </summary>
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public sealed class LoggerOrchestrationAnalyzer : OrchestrationAnalyzer<LoggerOrchestrationVisitor>
18+
{
19+
/// <summary>
20+
/// Diagnostic ID supported for the analyzer.
21+
/// </summary>
22+
public const string DiagnosticId = "DURABLE0010";
23+
24+
static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.LoggerOrchestrationAnalyzerTitle), Resources.ResourceManager, typeof(Resources));
25+
static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.LoggerOrchestrationAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources));
26+
27+
static readonly DiagnosticDescriptor Rule = new(
28+
DiagnosticId,
29+
Title,
30+
MessageFormat,
31+
AnalyzersCategories.Orchestration,
32+
DiagnosticSeverity.Warning,
33+
isEnabledByDefault: true);
34+
35+
/// <inheritdoc/>
36+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
37+
38+
/// <summary>
39+
/// Visitor that inspects the method body for ILogger usage.
40+
/// </summary>
41+
public sealed class LoggerOrchestrationVisitor : MethodProbeOrchestrationVisitor
42+
{
43+
INamedTypeSymbol? iLoggerSymbol;
44+
45+
/// <inheritdoc/>
46+
public override bool Initialize()
47+
{
48+
this.iLoggerSymbol = this.KnownTypeSymbols.ILogger;
49+
if (this.iLoggerSymbol == null)
50+
{
51+
return false;
52+
}
53+
54+
return true;
55+
}
56+
57+
/// <inheritdoc/>
58+
protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode methodSyntax, IMethodSymbol methodSymbol, string orchestrationName, Action<Diagnostic> reportDiagnostic)
59+
{
60+
IOperation? methodOperation = semanticModel.GetOperation(methodSyntax);
61+
if (methodOperation is null)
62+
{
63+
return;
64+
}
65+
66+
// Track which parameters we've already reported on to avoid duplicates
67+
HashSet<IParameterSymbol> reportedParameters = new(SymbolEqualityComparer.Default);
68+
69+
// Check for ILogger parameters in the method signature
70+
foreach (IParameterSymbol parameter in methodSymbol.Parameters.Where(
71+
parameter => this.IsILoggerType(parameter.Type) &&
72+
parameter.DeclaringSyntaxReferences.Length > 0))
73+
{
74+
// Found an ILogger parameter - report diagnostic at the parameter location
75+
SyntaxNode parameterSyntax = parameter.DeclaringSyntaxReferences[0].GetSyntax();
76+
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, parameterSyntax, methodSymbol.Name, orchestrationName));
77+
reportedParameters.Add(parameter);
78+
}
79+
80+
// Check for ILogger field or property references (but not parameter references, as those were already reported)
81+
foreach (IOperation descendant in methodOperation.Descendants())
82+
{
83+
ITypeSymbol? typeToCheck = null;
84+
SyntaxNode? syntaxNode = null;
85+
86+
switch (descendant)
87+
{
88+
case IFieldReferenceOperation fieldRef:
89+
typeToCheck = fieldRef.Field.Type;
90+
syntaxNode = fieldRef.Syntax;
91+
break;
92+
case IPropertyReferenceOperation propRef:
93+
typeToCheck = propRef.Property.Type;
94+
syntaxNode = propRef.Syntax;
95+
break;
96+
case IParameterReferenceOperation paramRef:
97+
// Skip parameter references that we already reported on in the parameter list
98+
if (reportedParameters.Contains(paramRef.Parameter))
99+
{
100+
continue;
101+
}
102+
103+
typeToCheck = paramRef.Parameter.Type;
104+
syntaxNode = paramRef.Syntax;
105+
break;
106+
}
107+
108+
if (typeToCheck != null && syntaxNode != null && this.IsILoggerType(typeToCheck))
109+
{
110+
reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, syntaxNode, methodSymbol.Name, orchestrationName));
111+
}
112+
}
113+
}
114+
115+
bool IsILoggerType(ITypeSymbol type)
116+
{
117+
if (this.iLoggerSymbol == null)
118+
{
119+
return false;
120+
}
121+
122+
// First check for exact match with ILogger
123+
if (SymbolEqualityComparer.Default.Equals(type, this.iLoggerSymbol))
124+
{
125+
return true;
126+
}
127+
128+
// Check if the type implements ILogger interface (covers ILogger<T> case)
129+
if (type is INamedTypeSymbol namedType)
130+
{
131+
return namedType.AllInterfaces.Any(interfaceType =>
132+
SymbolEqualityComparer.Default.Equals(interfaceType, this.iLoggerSymbol));
133+
}
134+
135+
return false;
136+
}
137+
}
138+
}

src/Analyzers/Resources.resx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,12 @@
183183
<data name="ThreadTaskOrchestrationAnalyzerTitle" xml:space="preserve">
184184
<value>Thread and Task calls must be deterministic inside an orchestrator function</value>
185185
</data>
186+
<data name="LoggerOrchestrationAnalyzerMessageFormat" xml:space="preserve">
187+
<value>The method '{0}' uses a non-contextual logger that may cause unexpected behavior when invoked from orchestration '{1}'. Use 'context.CreateReplaySafeLogger()' instead.</value>
188+
</data>
189+
<data name="LoggerOrchestrationAnalyzerTitle" xml:space="preserve">
190+
<value>Orchestrations should use replay-safe loggers created from the orchestration context</value>
191+
</data>
186192
<data name="InputArgumentTypeMismatchAnalyzerMessageFormat" xml:space="preserve">
187193
<value>CallActivityAsync is passing the incorrect type '{0}' instead of '{1}' to the activity function '{2}'</value>
188194
</data>
@@ -199,15 +205,21 @@
199205
<value>Use '{0}' instead of '{1}'</value>
200206
</data>
201207
<data name="ActivityNotFoundAnalyzerMessageFormat" xml:space="preserve">
202-
<value>The activity function '{0}' was not found in the current compilation. Ensure the activity is defined and the name matches exactly.</value>
208+
<value>The activity function '{0}' was not found in the current compilation. Ensure the activity is defined and the name matches exactly. If the function is defined in another assembly, this diagnostic can be safely ignored. Static resolution across referenced assemblies is not supported.</value>
203209
</data>
204210
<data name="ActivityNotFoundAnalyzerTitle" xml:space="preserve">
205211
<value>Activity function not found</value>
206212
</data>
207213
<data name="SubOrchestrationNotFoundAnalyzerMessageFormat" xml:space="preserve">
208-
<value>The sub-orchestration '{0}' was not found in the current compilation. Ensure the orchestrator is defined and the name matches exactly.</value>
214+
<value>The sub-orchestration '{0}' was not found in the current compilation. Ensure the orchestrator is defined and the name matches exactly. If the function is defined in another assembly, this diagnostic can be safely ignored. Static resolution across referenced assemblies is not supported.</value>
209215
</data>
210216
<data name="SubOrchestrationNotFoundAnalyzerTitle" xml:space="preserve">
211217
<value>Sub-orchestration not found</value>
212218
</data>
219+
<data name="GetInputOrchestrationAnalyzerMessageFormat" xml:space="preserve">
220+
<value>Consider using an input parameter instead of 'GetInput&lt;T&gt;()' in orchestration '{0}'</value>
221+
</data>
222+
<data name="GetInputOrchestrationAnalyzerTitle" xml:space="preserve">
223+
<value>Input parameter binding can be used instead of GetInput</value>
224+
</data>
213225
</root>

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
}

0 commit comments

Comments
 (0)