diff --git a/Directory.Packages.props b/Directory.Packages.props
index 11d8a4f5..8d71bbaa 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,7 +3,7 @@
true
5.9.0
1.67.0
- 1.0.0-prerelease.52
+ 1.0.0-prerelease.53
10.0.12
10.10.0
1.18.0
diff --git a/docs/wiki/Activities.md b/docs/wiki/Activities.md
index b138ffdb..5fb84ed6 100644
--- a/docs/wiki/Activities.md
+++ b/docs/wiki/Activities.md
@@ -55,7 +55,10 @@ When an `Exception` parameter is present, the default behaviour follows the [Ope
- `exception.stacktrace` — the value of `Exception.StackTrace`.
- `exception.type` — the `Type.FullName` of the exception.
-This behaviour can be overridden with the `EventAttribute` options (see below).
+This behaviour can be overridden with the `EventAttribute` options (see below). The standard `exception` event is only required when the exception is recorded with the OpenTelemetry rules: naming the event `exception` with `[Event(Name = "exception")]` attaches the tags above to *that* event instead of adding a separate one, and `TSG3021` suggests doing so when the name differs. The diagnostic and the standard name do not apply when `UseRecordExceptionRules` is `false`, when the exception is a `[Baggage]` parameter, or when the parameter is excluded from the Activities target with `[ExcludeTargets(Targets.Activities)]`.
+
+> [!NOTE]
+> `Name` on a *logging* attribute (`[Error]`, `[Log]`, `[Info]`, …) renames the **log entry** — its `EventId` name and default message template — not the activity event. The event name always comes from `[Event(Name = "…")]`.
### Context
diff --git a/docs/wiki/Breaking-Changes.md b/docs/wiki/Breaking-Changes.md
index c05eabb1..907b110e 100644
--- a/docs/wiki/Breaking-Changes.md
+++ b/docs/wiki/Breaking-Changes.md
@@ -160,6 +160,16 @@ v5 adds `MeterName` and `MeterNameGenerationType` to `[MeterGeneration]`, contro
The new `TSG3022` warning recommends returning `Activity?` from Activity methods. It is a warning, not an error, but plan to move to nullable Activity return types as the Activity can be `null` when no listeners are active.
+### Activities targets now honour parameter-level `[ExcludeTargets]`
+
+`[ExcludeTargets(Targets.Activities)]` on a parameter is now applied consistently: the parameter is no longer set as a tag, added as baggage, or used as a reserved parameter (`tags`, `parentContext`, `links`, `startTime`, `timestamp`, `[Escape]`, `[StatusDescription]`) for the Activity/ActivityEvent. It stays part of the generated method signature and is still passed to the Logging and Metrics targets.
+
+**Impact:** Low — the generated Activity tags/baggage change for interfaces that already use `[ExcludeTargets(Targets.Activities)]` (previously the attribute was ignored for the Activities target). Activities-specific diagnostics for the excluded parameter (`TSG3000`, `TSG3003`–`TSG3011`, `TSG3016`, `TSG3017`, `TSG3021`) are no longer raised.
+
+### `TSG3021` only applies when the OpenTelemetry exception rules are used
+
+`TSG3021` (exception event should be named `exception`) is no longer raised when the exception is not recorded using the OpenTelemetry exception rules — that is, when `[Event(UseRecordExceptionRules = false)]` is used, when the exception parameter is `[Baggage]`, or when it is excluded from the Activities target. When the `Name` was set on a *logging* attribute instead of `[Event]`, the diagnostic now says so explicitly.
+
## v1 and v2 to v3
### Logging event-name generation
diff --git a/docs/wiki/Diagnostics.md b/docs/wiki/Diagnostics.md
index e3d1a1ec..254dc87d 100644
--- a/docs/wiki/Diagnostics.md
+++ b/docs/wiki/Diagnostics.md
@@ -60,7 +60,7 @@ The package ships a single Roslyn analyzer, `TelemetryDiagnosticAnalyzer`, which
| `TSG3015` | Info | Activity should be the first parameter. Opt-in via `GenerateDiagnosticsForMissingActivity`. |
| `TSG3016` | Error | Status description parameter should be a string. |
| `TSG3017` | Error | Status Description parameters are only valid on Events, not Activity or Context methods. |
-| `TSG3021` | Info | Exception event does not use OpenTelemetry standard name. An `[Event]` method records an exception but the event name is not the standard `"exception"` (suggest `[Event(Name = "exception")]`). |
+| `TSG3021` | Info | Exception event does not use OpenTelemetry standard name. An `[Event]` method records an exception under the OpenTelemetry exception rules but the event name is not the standard `"exception"` (suggest `[Event(Name = "exception")]`). Not raised when `UseRecordExceptionRules` is disabled, for `[Baggage]` exceptions, or when the exception parameter is excluded from the Activities target. |
| `TSG3022` | Warning | Activity return type should be nullable. An Activity method returns non-nullable `Activity`; use `Activity?` because the Activity can be null when no listeners are active. |
## Metrics diagnostics (TSG4xxx)
@@ -107,6 +107,25 @@ Two or more methods share the same name, which is used to generate members on th
An Activity method does not return the created `Activity`, or an Event/Context method has no `Activity` parameter. Return the `Activity`/`Activity?` and pass it to Event/Context methods. These best-practice diagnostics are controlled by `ActivitySourceGeneration.GenerateDiagnosticsForMissingActivity`.
+### TSG3021 — exception event name
+
+Rename the event with `[Event(Name = "exception")]` so the OpenTelemetry exception tags (`exception.type`, `exception.message`, `exception.stacktrace`, `exception.escaped`) are attached to that event instead of a separate event named `exception`.
+
+```csharp
+[Event(Name = "exception")] // ✅ the event carries the exception tags
+void FailedToRetrieve(Activity? activity, Exception exception);
+```
+
+A `Name` set on a *logging* attribute does not name the event — it renames the log entry:
+
+```csharp
+[Event] // ❌ event name is the method name
+[Error(Name = "exception")] // the log entry is named 'exception'
+void FailedToRetrieve(Activity? activity, Exception exception);
+```
+
+The diagnostic is not raised when the exception is not recorded using the OpenTelemetry exception rules: `[Event(UseRecordExceptionRules = false)]`, a `[Baggage]` exception parameter, or an exception parameter excluded from the Activities target (`[ExcludeTargets(Targets.Activities)]`).
+
### TSG3022 — non-nullable Activity return
Return `Activity?` so callers can handle the `null` case when no listeners are active.
diff --git a/docs/wiki/Tags-and-Baggage.md b/docs/wiki/Tags-and-Baggage.md
index 1eb1fef2..04643d28 100644
--- a/docs/wiki/Tags-and-Baggage.md
+++ b/docs/wiki/Tags-and-Baggage.md
@@ -97,6 +97,8 @@ Excludes a parameter from specific telemetry targets. See [Multi-Targeting](Mult
string verboseMessage; // excluded from metrics only
```
+The parameter stays part of the generated method signature, but it is not applied to the excluded target: for the Activities target that means it is not set as a tag, not added as baggage, and not used as a reserved parameter such as `tags`, `parentContext`, `startTime`, or `[Escape]`. Excluding it from Activities also stops the Activities-specific diagnostics (`TSG3000`, `TSG3003`–`TSG3011`, `TSG3016`, `TSG3017`, `TSG3021`) for that parameter.
+
## `[ExpandEnumerable]`
Applied to an array or `IEnumerable` parameter on a log method, it logs the individual elements. See [Logging Generation v2](Logging-Generation-v2.md#expandenumerable).
diff --git a/package.json b/package.json
index 258c9ced..abc5ad6f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "purview-telemetry-sourcegenerator",
- "version": "5.0.0-prerelease.17",
+ "version": "5.0.0-prerelease.18",
"description": "Generates [`ActivitySource`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activitysource), [`ILogger`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger), and [`Metrics`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics) based on interface methods.",
"license": "MIT",
"readme": "README.md",
diff --git a/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.EventMethods.cs b/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.EventMethods.cs
index 136c2520..d2e13d15 100644
--- a/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.EventMethods.cs
+++ b/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.EventMethods.cs
@@ -147,11 +147,6 @@ static string EmitEventTags(
var escapeValue = escapeParam?.ParameterName ?? "true";
foreach (var tagParam in methodTarget.Tags)
{
- var emitTag =
- tagParam.IsException
- && methodTarget.ActivityOrEventName != PropertyLibrary.Activities.Tag_ExceptionEventName
- && useRecordedExceptionRules;
-
void EmitTag()
{
if (tagParam.IsException)
diff --git a/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.Parameters.cs b/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.Parameters.cs
index ad937fa0..104bff01 100644
--- a/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.Parameters.cs
+++ b/src/src/SourceGenerator/Emitters/ActivitySourceTargetClassEmitter.Parameters.cs
@@ -97,29 +97,35 @@ out ActivityBasedParameterTarget? statusDescriptionParam
escapeParam = null;
statusDescriptionParam = null;
- var activityParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.Activity)
+ // Parameters excluded from the Activities target are part of the generated method signature but
+ // are never passed to the ActivitySource, so they are not considered here.
+ var applicableParameters = methodTarget
+ .Parameters.Where(static p => !p.ExcludedFromActivities)
.ToImmutableArray();
- var parentContextOrIdParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.ParentContextOrId)
+
+ var activityParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.Activity)
+ .ToImmutableArray();
+ var parentContextOrIdParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.ParentContextOrId)
.ToImmutableArray();
- var tagsParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.TagsEnumerable)
+ var tagsParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.TagsEnumerable)
.ToImmutableArray();
- var linksParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.LinksEnumerable)
+ var linksParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.LinksEnumerable)
.ToImmutableArray();
- var startTimeParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.StartTime)
+ var startTimeParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.StartTime)
.ToImmutableArray();
- var timestampParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.Timestamp)
+ var timestampParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.Timestamp)
.ToImmutableArray();
- var escapeParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.Escape)
+ var escapeParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.Escape)
.ToImmutableArray();
- var statusDescriptionParams = methodTarget
- .Parameters.Where(m => m.ParamDestination == ActivityParameterDestination.StatusDescription)
+ var statusDescriptionParams = applicableParameters
+ .Where(m => m.ParamDestination == ActivityParameterDestination.StatusDescription)
.ToImmutableArray();
if (activityParams.Length > 1)
diff --git a/src/src/SourceGenerator/Helpers/DiagnosticLibrary.Activities.cs b/src/src/SourceGenerator/Helpers/DiagnosticLibrary.Activities.cs
index d4f7739d..bb35475f 100644
--- a/src/src/SourceGenerator/Helpers/DiagnosticLibrary.Activities.cs
+++ b/src/src/SourceGenerator/Helpers/DiagnosticLibrary.Activities.cs
@@ -236,11 +236,14 @@ public static class Activities
isBlocking: false
);
+ // The trailing '{1}' placeholder carries an optional hint supplied by the rule (for example when
+ // the 'Name' was applied to a logging attribute); rules pass string.Empty when there is no
+ // additional context.
public static readonly ReportableDiagnostic ExceptionEventNotStandardName = ReportableDiagnostic.Create(
new DiagnosticDescriptor(
id: "TSG3021",
title: "Exception event does not use OpenTelemetry standard name",
- messageFormat: "Event '{0}' records an exception but does not use the OpenTelemetry standard name 'exception'. Consider using [Event(Name = \"exception\")] for better observability.",
+ messageFormat: "Event '{0}' records an exception but does not use the OpenTelemetry standard name 'exception'. Consider using [Event(Name = \"exception\")] for better observability.{1}",
defaultSeverity: DiagnosticSeverity.Info,
category: Categories.Activity.Usage,
isEnabledByDefault: true
diff --git a/src/src/SourceGenerator/Helpers/PipelineHelpers.Activities.cs b/src/src/SourceGenerator/Helpers/PipelineHelpers.Activities.cs
index a83625da..6ec7c868 100644
--- a/src/src/SourceGenerator/Helpers/PipelineHelpers.Activities.cs
+++ b/src/src/SourceGenerator/Helpers/PipelineHelpers.Activities.cs
@@ -166,11 +166,14 @@ out var eventAttribute
namingConvention,
token
);
+ // Parameters can opt out of the Activities target to be used by the other target families
+ // only: they remain part of the generated method signature but are not applied to the
+ // Activity or ActivityEvent.
var baggageParameters = parameters
- .Where(m => m.ParamDestination == ActivityParameterDestination.Baggage)
+ .Where(m => m.ParamDestination == ActivityParameterDestination.Baggage && !m.ExcludedFromActivities)
.ToImmutableArray();
var tagParameters = parameters
- .Where(m => m.ParamDestination == ActivityParameterDestination.Tag)
+ .Where(m => m.ParamDestination == ActivityParameterDestination.Tag && !m.ExcludedFromActivities)
.ToImmutableArray();
var targetGenerationState = Utilities.IsValidGenerationTarget(
diff --git a/src/src/SourceGenerator/Records/ActivityRecords.cs b/src/src/SourceGenerator/Records/ActivityRecords.cs
index 475ef374..6f5cafeb 100644
--- a/src/src/SourceGenerator/Records/ActivityRecords.cs
+++ b/src/src/SourceGenerator/Records/ActivityRecords.cs
@@ -64,7 +64,15 @@ sealed record ActivityBasedParameterTarget(
bool SkipOnNullOrEmpty,
bool IsException,
GenerationType ExcludedTargets
-);
+)
+{
+ ///
+ /// True when the parameter opts out of the Activities target via
+ /// [ExcludeTargets(Targets.Activities)]. The parameter stays part of the generated method
+ /// signature (it is declared on the interface) but is never applied to the Activity or ActivityEvent.
+ ///
+ public bool ExcludedFromActivities => ExcludedTargets.HasFlag(GenerationType.Activities);
+}
enum ActivityParameterDestination
{
diff --git a/src/src/SourceGenerator/Records/TelemetryRules.Activities.cs b/src/src/SourceGenerator/Records/TelemetryRules.Activities.cs
index 93069591..2c2649d2 100644
--- a/src/src/SourceGenerator/Records/TelemetryRules.Activities.cs
+++ b/src/src/SourceGenerator/Records/TelemetryRules.Activities.cs
@@ -1,5 +1,6 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
+using Purview.Telemetry.SourceGenerator.Helpers;
using Purview.Telemetry.SourceGenerator.Records;
namespace Purview.Telemetry.SourceGenerator;
@@ -125,7 +126,10 @@ CancellationToken token
if (generateDiagnosticsForMissingActivity && method.HasActivityParameter && method.Parameters.Count > 0)
{
- if (method.Parameters[0].ParamDestination != ActivityParameterDestination.Activity)
+ // Parameters excluded from the Activities target keep their declared position in the generated
+ // signature, so the first applicable parameter is the one that can satisfy this rule.
+ var firstApplicableParameter = method.Parameters.FirstOrDefault(static p => !p.ExcludedFromActivities);
+ if (firstApplicableParameter?.ParamDestination != ActivityParameterDestination.Activity)
diagnostics.Add(
ReportableDiagnostic.Create(
DiagnosticLibrary.Activities.ActivityShouldBeTheFirstParameter.Descriptor,
@@ -135,17 +139,32 @@ CancellationToken token
);
}
- // TSG3021: an event recording an exception should use the OpenTelemetry standard name.
+ // TSG3021: an event recording an exception should use the OpenTelemetry standard name. This only
+ // applies when the exception is recorded using the OpenTelemetry exception rules: with
+ // UseRecordExceptionRules disabled the exception is emitted as a plain tag, a baggage exception is
+ // set as baggage, and a parameter excluded from the Activities target is not applied at all.
if (method.MethodType == ActivityMethodType.Event)
{
- var recordsException = method.Parameters.Any(static p => p.IsException);
- if (recordsException && !string.Equals(method.ActivityOrEventName, "exception", StringComparison.Ordinal))
+ var useRecordExceptionRules =
+ method.EventAttribute?.UseRecordExceptionRules
+ ?? PropertyLibrary.Activities.UseRecordExceptionRulesDefault;
+
+ var recordsException =
+ useRecordExceptionRules
+ && method.Parameters.Any(static p =>
+ p.IsException
+ && !p.ExcludedFromActivities
+ && p.ParamDestination != ActivityParameterDestination.Baggage
+ );
+
+ if (recordsException && !IsStandardExceptionEventName(method.ActivityOrEventName))
diagnostics.Add(
ReportableDiagnostic.Create(
DiagnosticLibrary.Activities.ExceptionEventNotStandardName.Descriptor,
isBlocking: false,
methodSymbol,
- method.ActivityOrEventName
+ method.ActivityOrEventName,
+ GetExceptionEventNameHint(methodSymbol, token)
)
);
}
@@ -192,7 +211,8 @@ ImmutableArray.Builder diagnostics
{
var duplicateReserved = method
.Parameters.Where(static p =>
- p.ParamDestination is not (ActivityParameterDestination.Tag or ActivityParameterDestination.Baggage)
+ !p.ExcludedFromActivities
+ && p.ParamDestination is not (ActivityParameterDestination.Tag or ActivityParameterDestination.Baggage)
)
.GroupBy(static p => p.ParamDestination)
.Where(static g => g.Count() > 1);
@@ -225,6 +245,10 @@ CancellationToken token
{
token.ThrowIfCancellationRequested();
+ // Only parameters applied to the Activities target are considered.
+ if (parameter.ExcludedFromActivities)
+ continue;
+
var location = GetParameterLocation(methodSymbol, parameter.ParameterName);
var parameterName = parameter.GeneratedName;
@@ -336,4 +360,58 @@ CancellationToken token
#pragma warning restore IDE0010 // Add missing cases
}
}
+
+ static bool IsStandardExceptionEventName(string? name) =>
+ string.Equals(name, PropertyLibrary.Activities.Tag_ExceptionEventName, StringComparison.Ordinal);
+
+ ///
+ /// Returns the optional hint appended to TSG3021. A Name on a logging attribute (such as
+ /// [Error]) renames the log entry, not the activity event, so surface that when it looks like
+ /// the name was applied to the wrong attribute.
+ ///
+ static string GetExceptionEventNameHint(IMethodSymbol methodSymbol, CancellationToken token)
+ {
+ if (
+ !Utilities.TryContainsAttribute(
+ methodSymbol,
+ TypeLibrary.Purview.Telemetry.LogAttributeTargets,
+ token,
+ out var matchingType,
+ out var attributeData
+ )
+ )
+ return string.Empty;
+
+ if (!IsStandardExceptionEventName(GetLogEntryName(methodSymbol, attributeData!, token)))
+ return string.Empty;
+
+ return $" The Name on [{matchingType.RenderAttributeTypeName}] renames the log entry, not the activity event.";
+ }
+
+ ///
+ /// Gets the name configured on a logging attribute. The parsed log-attribute models historically
+ /// resolved name from the constructor first, so an explicitly-set Name property is read
+ /// directly from the attribute's named arguments as well.
+ ///
+ ///
+ /// The framework's attribute-data model generator now prefers the named argument (see
+ /// sourcegenerator-framework); this fallback keeps the hint working for the pinned version.
+ ///
+ static string? GetLogEntryName(IMethodSymbol methodSymbol, AttributeData attributeData, CancellationToken token)
+ {
+ var fromModel = SharedHelpers.GetLogAttribute(methodSymbol, token)?.Name;
+ if (fromModel is not null)
+ return fromModel;
+
+ foreach (var namedArgument in attributeData.NamedArguments)
+ {
+ if (
+ string.Equals(namedArgument.Key, "Name", StringComparison.Ordinal)
+ && namedArgument.Value.Value is string name
+ )
+ return name;
+ }
+
+ return null;
+ }
}
diff --git a/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorActivityTests.BasicEventGen.cs b/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorActivityTests.BasicEventGen.cs
index cb75abb3..089f768c 100644
--- a/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorActivityTests.BasicEventGen.cs
+++ b/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorActivityTests.BasicEventGen.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using System.Globalization;
using Purview.SourceGeneratorFramework;
namespace Purview.Telemetry.SourceGenerator.Activities;
@@ -335,4 +336,227 @@ await Assert
.IsTrue()
.Because("the generated implementation must contain the second status-description event method");
}
+
+ [Test]
+ [Arguments("\"exception\"")]
+ [Arguments("name: \"exception\"")]
+ [Arguments("Name = \"exception\"")]
+ public async Task Generate_GivenEventWithExceptionAndOpenTelemetryName_DoesNotRaiseTSG3021(
+ string eventAttributeArguments,
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange: the event name can be set through the constructor (positionally or by name) or the
+ // Name property; all three forms must resolve to the OpenTelemetry standard name.
+ var basicActivity = $$"""
+
+using System.Diagnostics;
+
+namespace Testing;
+
+[ActivitySource("testing-activity-source")]
+public interface ITestActivities
+{
+ [Activity]
+ System.Diagnostics.Activity? Activity();
+
+ [Event({{eventAttributeArguments}})]
+ void FailedToRetrieveRepositories(Activity? activity, Exception exception);
+}
+
+""";
+
+ // Act
+ var generationResult = await GenerateAsync(basicActivity, cancellationToken: cancellationToken);
+
+ // Assert
+ await Assert.That(generationResult).DoesNotHaveDiagnostic("TSG3021");
+ }
+
+ [Test]
+ public async Task Generate_GivenEventWithExceptionAndNameOnLoggingAttribute_RaisesTSG3021WithLogEntryHint(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange: 'Name' on a logging attribute renames the log entry, not the activity event.
+ const string basicTelemetry = """
+
+using System.Diagnostics;
+
+namespace Testing;
+
+[ActivitySource("testing-activity-source")]
+[Logger]
+public interface ITestTelemetry
+{
+ [Activity]
+ [Info]
+ System.Diagnostics.Activity? Activity();
+
+ [Event]
+ [Error(Name = "exception")]
+ void FailedToRetrieveRepositories(Activity? activity, Exception exception);
+}
+
+""";
+
+ // Act
+ var generationResult = await GenerateAsync(basicTelemetry, cancellationToken: cancellationToken);
+
+ // Assert
+ await Assert.That(generationResult).HasDiagnostic("TSG3021");
+
+ var message = generationResult
+ .AnalyzerResult!.Diagnostics.Single(diagnostic => diagnostic.Id == "TSG3021")
+ .GetMessage(CultureInfo.InvariantCulture);
+
+ await Assert.That(message).Contains("The Name on [Error] renames the log entry, not the activity event");
+ }
+
+ [Test]
+ public async Task Generate_GivenEventWithDisabledOTelExceptionRules_DoesNotRaiseTSG3021(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange: with the rules disabled the exception becomes an ordinary tag, so the
+ // OpenTelemetry standard event name is not required.
+ const string basicTelemetry = """
+
+using System.Diagnostics;
+
+namespace Testing;
+
+[ActivitySource("testing-activity-source")]
+public interface ITestActivities
+{
+ [Activity]
+ System.Diagnostics.Activity? Activity();
+
+ [Event(UseRecordExceptionRules = false)]
+ void FailedToRetrieveRepositories(Activity? activity, Exception exception);
+}
+
+""";
+
+ // Act
+ var generationResult = await GenerateAsync(basicTelemetry, cancellationToken: cancellationToken);
+
+ // Assert
+ await Assert.That(generationResult).DoesNotHaveDiagnostic("TSG3021");
+ }
+
+ [Test]
+ public async Task Generate_GivenEventWithBaggageException_DoesNotRaiseTSG3021(CancellationToken cancellationToken)
+ {
+ // Arrange: a baggage exception is set as baggage, never recorded as an exception event.
+ const string basicTelemetry = """
+
+using System.Diagnostics;
+
+namespace Testing;
+
+[ActivitySource("testing-activity-source")]
+public interface ITestActivities
+{
+ [Activity]
+ System.Diagnostics.Activity? Activity();
+
+ [Event]
+ void FailedToRetrieveRepositories(Activity? activity, [Baggage] Exception exception);
+}
+
+""";
+
+ // Act
+ var generationResult = await GenerateAsync(basicTelemetry, cancellationToken: cancellationToken);
+
+ // Assert
+ await Assert.That(generationResult).DoesNotHaveDiagnostic("TSG3021");
+ }
+
+ [Test]
+ public async Task Generate_GivenEventWithExceptionExcludedFromActivities_DoesNotRaiseTSG3021OrRecordException(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange
+ const string basicTelemetry = """
+
+using System.Diagnostics;
+
+namespace Testing;
+
+[ActivitySource("testing-activity-source")]
+public interface ITestActivities
+{
+ [Activity]
+ System.Diagnostics.Activity? Activity();
+
+ [Event]
+ void FailedToRetrieveRepositories(Activity? activity, [ExcludeTargets(Targets.Activities)] Exception exception);
+}
+
+""";
+
+ // Act
+ var generationResult = await GenerateAsync(basicTelemetry, cancellationToken: cancellationToken);
+
+ // Assert
+ await Assert.That(generationResult).DoesNotHaveDiagnostic("TSG3021");
+
+ var implClass = generationResult.Generated().GetClass("TestActivitiesCore", "Testing");
+ await Assert
+ .That(
+ implClass.HasMethod(
+ "FailedToRetrieveRepositories",
+ TypeReference.Create(),
+ TypeReference.Create()
+ )
+ )
+ .IsTrue()
+ .Because("a parameter excluded from the Activities target stays in the generated signature");
+
+ await Assert
+ .That(generationResult.GetSource("TestActivitiesCore.Activity.g.cs"))
+ .DoesNotContain("RecordExceptionInternal(activity:")
+ .Because("an exception excluded from the Activities target must not be recorded on the Activity");
+ }
+
+ [Test]
+ public async Task Generate_GivenEventWithTagExcludedFromActivities_DoesNotSetExcludedTag(
+ CancellationToken cancellationToken
+ )
+ {
+ // Arrange
+ const string basicTelemetry = """
+
+using System.Diagnostics;
+
+namespace Testing;
+
+[ActivitySource("testing-activity-source")]
+public interface ITestActivities
+{
+ [Activity]
+ System.Diagnostics.Activity? Activity();
+
+ [Event]
+ void Event(Activity? activity, [Tag] string kept, [ExcludeTargets(Targets.Activities)] string dropped);
+}
+
+""";
+
+ // Act
+ var generationResult = await GenerateAsync(basicTelemetry, cancellationToken: cancellationToken);
+
+ // Assert
+ await Assert
+ .That(generationResult.GetSource("TestActivitiesCore.Activity.g.cs"))
+ .ContainsGeneratedCode("\"kept\"");
+
+ await Assert
+ .That(generationResult.GetSource("TestActivitiesCore.Activity.g.cs"))
+ .DoesNotContain("\"dropped\"")
+ .Because("a parameter excluded from the Activities target must not be set as a tag");
+ }
}
diff --git a/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorTests.TelemetryBasic.cs b/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorTests.TelemetryBasic.cs
index 9b7216ec..ed8e741f 100644
--- a/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorTests.TelemetryBasic.cs
+++ b/src/tests/SourceGenerator.IntegrationTests/Generators/TelemetrySourceGeneratorTests.TelemetryBasic.cs
@@ -300,7 +300,9 @@ public interface ITestTelemetry
var generationResult = await GenerateAsync(basicTelemetry, cancellationToken: cancellationToken);
// Assert
- await Assert.That(generationResult).HasDiagnostic("TSG3021");
+ // With the OpenTelemetry exception rules disabled the exception is emitted as an ordinary tag,
+ // so the standard 'exception' event name is not required (TSG3021 does not apply).
+ await Assert.That(generationResult).DoesNotHaveDiagnostic("TSG3021");
}
[Test]