# Telemetry SourceGenerator
> Interface-based telemetry for .NET with zero boilerplate.
A source generator that turns simple interface methods into OpenTelemetry-aligned Activities, structured ILogger logging, and Metrics. Define telemetry once on an interface; the generator emits the implementation, DI registration helpers, and multi-target generation — no runtime reflection, no boilerplate.
- Repository: https://github.com/purview-dev/telemetry-sourcegenerator
- Package: https://www.nuget.org/packages/Purview.Telemetry.SourceGenerator
- Project page: https://purview.dev/projects/telemetry-sourcegenerator/
- Documentation: https://purview.dev/docs/telemetry-sourcegenerator/
- Full machine-readable content: https://purview.dev/projects/telemetry-sourcegenerator/llms-full.txt
# Getting Started
This guide gets you up and running with the Purview Telemetry Source Generator in minutes. You will add the package, define a telemetry interface, register it with dependency injection, and start emitting Activities, structured logs, and Metrics.
## Installation
Add the analyzer package to your project:
```bash
dotnet add package Purview.Telemetry.SourceGenerator
```
Or reference it in your `.csproj` or `Directory.Build.props`:
```xml
all
analyzers
```
You may also need runtime dependencies depending on which telemetry types you use:
- `System.Diagnostics.DiagnosticSource` for Activities
- `Microsoft.Extensions.Logging.Abstractions` for `ILogger`
- `System.Diagnostics.Metrics` for metrics (built into .NET 8+)
See [Installation](installation/) for the full dependency matrix.
## Define a telemetry interface
Create a `public interface` and decorate it with class-level attributes. The generator creates the implementation and a DI registration extension.
### Single-target examples
```csharp
using Purview.Telemetry;
[Logger]
public interface IOrderServiceLogs
{
[Info]
void OrderPlaced(int orderId, string customerName);
[Warning]
void OrderNotFound(int orderId);
}
```
```csharp
[ActivitySource]
public interface IOrderServiceTracing
{
[Activity]
Activity? PlacingOrder(int orderId);
[Event]
void OrderValidated(Activity? activity, int orderId);
}
```
```csharp
[Meter]
public interface IOrderServiceMetrics
{
[Counter]
void OrderPlaced(int itemsInOrder);
[Histogram]
void OrderProcessingTime(long milliseconds);
}
```
### Multi-target example
One interface can generate Activities, Logging, and Metrics from the same methods:
```csharp
[ActivitySource]
[Logger]
[Meter]
public interface IOrderServiceTelemetry
{
[Activity]
[Info]
[AutoCounter]
Activity? PlacingOrder(int orderId, [Baggage] string region);
[Event]
[Trace]
void OrderProcessed(Activity? activity, long durationMs);
[Warning]
void OrderFailed(int orderId, Exception exception);
}
```
## Register with DI
The generator creates an extension method named `Add{InterfaceNameWithoutI}()`:
```csharp
services.AddOrderServiceTelemetry();
```
Inject the interface into your service:
```csharp
public class OrderService(IOrderServiceTelemetry telemetry)
{
public void PlaceOrder(int orderId)
{
using var activity = telemetry.PlacingOrder(orderId, "EMEA");
// ...
telemetry.OrderProcessed(activity, stopwatch.ElapsedMilliseconds);
}
}
```
A single method call can emit an Activity, a log entry, and a metric simultaneously. See [Generation](generation/) for how registration helpers are produced and how to control them.
## Register names with OpenTelemetry
The generator also produces a `TelemetryNames` static class containing the meter and activity source names:
```csharp
builder.AddServiceDefaults(TelemetryNames.MeterNames, TelemetryNames.ActivitySourceNames);
```
## Common patterns
### Pattern 1: Scoped logging
Return `IDisposable?` from a log method to create scoped log entries:
```csharp
[Logger]
interface IOrderTelemetry
{
[Info]
IDisposable? ProcessingOrder(Guid orderId); // Logs at start and end of scope
[Error]
void OrderFailed(Exception ex, Guid orderId);
}
public async Task ProcessOrderAsync(Guid orderId)
{
using (telemetry.ProcessingOrder(orderId))
{
// Processing logic here
}
}
```
### Pattern 2: Activity with multiple events
Track multiple stages within a single activity:
```csharp
[ActivitySource("ShippingService")]
interface IShippingTelemetry
{
[Activity]
Activity? ShippingPackage([Baggage]string trackingNumber);
[Event]
void PackageLabeled(Activity? activity);
[Event]
void PackageWeighed(Activity? activity, [Tag]decimal weight);
[Event]
void PackageShipped(Activity? activity, [Tag]string carrier);
}
```
### Pattern 3: Auto-incrementing counters
Use `[AutoCounter]` for simple counting without a measurement parameter — every call increments by 1:
```csharp
[Meter("ApiService")]
interface IApiMetrics
{
[AutoCounter]
void RequestReceived([Tag]string endpoint, [Tag]string method);
[AutoCounter]
void RequestFailed([Tag]string endpoint, [Tag]int statusCode);
}
```
## Viewing generated code
To inspect the generated source, add this to your `.csproj`:
```xml
true
```
Generated files appear under `obj/Debug|Release//generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/`.
## Tips and best practices
1. **Return `Activity?`** from Activity-starting methods, not `Activity` or `void`, so callers can dispose and reuse the activity.
2. **Pass activities explicitly** — pass the `Activity?` from the starting method to event/context methods rather than relying on `Activity.Current`.
3. **One namespace** — use the single `using Purview.Telemetry;` import.
4. **OpenTelemetry naming** — v5 defaults to OpenTelemetry conventions (snake_case tags, hierarchical metrics). See [Naming conventions](generation/#naming-conventions).
5. **DI registration** — use the generated `Add{InterfaceName}()` extension methods.
6. **Testing** — telemetry interfaces are easy to mock in unit tests. See [Testing](testing/).
## Next steps
- [Activities](activities/) — deep dive into distributed tracing with Activities, Events, and Context
- [Logging](logging/) — structured logging and the v1/v2 generation modes
- [Metrics](metrics/) — counters, histograms, and observable instruments
- [Multi-Targeting](multi-targeting/) — combine multiple telemetry types in one interface
- [Generation Options](generation/) — control code generation, DI, and naming
- [Generated Output](generated-output/) — see what code is actually generated
- [Migration](refactorings/) — convert existing `ILogger`/`ActivitySource`/metrics code using the IDE refactorings
---
# Installation
This page covers supported frameworks, installation methods, per-feature dependencies, and build configuration for the `Purview.Telemetry.SourceGenerator` package.
## Supported frameworks
**Consumer runtime targets:**
- .NET Framework 4.8
- .NET 8 or higher
**Build toolchain requirement:**
- Visual Studio 2026 (18.x) or the .NET 10 SDK (Roslyn 5.9.0+)
The generator itself is a Roslyn component targeting `netstandard2.0`, so it works in any supported compiler host.
## Install the package
### .NET CLI
```bash
dotnet add package Purview.Telemetry.SourceGenerator
```
### Package Manager Console
```powershell
Install-Package Purview.Telemetry.SourceGenerator
```
### Project file (.csproj or Directory.Build.props)
```xml
all
analyzers
```
`PrivateAssets="all"` keeps the generator out of consumers' dependencies and `IncludeAssets="analyzers"` treats it purely as an analyzer.
## Runtime dependencies
| Telemetry type | Required reference | Notes |
| --- | --- | --- |
| Activities | `System.Diagnostics.DiagnosticSource` | Included in the SDK for .NET 5+ |
| Logging | `Microsoft.Extensions.Logging.Abstractions` | Required for `[Logger]` interfaces; `TSG2003` is raised if `ILogger` cannot be resolved |
| Logging (v2) | `Microsoft.Extensions.Telemetry.Abstractions` | Required for state-based v2 generation (`LoggerMessageHelper`, `[LogProperties]`) |
| Metrics | `System.Diagnostics.Metrics` | Included in the SDK for .NET 8+ |
## Verifying the install
Create an interface with a telemetry attribute and build:
```csharp
using Purview.Telemetry;
[Logger]
interface IMyTelemetry
{
[Info]
void Hello();
}
```
If generation succeeds you will see a `MyTelemetryCore` implementation and an `AddMyTelemetry` extension method. To inspect the generated code, enable:
```xml
true
```
Generated files land in `obj/Debug|Release//generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/`.
## Build configuration
### `PURVIEW_TELEMETRY_ATTRIBUTES`
Marker attributes are generated as `[Conditional("PURVIEW_TELEMETRY_ATTRIBUTES")]` and are internal to your assembly. Define this constant if you want to retain them in your build (for example, so the analyzer sees attribute usage in projects that share attribute source):
```xml
$(DefineConstants);PURVIEW_TELEMETRY_ATTRIBUTES
```
### `EXCLUDE_PURVIEW_TELEMETRY_LOGGING`
Define this constant to disable logging generation entirely. This is useful when the `Microsoft.Extensions.Logging` types are not available in the compilation:
```xml
$(DefineConstants);EXCLUDE_PURVIEW_TELEMETRY_LOGGING
```
### `PURVIEW_TELEMETRY_NON_NULLABLE`
Define this constant to opt out of the `TSG1011` unsupported-target-framework check for non-net8+/net48+ compilations:
```xml
$(DefineConstants);PURVIEW_TELEMETRY_NON_NULLABLE
```
## Project-type snippets
### ASP.NET Core
```csharp
builder.Services.AddWeatherServiceTelemetry();
```
### Console / library
```csharp
services.AddOrderServiceTelemetry();
```
### .NET Aspire
```csharp
builder.AddServiceDefaults(TelemetryNames.MeterNames, TelemetryNames.ActivitySourceNames);
```
See the [sample application](../sample-application/) for a complete Aspire setup on .NET 10.
## Troubleshooting
**"Type or namespace 'ActivitySourceAttribute' could not be found"**
- Ensure `using Purview.Telemetry;` is present (all attributes live in the single `Purview.Telemetry` namespace).
- Check the package reference includes `IncludeAssets="analyzers"`.
**"No implementation found for interface"**
- Verify the interface has at least one of `[ActivitySource]`, `[Logger]`, or `[Meter]`.
- Check that methods have the appropriate method-level attributes.
- Rebuild to trigger source generation.
**"ActivitySource not producing traces"**
- Ensure the generated `Add{InterfaceName}()` DI registration method has been called.
- Configure OpenTelemetry to listen to your ActivitySource name.
**"Logs not appearing"**
- Verify `ILogger` is configured in your application.
- Check that log-level filters are not excluding your messages.
- Ensure DI registration was called.
**"Metrics not collected"**
- Configure a metrics exporter in your application.
- Verify the meter name matches what your collector expects.
- Confirm instruments are recorded with valid measurement values.
## Next steps
- [Getting Started](../) — write your first telemetry interface
- [Generation Options](../generation/) — control class names, DI, and naming
- [Diagnostics](../diagnostics/) — analyzer warnings and errors
---
# Generation Options
This page describes the `[TelemetryGeneration]` attribute and the other assembly-level generation attributes that control how code is generated: class names, DI registration, naming conventions, and the `TelemetryNames` class.
## Generated class naming
For an interface `IOrderServiceTelemetry`, the generator produces:
- An implementation class named `OrderServiceTelemetryCore` (the interface name without the leading `I`, plus `Core`).
- A static DI extension class `OrderServiceTelemetryCoreDIExtension` with an `AddOrderServiceTelemetry` extension method.
Both can be overridden with the `[TelemetryGeneration]` attribute.
## `[TelemetryGeneration]`
`[TelemetryGeneration]` can be applied at the **assembly** level (affects every generated interface) or on an individual **interface**.
```csharp
// Assembly-level
[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]
// Interface-level
[TelemetryGeneration(ClassName = "MyCustomTelemetry")]
interface IOrderServiceTelemetry { }
```
| Property | Default | Description |
| --- | --- | --- |
| `GenerateDependencyExtension` | `true` | Whether to emit the static DI extension class with the `Add{InterfaceName}()` method. |
| `ClassName` | `null` | Overrides the generated implementation class name. When unset, the name is `{InterfaceName without I}Core`. |
| `DependencyInjectionClassName` | `null` | Overrides the DI extension class name. When unset, the name is `{implementationClassName}DIExtension`. |
| `DependencyInjectionClassIsPublic` | `false` | When `true` the DI extension class is `public`; otherwise it is `internal`. |
| `NamingConvention` | `NamingConvention.OpenTelemetry` | Controls generated telemetry names; see [Naming conventions](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/docs/wiki#naming-conventions). |
| `GenerateTelemetryNamesClass` | `true` | When `false`, suppresses the whole-assembly `TelemetryNames` class. |
| `TelemetryNamesClassName` | `null` | Custom name for the `TelemetryNames` class (default `TelemetryNames`). |
| `TelemetryNamesNamespace` | `null` | When set, relocates the implementation classes, the DI extension class, and the `TelemetryNames` class into this namespace. |
Resolution order is interface-level first, then assembly-level, then built-in defaults.
## Naming conventions
The `NamingConvention` enum controls the generated telemetry names.
```csharp
public enum NamingConvention
{
Legacy = 0, // v3 behaviour: lowercase, smashed compounds
OpenTelemetry = 1 // v4+ default: OTel conventions
}
```
| Telemetry type | Legacy (v3) | OpenTelemetry (default) |
| --- | --- | --- |
| ActivitySource name | Assembly name lowercased: `"myapp"` | Assembly name preserved: `"MyApp"` |
| Tag / baggage keys | Lowercased, smashed: `"entityid"` | snake_case: `"entity_id"` |
| Metric instrument names | Lowercased, smashed: `"recordhistogram"` | Hierarchical dot.separated: `"myapp.products.record.histogram"` |
| Metric tag keys | Lowercased, smashed: `"requestcount"` | snake_case: `"request_count"` |
```csharp
// Revert all telemetry to v3 naming (assembly-level)
[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]
// Or set per-interface
[TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]
interface IMyTelemetry { }
```
:::tip
Use `NamingConvention.OpenTelemetry` (the default) for new projects. Only use `Legacy` if you need exact v3 name compatibility.
:::
## Dependency injection
By default every telemetry interface generates a static extension class:
```csharp
public static class OrderServiceTelemetryCoreDIExtension
{
public static IServiceCollection AddOrderServiceTelemetry(this IServiceCollection services);
}
```
- The extension method name is `Add` + `{InterfaceName without the leading I}`.
- The class lives in the `Microsoft.Extensions.DependencyInjection` namespace so the extension method is discoverable with the standard `using`.
- The generated registration is `services.AddSingleton()`.
- Set `GenerateDependencyExtension = false` to disable it, or `DependencyInjectionClassIsPublic = true` to make the class public.
## The `TelemetryNames` class
When a compilation contains at least one `[ActivitySource]` or `[Meter]` target, the generator emits a single `TelemetryNames` static class per assembly with the distinct source names:
```csharp
public static class TelemetryNames
{
public static readonly string[] MeterNames;
public static readonly string[] ActivitySourceNames;
}
```
It is used to register names with OpenTelemetry/ServiceDefaults:
```csharp
builder.AddServiceDefaults(TelemetryNames.MeterNames, TelemetryNames.ActivitySourceNames);
```
Control it with `GenerateTelemetryNamesClass`, `TelemetryNamesClassName`, and `TelemetryNamesNamespace`.
## `[Exclude]`
Mark a method with `[Exclude]` to skip it entirely during generation. This is useful for interface members that should not produce telemetry.
```csharp
[Logger]
interface IOrderServiceTelemetry
{
[Info]
void OrderPlaced(int orderId);
[Exclude]
void DiagnosticOnly();
}
```
## Assembly-level generation attributes
| Attribute | What it controls |
| --- | --- |
| `[ActivitySourceGeneration]` | Default ActivitySource name, `DefaultToTags` behaviour, baggage/tag prefix and separator, and whether missing-Activity diagnostics (`TSG3014`/`TSG3015`) are generated. |
| `[LoggerGeneration]` | Assembly-level default log level, default `LogPrefixType`, and default logging generation mode. |
| `[MeterGeneration]` | Default meter name, meter-name generation type, instrument prefix/separator, and casing defaults. |
See [Activities](../activities/), [Logging](../logging/), and [Metrics](../metrics/) for the full property tables.
## Next steps
- [Activities](../activities/), [Logging](../logging/), [Metrics](../metrics/) — per-target attribute reference
- [Tags, Baggage, and Parameter Attributes](../tags-and-baggage/) — parameter-level attributes
- [Diagnostics](../diagnostics/) — analyzer rules
---
# Activities
All activity-related attributes live in the `Purview.Telemetry` namespace. To signal an interface for Activity generation, decorate it with the `[ActivitySource]` attribute.
:::caution
All attributes are now in the unified `Purview.Telemetry` namespace. See [Breaking Changes](../breaking-changes/#namespace-consolidation) for migration details.
:::
## ActivitySource naming
In v5 the ActivitySource name defaults to the **assembly name with casing preserved** (OpenTelemetry convention). You can override it at the interface or assembly level:
- Interface: `ActivitySourceAttribute.Name`
- Assembly: `ActivitySourceGenerationAttribute.Name`
:::note
`purview` is only used as a fallback when no assembly name is available, in which case the `TSG3001` diagnostic is generated.
:::
## Activity, Event, or Context
There are three method types:
1. **Activity** methods that generate either a started or un-started [`Activity`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activity).
2. **Event** methods that generate an [`ActivityEvent`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activityevent) attached to an Activity.
3. **Context** methods that add either tags or baggage to the current Activity.
:::tip
Always specify the `Activity` explicitly — otherwise `Activity.Current` is used, which may not be the Activity you expect. When generating an Activity, always return `Activity` or `Activity?`. When using Event and Context methods, always pass in the `Activity` instance returned by an Activity method.
:::
### Activity
Decorate the method with the `[Activity]` attribute to explicitly define an Activity method. Parameters can be passed directly to `ActivitySource.CreateActivity` or `ActivitySource.StartActivity`:
- **tags** — the parameter type must be an [`ActivityTagsCollection`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activitytagscollection), `IEnumerable>`, or [`TagList`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.taglist).
- **parentContext** — the parameter type must be [`ActivityContext`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activitycontext).
- **parentId** — the parameter must be named `parentId` with type `string`.
- **links** — the parameter type must be an `IEnumerable`.
- **startTime** — the parameter must be named `startTime` with type `DateTimeOffset`.
Other parameters can be marked with `[Tag]` or `[Baggage]` to specify where they are applied. The return type must be `void`, `Activity`, or `Activity?`; returning the Activity returns the created or started Activity.
### Events
Decorate the method with the `[Event]` attribute to generate a method that creates an `ActivityEvent` and attaches it to the specified `Activity` or `Activity.Current`. To specify an Activity explicitly, the parameter type must be `Activity` or `Activity?`.
The tags collection can be populated with a parameter of type `ActivityTagsCollection`, `IEnumerable>`, or `TagList`. To specify the `timestamp`, name the parameter `timestamp` with type `DateTimeOffset`.
The return type must be `void`, `Activity`, or `Activity?`. When returning an Activity, either the one specified as a parameter is used, or `Activity.Current`.
#### Exceptions
When an `Exception` parameter is present, the default behaviour follows the [OpenTelemetry exception rules](https://opentelemetry.io/docs/specs/otel/trace/exceptions/): an event named `exception` is added with the following tags:
- `exception.escaped` — `true` by default; override it by decorating a `bool` parameter with `[Escape]`.
- `exception.message` — the value of `Exception.Message`.
- `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). 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
Decorate the method with the `[Context]` attribute to generate a method that populates tags and/or baggage on a specified `Activity` or `Activity.Current`. Parameters are marked with `[Tag]` or `[Baggage]`. The return type must be `void`, `Activity`, or `Activity?`.
## Inferring method type
On a **single-target** Activities interface you can skip the method-level attribute:
- If the method name ends with `Event` (case-sensitive), it is treated as an Event.
- If the first parameter is an `Activity`, it is treated as an Event.
- If the method name ends with `Context` (case-sensitive), it is treated as a Context.
- Anything else defaults to creating an Activity.
:::note
Inference is disabled on multi-target interfaces — see [Multi-Targeting](../multi-targeting/).
:::
## Inferring tags or baggage
If you decorate a parameter with `[Tag]` or `[Baggage]`, that stops inference. Any undecorated parameter is treated as a tag or baggage based on the default settings:
- `ActivitySourceAttribute.DefaultToTags` (interface) — `true` means tags, `false` means baggage. Default `true`.
- `ActivitySourceGenerationAttribute.DefaultToTags` (assembly) — same. Default `true`.
See also [Tags, Baggage, and Parameter Attributes](../tags-and-baggage/).
## Attribute reference
### `[Activity]`
Defines Activity creation on a method.
| Property | Type | Description |
| --- | --- | --- |
| `Name` | `string?` | The name of the Activity. If not provided, the method name is used. Available on construction. |
| `Kind` | [`ActivityKind`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activitykind) | The kind used to create the Activity. Default `Internal`. Available on construction. |
| `CreateOnly` | `bool` | Whether the created Activity is started or not. Default `false`. When `true`, you must return the `Activity`/`Activity?` from the method. |
### `[ActivitySource]`
Defines the creation of the [`ActivitySource`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activitysource) on an interface.
| Property | Type | Description |
| --- | --- | --- |
| `Name` | `string?` | The ActivitySource name. If not provided, `ActivitySourceGenerationAttribute.Name` is used, then the assembly name. A warning (`TSG3001`) is generated when no custom name is defined anywhere. Available on construction. |
| `DefaultToTags` | `bool` | Whether undecorated parameters are added as tags (`true`) or baggage (`false`). Special-case parameters are matched first. Default `true`. |
| `BaggageAndTagPrefix` | `string?` | Prefix for tag/baggage names. Useful for grouping. Default `null`. |
| `IncludeActivitySourcePrefix` | `bool` | Whether the `ActivitySourceGenerationAttribute.BaggageAndTagSeparator` is used to generate a prefix. Default `true`. |
| `LowercaseBaggageAndTagKeys` | `bool` | Whether tag/baggage names are lower-cased. Default `true`. |
### `[ActivitySourceGeneration]`
Defines ActivitySource defaults at the assembly level.
| Property | Type | Description |
| --- | --- | --- |
| `Name` | `string` | Default ActivitySource name when none is defined on an interface. |
| `DefaultToTags` | `bool` | Whether undecorated parameters are tags (`true`) or baggage (`false`). Default `true`. |
| `BaggageAndTagPrefix` | `string?` | Prefix for tag/baggage names. Default `null`. |
| `BaggageAndTagSeparator` | `string` | Separator used when generating prefixes. Default `.`. |
| `LowercaseBaggageAndTagKeys` | `bool` | Whether tag/baggage names are lower-cased. Default `true`. |
| `GenerateDiagnosticsForMissingActivity` | `bool` | Whether diagnostics (`TSG3014`/`TSG3015`) are raised when Activity parameters or return values are missing. Default `true`. |
### `[Baggage]`
Marks a parameter as baggage on an Activity or Event.
| Property | Type | Description |
| --- | --- | --- |
| `Name` | `string?` | The baggage name. Defaults to `null`, meaning the parameter name is used. |
| `SkipOnNullOrEmpty` | `bool` | Whether the parameter is skipped when it is `null` or default. Default `false`. |
### `[Context]`
Marks a method as adding parameters as tags or baggage to an Activity. There are no properties.
### `[StatusDescription]`
Marks a `string` parameter as the status description for an Activity Event — typically used with events that set an error status code.
```csharp
[ActivitySource("MyApp")]
interface IMyTelemetry
{
[Event]
void OperationFailed(
Activity? activity,
[StatusDescription]string failureReason
);
}
```
The parameter must be of type `string`, and this attribute is only valid on Event methods, not Activity or Context methods.
### `[Escape]`
Marks a `bool` parameter as the escape value on an event-based method. See the [OpenTelemetry exception rules](https://opentelemetry.io/docs/specs/otel/trace/exceptions/).
### `[Event]`
Defines a method that creates an `ActivityEvent`.
| Property | Type | Description |
| --- | --- | --- |
| `Name` | `string?` | The name of the event. If not provided, the method name is used. Available on construction. |
| `StatusCode` | [`ActivityStatusCode`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.activitystatuscode) | Sets the status code on the Activity after the event is added. Default `Unset`. Available on construction. When set to `Error`, the status description is sourced (in order of precedence) from a `[StatusDescription]`-marked parameter, the `StatusDescription` property, the first `Exception` parameter's `Message`, or `null`. |
| `StatusDescription` | `string?` | A static description for the `StatusCode` when set to `Error`. Overridden by a `[StatusDescription]`-marked parameter if present. |
| `UseRecordExceptionRules` | `bool` | Whether the OpenTelemetry exception rules are followed when a parameter is an `Exception`. Default `true`. |
| `RecordExceptionAsEscaped` | `bool` | The value used for `exception.escaped` when `UseRecordExceptionRules` is `true` and an exception is present. Overridable with `[Escape]`. Default `true`. |
## Next steps
- [Tags, Baggage, and Parameter Attributes](../tags-and-baggage/) — parameter-level attributes
- [Multi-Targeting](../multi-targeting/) — combining Activities with Logging and Metrics
- [Diagnostics](../diagnostics/) — the `TSG3xxx` Activity rules
---
# Logging
There are two distinct generated types for [`ILogger`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger)-based generation.
To signal an interface for logging generation, decorate it with the `[Logger]` attribute. To signal a method, use `[Log]` (or one of the semantic level attributes). All logging attributes live in the `Purview.Telemetry` namespace.
## Generation v1 vs v2
| | Generation v1 | Generation v2 |
| --- | --- | --- |
| Implementation | [`LoggerMessage`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loggermessage)/[`LoggerMessage.Define`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loggermessagedefine) high-performance logging | State-based output resembling the built-in [`[LoggerMessage]`](https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator) generator |
| Parameter limit | Maximum 6 non-exception parameters plus one optional `Exception` | No fixed parameter limit |
| `[ExpandEnumerable]` | No effect | Supported |
| `[LogProperties]` | No effect | Supported |
| Required reference | `Microsoft.Extensions.Logging` | `Microsoft.Extensions.Telemetry.Abstractions` |
### How `Auto` mode works
The default `LoggerGenerationMode.Auto` selects the mode **per method**:
- **v1** is used when the method is within the v1 limits (≤ 6 non-exception parameters, at most one `Exception`, and no `[ExpandEnumerable]` or `[LogProperties]` parameters).
- **v2** is used when the method needs it (more than 6 parameters, `[ExpandEnumerable]`, or `[LogProperties]`), which requires `Microsoft.Extensions.Telemetry.Abstractions`.
You can force a mode with the `GenerationMode` property:
- `[Log].GenerationMode` — per method
- `[Logger].GenerationMode` — per interface
- `[LoggerGeneration].GenerationMode` — per assembly
```csharp
// Force v2 for one method
[Logger]
interface IOrderServiceTelemetry
{
[Info(GenerationMode = LoggerGenerationMode.V2)]
void OrderPlaced(int orderId, string customerName);
}
```
:::caution
If you force `V2` (or use a method that requires v2) without referencing `Microsoft.Extensions.Telemetry.Abstractions`, the generated code will not compile because it uses `LoggerMessageHelper` and `LogPropertiesAttribute` from that package.
:::
## Disabling logging generation
Define the `EXCLUDE_PURVIEW_TELEMETRY_LOGGING` constant to ignore the logging attributes entirely:
```xml
EXCLUDE_PURVIEW_TELEMETRY_LOGGING
```
This is primarily used when the [`ILogger`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.ilogger) type is unavailable — without it your project will fail to compile because the generated attributes reference related types such as [`LogLevel`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel).
## Scoped loggers
Return `IDisposable`/`IDisposable?` from a log method to generate a scoped log entry:
```csharp
[Logger]
interface IOrderServiceTelemetry
{
[Info]
IDisposable? ProcessingOrder(Guid orderId);
}
```
When a method returns `IDisposable?`, the entry is emitted when the scope is created and the duration is logged when it is disposed. A scoped method should not specify an explicit level (`TSG2007` warns if it does, since the level is ignored).
## Next pages
- [Generation v1](../logging-generation-v1/) — `LoggerMessage.Define` mode and its limits
- [Generation v2](../logging-generation-v2/) — state-based mode, `[ExpandEnumerable]`, `[LogProperties]`
- [Breaking Changes](../breaking-changes/#namespace-consolidation) — namespace migration
- [Diagnostics](../diagnostics/) — the `TSG2xxx` Logging rules
---
# Logging Generation v1
:::caution
All attributes are now in the unified `Purview.Telemetry` namespace. See [Breaking Changes](../breaking-changes/#namespace-consolidation) for migration details.
:::
Generation v1 is the previous style of log generation, built on [`LoggerMessage.Define`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loggermessagedefine) from the [high-performance logging](https://learn.microsoft.com/en-us/dotnet/core/extensions/high-performance-logging) libraries.
It has these limitations:
- A maximum of **6** non-exception parameters.
- At most **one** `Exception` parameter.
- No support for expanding enumerations (`[ExpandEnumerable]` has no effect).
- No support for `[LogProperties]`.
:::caution
The maximum number of parameters allowed by the `LoggerMessage` class is **6**, plus one optional `Exception`. You must reference the `Microsoft.Extensions.Logging` package.
:::
## Selecting v1
v1 is used automatically in `LoggerGenerationMode.Auto` for any method that is within the v1 limits. You can force it for all methods on an interface or assembly:
```csharp
[Logger(GenerationMode = LoggerGenerationMode.V1)]
interface IOrderServiceTelemetry { }
```
## Log generation
To generate a log entry, decorate the method with `[Log]` and return either `void` (non-scoped) or `IDisposable`/`IDisposable?` (scoped). Parameters form part of the structured log generation and are referenced by the `MessageTemplate`.
You can also use the semantic level attributes instead of `[Log].Level`:
- `[Trace]`, `[Debug]`, `[Info]`, `[Warning]`, `[Error]`, `[Critical]`
These support all the same properties as `[Log]`, except `Level`.
## Inferring
When not using multi-targeting you can omit `[Log]` entirely — every method on a `[Logger]` interface becomes a log method. Set the default level with `[Logger].DefaultLevel` (interface) or `[LoggerGeneration].DefaultLevel` (assembly).
If no level is specified on the method and an `Exception` parameter is present, the level is changed to `Error` automatically. This raises the `TSG2002` diagnostic, which can safely be ignored if you are comfortable with the inferred behaviour.
## Attribute reference
### `[Log]`
| Property | Type | Description |
| --- | --- | --- |
| `Level` | [`LogLevel`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel) | The level used when defining the log entry. Available on construction. Defaults to `Information`, unless an `Exception` is detected in the parameters, then `Error`. |
| `MessageTemplate` | `string?` | The template used to populate the log entry. If not specified, one is generated from the prefixes and available parameters. Available on construction. Default `null`. |
| `EventId` | `int?` | Used when generating the [`EventId`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.eventid). Available on construction. Default `null` (one is generated if not supplied). |
| `Name` | `string?` | The name of the log entry. If not defined, the method name is used. Available on construction. Default `null`. |
| `GenerationMode` | `LoggerGenerationMode` | Per-method generation-mode override. `Auto` (default) inherits from the interface/assembly level. |
### `[Logger]`
| Property | Type | Description |
| --- | --- | --- |
| `DefaultLevel` | `LogLevel` | The default level when one is not provided. Available on construction. Default `Information`. |
| `CustomPrefix` | `string?` | Used when generating the log entry name's prefix. When set, `PrefixType` is automatically `Custom`. Available on construction. Default `null`. |
| `PrefixType` | `LogPrefixType` | The prefix type used when generating the log entry name. Default `Default`. |
| `GenerationMode` | `LoggerGenerationMode` | Controls the generation mode for all log methods on the interface. `Auto` (default) selects the best mode per method. |
### `[LoggerGeneration]`
| Property | Type | Description |
| --- | --- | --- |
| `DefaultLevel` | `LogLevel` | The default level when one is not provided. Available on construction. Default `Information`. |
| `GenerationMode` | `LoggerGenerationMode` | Controls the generation mode for all log methods in the assembly. `Auto` (default) selects the best mode per method. |
| `DefaultPrefixType` | `LogPrefixType` | The default prefix type for all log entries in the assembly. Default `Default`. |
### `LogPrefixType`
| Value | Description |
| --- | --- |
| `Default` | No prefix. |
| `Interface` | Uses the name of the interface. |
| `Class` | The name of the class used for generation (`TelemetryGenerationAttribute.ClassName` or auto-generated). |
| `Custom` | Used when `LoggerAttribute.CustomPrefix` is set. |
| `TrimmedClassName` | The interface name without the `I` prefix or `Log`, `Logger`, or `Telemetry` suffixes. |
## Next steps
- [Logging Generation v2](../logging-generation-v2/) — the state-based mode with `[ExpandEnumerable]`/`[LogProperties]`
- [Logging](../logging/) — choosing between v1 and v2
- [Diagnostics](../diagnostics/) — the `TSG2xxx` Logging rules
---
# Logging Generation v2
:::caution
All attributes are now in the unified `Purview.Telemetry` namespace. See [Breaking Changes](../breaking-changes/#namespace-consolidation) for migration details.
:::
Generation v2 is the state-based logging mode. Its output closely resembles the built-in [`[LoggerMessage]`](https://learn.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator) generator, but with additional features: dynamically generated `MessageTemplate`s and the ability to expand array/`IEnumerable` parameters.
It is used automatically in `LoggerGenerationMode.Auto` for methods that exceed the [v1](../logging-generation-v1/) limits (more than 6 non-exception parameters, or `[ExpandEnumerable]`/`[LogProperties]` parameters).
:::caution
Generation v2 requires the `Microsoft.Extensions.Telemetry.Abstractions` package (`LoggerMessageHelper` and `LogPropertiesAttribute`). Reference it directly or transitively when any method uses v2 generation.
:::
## Selecting v2
```csharp
// Force v2 for the whole interface
[Logger(GenerationMode = LoggerGenerationMode.V2)]
interface IOrderServiceTelemetry { }
// Or per method
[Logger]
interface IOrderServiceTelemetry
{
[Info(GenerationMode = LoggerGenerationMode.V2)]
void OrderPlaced(int orderId, string customerName);
}
```
## Log generation
As with v1, decorate a method with `[Log]` (or a semantic level attribute) and return either `void` (non-scoped) or `IDisposable`/`IDisposable?` (scoped).
The shared `[Log]`, `[Logger]`, `[LoggerGeneration]`, and `LogPrefixType` reference tables are on the [Logging Generation v1](../logging-generation-v1/#log) page.
## Custom message templates
`[Log].MessageTemplate` lets you customise the log message. Template placeholders map to method parameters:
```csharp
[Logger]
interface IOrderServiceTelemetry
{
[Info("Order {OrderId} placed for {CustomerName}")]
void OrderPlaced(int orderId, string customerName);
}
```
If no template is specified, one is generated from the method name and parameters.
## `[ExpandEnumerable]`
Applied to an array or `IEnumerable` parameter, it logs the individual elements rather than the collection as a whole.
| Property | Type | Description |
| --- | --- | --- |
| `MaximumValueCount` | `int` | The maximum number of elements to output. Default `5`. |
```csharp
[Logger]
interface IOrderServiceTelemetry
{
[Info]
void OrdersRetrieved(int orderId, [ExpandEnumerable(maximumValueCount: 100)] string[] orderNumbers);
}
```
:::note
A `MaximumValueCount` greater than the recommended default of 5 generates the `TSG2008` warning. It can be ignored, but test your application's performance thoroughly.
:::
## `[LogProperties]`
The external `Microsoft.Extensions.Logging.LogPropertiesAttribute` (from `Microsoft.Extensions.Telemetry.Abstractions`) expands an object's public properties into individual log properties.
| Property | Type | Description |
| --- | --- | --- |
| `OmitReferenceName` | `bool` | Whether the reference name is omitted from property names. Default `false`. |
| `SkipNullProperties` | `bool` | Whether null properties are skipped. Default `false`. |
| `Transitive` | `bool` | Whether nested objects are expanded transitively. Default `false`. |
The companion `LogPropertyIgnoreAttribute` marks a property to be skipped during expansion.
:::caution
`[LogProperties]` and `[ExpandEnumerable]` cannot be applied to the same parameter — this raises `TSG2006`.
:::
## Next steps
- [Logging Generation v1](../logging-generation-v1/) — the `LoggerMessage.Define` mode and its limits
- [Logging](../logging/) — choosing between v1 and v2
- [Diagnostics](../diagnostics/) — the `TSG2xxx` Logging rules
---
# Metrics
:::caution
All attributes are now in the unified `Purview.Telemetry` namespace. See [Breaking Changes](../breaking-changes/#namespace-consolidation) for migration details.
:::
All metric-related attributes live in the `Purview.Telemetry` namespace. To signal an interface for meter generation, decorate it with the `[Meter]` attribute.
When creating the meter types, the [`IMeterFactory`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.imeterfactory) is used when available (it is not available on .NET Framework 4.8, where a `new Meter(...)` is created instead).
## Meter naming
The meter name is resolved in this order:
1. `MeterAttribute.Name` (interface)
2. `MeterGenerationAttribute.MeterName` (assembly)
3. The assembly name
```csharp
[Meter("InventoryService")]
interface IInventoryMetrics { }
```
When the `NamingConvention.OpenTelemetry` naming convention is combined with `MeterNameGenerationType.OpenTelemetry`, instrument names are generated with the meter name as a dot-separated prefix (for example, `myapp.products.record.count`).
## Initialisation
During initialisation you can implement a partial method to provide additional tags to any meters created by the `IMeterFactory`:
```csharp
partial void PopulateMeterTags(System.Collections.Generic.Dictionary meterTags)
{
meterTags["environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
}
```
Any tags added to `meterTags` are included on every meter created.
## Instrument types
Each instrument is determined by its corresponding attribute:
- `[AutoCounter]` and `[Counter]` generate the [`Counter`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.counter-1) instrument.
- `[Histogram]` generates the [`Histogram`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.histogram-1) instrument.
- `[UpDownCounter]` generates the [`UpDownCounter`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.updowncounter-1) instrument.
- `[ObservableCounter]` generates the [`ObservableCounter`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.observablecounter-1) instrument.
- `[ObservableGauge]` generates the [`ObservableGauge`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.observablegauge-1) instrument.
- `[ObservableUpDownCounter]` generates the [`ObservableUpDownCounter`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.observableupdowncounter-1) instrument.
The measurement type must be one of `byte`, `short`, `int`, `long`, `float`, `double`, or `decimal`.
## The measurement value
The measurement parameter is the parameter decorated with `[InstrumentMeasurement]`. For non-auto-increment instruments, if no parameter is decorated the first parameter with a valid measurement type that is not a `[Tag]`/`[Baggage]` is used.
- `[AutoCounter]` and `[Counter(AutoIncrement = true)]` increment by **1** each time the method is called and must **not** declare a measurement parameter (`TSG4002`).
- `[Counter]`, `[Histogram]`, and `[UpDownCounter]` require a measurement value (`TSG4004`).
- Observable instruments must declare a `Func` parameter (`TSG4005`).
```csharp
[Meter]
interface IMeterTelemetry
{
[AutoCounter]
void AutoIncrementMeter([Tag]string someValue);
[Counter(AutoIncrement = true)]
void AutoIncrementCounterMeter([Tag]string someValue);
[Counter]
void CounterMeter([InstrumentMeasurement]int measurement, [Tag]float someValue);
[Histogram]
void HistogramMeter([InstrumentMeasurement]int measurement, [Tag]int someValue, [Tag]bool anotherValue);
[UpDownCounter]
void UpDownCounterMeter([InstrumentMeasurement]decimal measurement, [Tag]byte someValue);
}
```
### Observable instruments
Observable instruments always take a `System.Func<>` parameter with one of the following shapes:
- Any supported measurement type: `byte`, `short`, `int`, `long`, `float`, `double`, or `decimal`
- [`Measurement`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.measurement-1) where `T` is a supported measurement type
- `IEnumerable>` where `T` is a supported measurement type
```csharp
[ObservableCounter]
void Counter(Func func);
[ObservableGauge]
void Gauge(Func> func);
[ObservableUpDownCounter]
void UpDownCounter(Func>> func);
```
### Tags
Other parameters on the method are used as tags. This is implicit for non-measurement parameters, but can also be made explicit with [`[Tag]`](../tags-and-baggage/). When there are four or more tags, the generated code uses a stack-allocated [`TagList`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.taglist).
## Attribute reference
### `[Meter]`
Defines and controls the generation of meters and instruments on an interface, overriding assembly-level defaults.
| Property | Type | Description |
| --- | --- | --- |
| `Name` | `string?` | The name of the meter, used to group instruments. If not specified, `MeterGenerationAttribute.MeterName` is used, then the assembly name. Default `null`. |
| `InstrumentPrefix` | `string?` | The prefix used when generating instrument names. Default `null`. |
| `IncludeAssemblyInstrumentPrefix` | `bool` | Whether the `MeterGenerationAttribute.InstrumentPrefix` prefix is included in instrument names. Default `true`. |
| `LowercaseInstrumentName` | `bool` | Whether instrument names (including prefix) are lower-cased. Default `true`. |
| `LowercaseTagKeys` | `bool` | Whether tag names are lower-cased. Default `true`. |
### `[MeterGeneration]`
Controls meter and instrument defaults at the assembly level.
| Property | Type | Description |
| --- | --- | --- |
| `MeterName` | `string?` | The default meter name when none is defined on an interface. Available on construction. |
| `MeterNameGenerationType` | `MeterNameGenerationType` | Whether the meter name is used as a dot-separated prefix for instrument names. `OpenTelemetry` (0) adds the prefix; `DotNet` (1, default) does not. Available on construction. |
| `InstrumentPrefix` | `string?` | The prefix used when generating instrument names. Default `null`. |
| `InstrumentSeparator` | `string` | The separator used when generating prefixes. Default `.`. |
| `LowercaseInstrumentName` | `bool` | Whether instrument names (including prefix) are lower-cased. Default `true`. |
| `LowercaseTagKeys` | `bool` | Whether tag names are lower-cased. Default `true`. |
### `[InstrumentMeasurement]`
Marks the parameter used as the instrument measurement value. Not supported with `[AutoCounter]` or `[Counter(AutoIncrement = true)]`.
## Instrument attributes
### `[AutoCounter]`
Creates a `Counter` that increments by 1 per call.
| Property | Type | Description |
| --- | --- | --- |
| `Name` | `string?` | Instrument name; defaults to the method name. Available on construction. Default `null`. |
| `Unit` | `string?` | The unit used during meter generation. Available on construction. Default `null`. |
| `Description` | `string?` | The description used during meter generation. Available on construction. Default `null`. |
### `[Counter]`
Creates a `Counter`.
| Property | Type | Description |
| --- | --- | --- |
| `AutoIncrement` | `bool` | When `true`, generates an auto-incrementing counter instead of accepting a measurement value from a parameter. Available on construction. Default `false`. |
| `Name` | `string?` | Instrument name; defaults to the method name. Available on construction. Default `null`. |
| `Unit` | `string?` | The unit used during meter generation. Available on construction. Default `null`. |
| `Description` | `string?` | The description used during meter generation. Available on construction. Default `null`. |
When `AutoIncrement` is `true` and `[InstrumentMeasurement]` is used, the `TSG4002` diagnostic is raised.
### `[Histogram]` and `[UpDownCounter]`
Create a `Histogram` or `UpDownCounter`. Both share the `Name`, `Unit`, and `Description` properties described above.
### Observable attributes
`[ObservableCounter]`, `[ObservableGauge]`, and `[ObservableUpDownCounter]` share the `Name`, `Unit`, and `Description` properties, plus:
| Property | Type | Description |
| --- | --- | --- |
| `ThrowOnAlreadyInitialized` | `bool` | Whether the method throws when called more than once. Available on construction. Default `false`. |
## Next steps
- [Tags, Baggage, and Parameter Attributes](../tags-and-baggage/) — tagging parameters
- [Multi-Targeting](../multi-targeting/) — combining Metrics with Activities and Logging
- [Diagnostics](../diagnostics/) — the `TSG4xxx` Metrics rules
---
# Multi-Targeting
Multi-targeting lets you generate **multiple types of telemetry** (Activities, Logs, and Metrics) from a single interface or even a single method. One method call can emit an Activity, a structured log entry, and a metric simultaneously.
## Interface-level multi-targeting
Apply multiple generation attributes to an interface to enable all telemetry types:
```csharp
using Purview.Telemetry;
[ActivitySource("OrderService")]
[Logger]
[Meter("OrderService")]
interface IOrderTelemetry
{
// Methods can use one or more telemetry types
}
```
## Method-level multi-targeting
Multiple telemetry attributes can be combined on a single method. When called, the method emits all specified telemetry types:
```csharp
[ActivitySource("OrderService")]
[Logger]
[Meter]
interface IOrderTelemetry
{
// MULTI-TARGET: Creates Activity + Logs Info + Increments Counter from one call
[Activity]
[Info]
[AutoCounter]
Activity? ProcessingOrder([Baggage]int orderId, [Tag]string customerName);
// MULTI-TARGET: Adds ActivityEvent + Logs as Debug
[Event]
[Debug]
void OrderValidated(Activity? activity, decimal amount);
// SINGLE-TARGET: only logs
[Warning]
void OrderRejected(int orderId, string reason);
// SINGLE-TARGET: only metric
[Histogram]
void OrderProcessingDuration([InstrumentMeasurement]int milliseconds);
}
```
```csharp
// Single method call emits 3 telemetry types
using var activity = telemetry.ProcessingOrder(123, "Acme Corp");
// ✓ Activity created and started
// ✓ Info log entry written
// ✓ Counter incremented by 1
```
### Supported combinations
| Combination | Supported | Example |
| --- | --- | --- |
| Activity + Log | ✅ | `[Activity]` + `[Info]` |
| Activity + Metric | ✅ | `[Activity]` + `[AutoCounter]` |
| Log + Metric | ✅ | `[Info]` + `[Histogram]` |
| Activity + Log + Metric | ✅ | `[Activity]` + `[Info]` + `[AutoCounter]` |
| Event + Log | ✅ | `[Event]` + `[Debug]` |
| Context + Log | ✅ | `[Context]` + `[Trace]` |
**Rules:**
- Only **one attribute per telemetry family** is allowed per method (`TSG1002`).
- Activities: use one of `[Activity]`, `[Event]`, or `[Context]`.
- Logging: use one of `[Log]`, `[Trace]`, `[Debug]`, `[Info]`, `[Warning]`, `[Error]`, `[Critical]`.
- Metrics: use one of `[Counter]`, `[AutoCounter]`, `[Histogram]`, `[UpDownCounter]`, or the observable variants.
```csharp
// ERROR TSG1002: multiple activity attributes
[Activity]
[Event]
void InvalidMethod(Activity? activity);
// ERROR TSG1002: multiple logging attributes
[Info]
[Warning]
void InvalidMethod(string message);
// ERROR TSG1002: multiple metric attributes
[Counter]
[Histogram]
void InvalidMethod([InstrumentMeasurement]int value);
```
## Excluding parameters from specific targets
Use `[ExcludeTargets(Targets.X)]` on a parameter to exclude it from specific telemetry families:
```csharp
[ActivitySource("PaymentService")]
[Logger]
[Meter]
interface IPaymentTelemetry
{
[Activity]
[Info]
[Counter]
Activity? ProcessingPayment(
[Baggage]Guid paymentId,
// Exclude the verbose message from metrics (would be wasted as a tag)
[ExcludeTargets(Targets.Metrics)]
string processingMessage,
// Measurement only applies to metrics
[InstrumentMeasurement]
decimal amount,
// Exclude internal details from Activity baggage
[ExcludeTargets(Targets.Activities)]
[Tag] // still included in logs and metrics
string internalReference
);
}
```
### The `Targets` enum
```csharp
[Flags]
public enum Targets
{
None = 0,
Activities = 1,
Logging = 2,
Metrics = 4,
All = Activities | Logging | Metrics
}
```
```csharp
// Exclude from multiple targets
[ExcludeTargets(Targets.Activities | Targets.Metrics)]
string loggingOnlyParameter;
// Exclude from a single target
[ExcludeTargets(Targets.Logging)]
int metricsAndActivityParameter;
```
## Inference is disabled with multi-targeting
When an interface has **multiple** class-level attributes (`[ActivitySource]`, `[Logger]`, `[Meter]`), inference is disabled and every method must declare its targets explicitly:
```csharp
[ActivitySource("MyApp")]
[Logger]
[Meter]
interface IMyTelemetry
{
// ERROR TSG1001: no explicit attribute
void ProcessItem(int id);
// ✅ CORRECT: explicit attribute
[Info]
void ProcessItem(int id);
// ✅ CORRECT: excluded from generation
[Exclude]
void ProcessItem(int id);
}
```
Single-target interfaces (only one class-level attribute) keep inference — see [Activities](../activities/#inferring-method-type) and [Logging](../logging/).
## Return types
- **Activity + other targets** — return `Activity?` so callers can dispose and reuse the Activity:
```csharp
[Activity]
[Info]
[AutoCounter]
Activity? ProcessingOrder(int orderId);
```
- **Log + Metric (no Activity)** — return `void` or `IDisposable?`:
```csharp
[Info]
[Histogram]
void RecordOperation([InstrumentMeasurement]int duration, string operation);
[Info]
[AutoCounter]
IDisposable? ProcessingBatch(int batchId); // scoped log + counter
```
- **Event/Context + Log** — return `void`:
```csharp
[Event]
[Debug]
void OrderCompleted(Activity? activity, decimal total);
```
## Common patterns
### Complete observability method
```csharp
[Activity] // distributed tracing
[Info] // structured logging
[AutoCounter] // count occurrences
Activity? ProcessingRequest(
[Baggage]string requestId,
[Tag]string endpoint,
[Tag]string method
);
```
### Event with context logging
```csharp
[Event]
[Debug]
void StepCompleted(
Activity? activity,
[Tag]string stepName,
[Tag]int duration
);
```
### Selective parameter usage
```csharp
[Activity]
[Info]
[Counter]
Activity? ApiCall(
[Baggage]string traceId, // Activity baggage only
[ExcludeTargets(Targets.Metrics)]
string verboseMessage, // Activity + Log only
[InstrumentMeasurement]
[ExcludeTargets(Targets.Activities | Targets.Logging)]
int callCount, // Metrics only
[Tag]string endpoint // All three
);
```
## Benefits
1. **Less code** — one method definition generates multiple telemetry types
2. **Consistency** — the same parameters feed every telemetry type
3. **Atomicity** — all telemetry emitted together
4. **Maintainability** — change once, affects all telemetry types
5. **Performance** — a single method call instead of several
## Diagnostics
| Diagnostic | When | Resolution |
| --- | --- | --- |
| `TSG1001` | Method has no explicit attribute on a multi-target interface | Add `[Activity]`, `[Info]`, `[Counter]`, etc., or `[Exclude]` |
| `TSG1002` | Multiple attributes from the same family on one method | Use only one Activity, Logging, or Metrics attribute per method |
| `TSG1006` | `[ExcludeTargets]` references a target not present on the method | Remove `[ExcludeTargets]` or add the target attribute to the method |
| `TSG1007` | `[ExcludeTargets]` leaves an invalid parameter set for a target | Adjust exclusions so valid parameters remain for each target |
## See also
- [Activities](../activities/) — Activity generation details
- [Logging](../logging/) — logging generation details
- [Metrics](../metrics/) — metrics generation details
- [Tags, Baggage, and Parameter Attributes](../tags-and-baggage/) — `[Tag]`, `[Baggage]`, `[ExcludeTargets]`
- [Diagnostics](../diagnostics/) — error codes and resolutions
---
# Tags, Baggage, and Parameter Attributes
:::caution
All attributes are now in the unified `Purview.Telemetry` namespace.
:::
Parameters on telemetry methods can be decorated to control how they are emitted. This page covers the parameter-level attributes.
## `[Tag]`
Used within [Activity](../activities/) and [Metrics](../metrics/) generation to add a parameter as a tag. Tag names follow the configured `NamingConvention`:
- **OpenTelemetry** (default): `snake_case` for compound words (e.g. `"entity_id"`)
- **Legacy**: lowercased, smashed (e.g. `"entityid"`)
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `Name` | `string?` | `null` | Explicitly sets the tag name. When `null`, the parameter name is used (transformed according to the naming convention). |
| `SkipOnNullOrEmpty` | `bool` | `false` | When `true`, the tag is not added if the parameter value is `null` or default. |
### Examples
```csharp
using Purview.Telemetry;
[ActivitySource("OrderService")]
interface IOrderTelemetry
{
[Activity]
Activity? ProcessingOrder(
// Auto-named tag (becomes "order_id" in OpenTelemetry mode)
[Tag]int orderId,
// Explicitly named tag (explicit names are not transformed)
[Tag(Name = "customer.name")]string customerName,
// Skip if null
[Tag(SkipOnNullOrEmpty = true)]string? notes
);
}
```
**OpenTelemetry convention (default):**
```csharp
[Tag]int orderId // Generated: "order_id"
[Tag]string userName // Generated: "user_name"
[Tag(Name = "my.custom.tag")]int value // Generated: "my.custom.tag" (not transformed)
```
**Legacy convention:**
```csharp
[Tag]int orderId // Generated: "orderid"
[Tag]string userName // Generated: "username"
```
To change the convention, see [Naming conventions](../generation/#naming-conventions).
### Best practices
1. **Use explicit names for cross-service tags** to prevent breakage if parameter names change:
```csharp
[Tag(Name = "trace.id")]string traceId
```
2. **Use `SkipOnNullOrEmpty` for optional tags** to avoid cluttering telemetry with null values:
```csharp
[Tag(SkipOnNullOrEmpty = true)]string? optionalContext
```
3. **Follow OpenTelemetry semantic conventions** for standard names such as `http.method`, `http.status_code`, `service.name`.
## `[Baggage]`
Marks a parameter as baggage on an Activity or ActivityEvent. Baggage propagates across service boundaries, unlike tags.
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `Name` | `string?` | `null` | Explicitly sets the baggage name. When `null`, the parameter name is used. |
| `SkipOnNullOrEmpty` | `bool` | `false` | When `true`, the parameter is skipped when `null` or default. |
:::note
`[Baggage]` parameters should be `string`; `TSG3000` warns if a non-string is used (`ToString()` is called).
:::
## `[ExcludeTargets]`
Excludes a parameter from specific telemetry targets. See [Multi-Targeting](../multi-targeting/) for the `Targets` enum values (`None`, `Activities`, `Logging`, `Metrics`, `All`).
| Property | Type | Description |
| --- | --- | --- |
| `ExcludedTargets` | `Targets` | The targets to exclude the parameter from. Available on construction. |
```csharp
[ExcludeTargets(Targets.Metrics)]
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/#expandenumerable).
| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `MaximumValueCount` | `int` | `5` | The maximum number of elements to output from the enumeration/array. |
## `[InstrumentMeasurement]`
Marks the parameter used as the instrument measurement value on a metrics method. See [Metrics](../metrics/#the-measurement-value).
## `[Escape]`
Marks a `bool` parameter as the escape value for an exception event on an Event method. See [Activities](../activities/#escape).
## `[StatusDescription]`
Marks a `string` parameter as the status description for an Event that sets an error status code. See [Activities](../activities/#statusdescription).
## See also
- [Activities](../activities/) — tags/baggage in Activity generation
- [Metrics](../metrics/) — tags in metrics generation
- [Multi-Targeting](../multi-targeting/) — excluding parameters per target
- [Generation](../generation/) — naming conventions
- [Breaking Changes](../breaking-changes/#opentelemetry-aligned-naming) — v3 to v4 naming changes
---
# Generated Output
This page shows real generated output from the [sample application](../sample-application/), produced by `5.0.0`. The interface:
```csharp
[ActivitySource]
[Logger]
[Meter]
interface IEntityStoreTelemetry
{
[Activity]
[Info]
[AutoCounter]
Activity? GettingEntityFromStore(int entityId, [Baggage] string serviceUrl);
[Event]
[Trace]
void GetDuration(Activity? activity, int durationInMS);
[Context]
void RetrievedEntity(Activity? activity, float totalValue, int lastUpdatedByUserId);
[Warning]
void EntityNotFound(int entityId);
[Histogram]
void RecordEntitySize(int sizeInBytes);
}
```
generates one partial `EntityStoreTelemetryCore` class split across Activity, Logging, and Metric files, plus a DI extension class and an assembly-level `TelemetryNames` class.
:::note
To inspect the generated output in your own project, enable `EmitCompilerGeneratedFiles` and look under `obj///generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/`.
:::
## Activities (`EntityStoreTelemetryCore.Activity.g.cs`)
```csharp
//
#nullable enable
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal sealed partial class EntityStoreTelemetryCore : IEntityStoreTelemetry
{
private static readonly global::System.Diagnostics.ActivitySource _activitySource = new global::System.Diagnostics.ActivitySource("sample-weather-app-api");
public static void RecordExceptionInternal(
global::System.Diagnostics.Activity? activity,
global::System.Exception? exception,
bool escape
)
{
if (activity == null || exception == null)
{
return;
}
global::System.Diagnostics.ActivityTagsCollection tagsCollection = new global::System.Diagnostics.ActivityTagsCollection();
tagsCollection.Add("exception.escaped", escape);
tagsCollection.Add("exception.message", exception.Message);
tagsCollection.Add("exception.type", exception.GetType().FullName);
tagsCollection.Add("exception.stacktrace", exception.StackTrace);
global::System.Diagnostics.ActivityEvent recordExceptionEvent = new global::System.Diagnostics.ActivityEvent(name: "exception", timestamp: default, tags: tagsCollection);
activity.AddEvent(recordExceptionEvent);
}
private global::System.Diagnostics.Activity? GettingEntityFromStore_Activity(
int entityId,
string serviceUrl
)
{
if (!_activitySource.HasListeners())
{
return null;
}
global::System.Diagnostics.Activity? activityGettingEntityFromStore = _activitySource.StartActivity("GettingEntityFromStore", global::System.Diagnostics.ActivityKind.Internal, parentId: default, tags: default, links: default, startTime: default);
if (activityGettingEntityFromStore != null)
{
activityGettingEntityFromStore.SetTag("entity_id", entityId);
activityGettingEntityFromStore.SetBaggage("service_url", serviceUrl);
}
return activityGettingEntityFromStore;
}
public global::System.Diagnostics.Activity? GettingEntityFromStore(
int entityId,
string serviceUrl
)
{
var activityResult = GettingEntityFromStore_Activity(entityId, serviceUrl);
GettingEntityFromStore_Logging(entityId, serviceUrl);
GettingEntityFromStore_Metrics(entityId, serviceUrl);
return activityResult;
}
public void RetrievedEntity(
global::System.Diagnostics.Activity? activity,
float totalValue,
int lastUpdatedByUserId
)
{
if (!_activitySource.HasListeners())
{
return;
}
if (activity != null)
{
activity.SetTag("total_value", totalValue);
activity.SetTag("last_updated_by_user_id", lastUpdatedByUserId);
}
}
}
```
Key points:
- A `HasListeners()` fast-path avoids work when no listener is subscribed.
- Tag/baggage keys use snake_case (`entity_id`, `service_url`) — the OpenTelemetry naming convention.
- Multi-target methods orchestrate the per-target private methods.
## Logging (`EntityStoreTelemetryCore.Logging.g.cs`)
```csharp
internal sealed partial class EntityStoreTelemetryCore : IEntityStoreTelemetry
{
private readonly global::Microsoft.Extensions.Logging.ILogger _logger;
private static readonly global::System.Action _gettingEntityFromStoreAction = global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, "GettingEntityFromStore"), "GettingEntityFromStore: EntityId = {EntityId}, ServiceUrl = {ServiceUrl}");
private static readonly global::System.Action _entityNotFoundAction = global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Warning, new global::Microsoft.Extensions.Logging.EventId(0, "EntityNotFound"), "EntityNotFound: EntityId = {EntityId}");
public EntityStoreTelemetryCore(
global::Microsoft.Extensions.Logging.ILogger logger,
global::System.Diagnostics.Metrics.IMeterFactory meterFactory
)
{
_logger = logger;
InitializeMeters(meterFactory);
}
private void GettingEntityFromStore_Logging(int entityId, string serviceUrl)
{
if (!_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
{
return;
}
_gettingEntityFromStoreAction(_logger, entityId, serviceUrl, null);
}
public void EntityNotFound(int entityId)
{
if (!_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Warning))
{
return;
}
_entityNotFoundAction(_logger, entityId, null);
}
}
```
These methods fit the v1 limits (≤ 6 non-exception parameters, no `[ExpandEnumerable]`/`[LogProperties]`), so `LoggerGenerationMode.Auto` selected v1 `LoggerMessage.Define` generation with an `IsEnabled` fast-path. See [Logging](../logging/).
## Metrics (`EntityStoreTelemetryCore.Metric.g.cs`)
```csharp
internal sealed partial class EntityStoreTelemetryCore : IEntityStoreTelemetry
{
private global::System.Diagnostics.Metrics.Meter _meter = default!;
private global::System.Diagnostics.Metrics.Counter _gettingEntityFromStoreInstrument = default!;
private global::System.Diagnostics.Metrics.Histogram _recordEntitySizeInstrument = default!;
public void InitializeMeters(global::System.Diagnostics.Metrics.IMeterFactory meterFactory)
{
if (_meter != null)
{
throw new global::System.Exception("The meters have already been initialized.");
}
global::System.Collections.Generic.Dictionary meterTags = new global::System.Collections.Generic.Dictionary();
PopulateMeterTags(meterTags);
_meter = meterFactory.Create(new global::System.Diagnostics.Metrics.MeterOptions("SampleApp.APIService") {
Version = null,
Tags = meterTags
});
_gettingEntityFromStoreInstrument = _meter.CreateCounter(name: "entity_store.getting_entity_from_store", unit: null, description: null, tags: gettingEntityFromStoreTags);
_recordEntitySizeInstrument = _meter.CreateHistogram(name: "entity_store.record_entity_size", unit: null, description: null, tags: recordEntitySizeTags);
}
partial void PopulateMeterTags(global::System.Collections.Generic.Dictionary meterTags);
partial void PopulateGettingEntityFromStoreTags(global::System.Collections.Generic.Dictionary instrumentTags);
partial void PopulateRecordEntitySizeTags(global::System.Collections.Generic.Dictionary instrumentTags);
private void GettingEntityFromStore_Metrics(int entityId, string serviceUrl)
{
_gettingEntityFromStoreInstrument.Add(1, new global::System.Collections.Generic.KeyValuePair("entity_id", entityId), new global::System.Collections.Generic.KeyValuePair("service_url", serviceUrl));
}
public void RecordEntitySize(int sizeInBytes)
{
_recordEntitySizeInstrument.Record(sizeInBytes);
}
}
```
Key points:
- The meter name defaults to the **assembly name** (`SampleApp.APIService`).
- `[AutoCounter]` emits `Add(1, ...)`.
- `PopulateMeterTags`/`Populate{Method}Tags` partial methods let you add tags at initialisation.
## DI registration (`EntityStoreTelemetryCoreDIExtension.DependencyInjection.g.cs`)
```csharp
namespace Microsoft.Extensions.DependencyInjection
{
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class EntityStoreTelemetryCoreDIExtension
{
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddEntityStoreTelemetry(
this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
)
{
return services.AddSingleton();
}
}
}
```
## Telemetry names (`SampleApp.APIService.TelemetryNames.g.cs`)
```csharp
namespace SampleApp.APIService
{
public static partial class TelemetryNames
{
public static readonly string[] MeterNames = new string[] { "SampleApp.APIService" };
public static readonly string[] ActivitySourceNames = new string[] { "sample-weather-app-api" };
}
}
```
The assembly-level `[assembly: ActivitySourceGeneration("sample-weather-app-api")]` supplies the ActivitySource name; the meter name defaults to the assembly name.
## See also
- [Sample Application](../sample-application/) — the full source
- [Generation](../generation/) — controlling class names, DI, and names
- [Diagnostics](../diagnostics/) — analyzer rules
---
# Diagnostics
The package ships a single Roslyn analyzer, `TelemetryDiagnosticAnalyzer`, which validates telemetry interfaces and raises diagnostics with the `TSG` prefix. All diagnostics are grouped by category.
:::note
Each rule is also tracked for release purposes in `AnalyzerReleases.Shipped.md` and
`AnalyzerReleases.Unshipped.md`, which carry the rule ID, category, and severity and are validated by
the Roslyn release-tracking analyzers on every build.
:::
- **General** — `TSG1xxx`
- **Logging** — `TSG2xxx`
- **Activities** — `TSG3xxx`
- **Metrics** — `TSG4xxx`
## General diagnostics (TSG1xxx)
| ID | Severity | Description |
| --- | --- | --- |
| `TSG1000` | Error | Fatal execution error occurred (`Failed to execute the generation stage: {0}`). Raised by the generator itself when an unexpected exception escapes the pipeline. |
| `TSG1001` | Error | Inferring generation targets is not supported when using multi-target generation. A method on a multi-target interface has no explicit generation attribute. |
| `TSG1002` | Error | Multiple attributes from the same target family are not supported. Only one Activity, Logging, or Metrics attribute is allowed per method. |
| `TSG1003` | Error | Duplicate method names are not supported. Two or more methods on the interface share the same name. |
| `TSG1004` | Error | Generic interfaces are not supported. |
| `TSG1005` | Error | Generic methods are not supported. |
| `TSG1006` | Warning | `ExcludeTargets` references a target not present on this method. |
| `TSG1007` | Warning | `ExcludeTargets` results in an empty or invalid parameter set for a target. |
| `TSG1008` | Warning | Activity parameter has no Activity target. A parameter of type `Activity` is present on a method with no `[Activity]`/`[Event]`/`[Context]` attribute. |
| `TSG1010` | Error | Method target not registered on interface. A method carries an attribute for a target family that is not registered on the interface. |
| `TSG1011` | Error | Unsupported target framework. The compilation targets neither .NET 8+ nor .NET Framework 4.8+; define `PURVIEW_TELEMETRY_NON_NULLABLE` to opt out. |
## Logging diagnostics (TSG2xxx)
| ID | Severity | Description |
| --- | --- | --- |
| `TSG2000` | Error | Too many exception parameters. A non-scoped log method has more than one `Exception`-typed parameter. |
| `TSG2001` | Error | More than 6 parameters. A log method in v1 generation mode has more than 6 non-exception parameters. |
| `TSG2002` | Info | Inferring error log level. In v1 generation, a single `Exception` parameter is present with no explicit level, so `Error` is inferred. |
| `TSG2003` | Warning | Could not find a reference to `Microsoft.Extensions.Logging.ILogger`, skipping log generation. |
| `TSG2004` | Error | Cannot mix ordinal and named property placeholders in a message template. |
| `TSG2005` | Error | Ordinal values exceed parameter count. The maximum ordinal placeholder value exceeds the number of provided parameters. |
| `TSG2006` | Error | Using `[LogProperties]` and `[ExpandEnumerable]` on the same parameter is not supported. |
| `TSG2007` | Warning | A scoped log shouldn't have a `LogLevel`; it will be ignored. |
| `TSG2008` | Warning | Unbounded enumeration possible. `[ExpandEnumerable]` has a `MaximumValueCount` greater than the recommended default of 5. |
| `TSG2021` | Error | Log method must return void or IDisposable. (Returning `Activity` is allowed when the method is also an Activity method.) |
## Activities diagnostics (TSG3xxx)
| ID | Severity | Description |
| --- | --- | --- |
| `TSG3000` | Warning | Baggage parameter types only accept strings (`ToString()` will be called). |
| `TSG3001` | Warning | No activity source specified. Generation defaults to `purview` when no name is available anywhere. |
| `TSG3002` | Error | Invalid return type. An Activity/Event method returns something other than `void` or `System.Diagnostics.Activity`. |
| `TSG3003` | Error | Duplicate reserved parameters defined. More than one parameter maps to the same reserved destination. |
| `TSG3004` | Error | Activity parameter is not valid. An `Activity`-destination parameter appears on an Activity method (only valid on `[Event]` methods). |
| `TSG3005` | Error | Timestamp parameter is not valid. A `timestamp` parameter appears on a method that is not an `[Event]` method. |
| `TSG3006` | Error | Start time parameter is not valid on Create activity or Event method. |
| `TSG3007` | Error | Parent context or Parent Id parameter is not valid on event. |
| `TSG3008` | Error | Activity links parameters are not valid on events or context methods. |
| `TSG3009` | Error | Activity tags parameter is not valid on context methods. |
| `TSG3010` | Error | Escaped parameters must be a boolean. |
| `TSG3011` | Error | Escaped parameters are only valid on Events, not Activity or Context methods. |
| `TSG3012` | Info | There are no Activity methods defined, assumed use of `Activity.Current`. |
| `TSG3013` | Warning | Should return the created Activity. An Activity method does not return the created `Activity`. |
| `TSG3014` | Warning | Should accept an Activity to apply the Event/Tags/Baggage to. An Event/Context method has no `Activity` parameter. Opt-in via `ActivitySourceGeneration.GenerateDiagnosticsForMissingActivity` (default `true`). |
| `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 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)
| ID | Severity | Description |
| --- | --- | --- |
| `TSG4000` | Error | No instrument defined. A method on a Metrics interface has no instrument attribute and is not excluded. |
| `TSG4001` | Error | Must return void or bool. A metrics-owned method returns something other than `void` or `bool`. |
| `TSG4002` | Error | Auto increment counter and measurement defined. An auto-increment instrument also has a measurement parameter. |
| `TSG4003` | Error | Multiple measurement values defined. |
| `TSG4004` | Error | No measurement value defined. A non-auto-increment instrument method has no measurement parameter. |
| `TSG4005` | Error | Observable instrument requires `Func`. |
| `TSG4006` | Error | Invalid measurement type. Not one of `byte`, `short`, `int`, `long`, `double`, `float`, `decimal`, `Measurement`, or `IEnumerable>`. |
| `TSG4007` | Error | Observable metrics cannot return bool. |
| `TSG4008` | Error | AutoCounter must return void. |
| `TSG4009` | Warning | Instrument name matches the instrument type name. Use a name that describes what is measured. |
## Common resolutions
### TSG1001 — inference disabled on multi-target interfaces
A method on a multi-target interface (`[ActivitySource]` + `[Logger]` + `[Meter]`) has no explicit attribute. Add the attributes for each target the method should emit, or mark it `[Exclude]`.
```csharp
[ActivitySource("MyApp")]
[Logger]
[Meter]
interface IMyTelemetry
{
[Info] // ✅ explicit target
void ProcessItem(int id);
}
```
### TSG1003 — duplicate method names
Two or more methods share the same name, which is used to generate members on the implementation class. Rename the methods.
### TSG2008 — unbounded enumeration
`[ExpandEnumerable]` is configured with a `MaximumValueCount` greater than 5. Reduce it to the recommended default (5) unless you have tested the performance impact.
### TSG3013 / TSG3014 — missing Activity
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.
### TSG4002 — auto-increment counter with a measurement
`[AutoCounter]` (or `[Counter(AutoIncrement = true)]`) must not declare a measurement parameter — the measurement is fixed to 1.
## See also
- [Getting Started](../)
- [Activities](../activities/), [Logging](../logging/), [Metrics](../metrics/)
- [Multi-Targeting](../multi-targeting/)
---
# Performance
Full cross-runtime benchmark results for the Purview Telemetry Source Generator, generated by [BenchmarkDotNet](https://benchmarkdotnet.org/) from the [`benchmarks/`](https://github.com/purview-dev/telemetry-sourcegenerator/tree/main/benchmarks) project.
## Reproducing
```bash
dotnet run --project benchmarks/Purview.Telemetry.Benchmarks/Purview.Telemetry.Benchmarks.csproj \
--configuration Release --framework net10.0
```
Results are written to `BenchmarkDotNet.Artifacts/results/` as `*-report-github.md`, `*.csv`, and `*.html`.
:::caution
BenchmarkDotNet numbers are only meaningful from **Release** builds. Run the suite fresh (`just benchmark-docs`) before relying on the tables below for a specific machine/SDK combination.
:::
## Environment
```
BenchmarkDotNet v0.15.8, Windows 11 (10.0.28020.2991)
13th Gen Intel Core i9-13900KF 3.00GHz, 1 CPU, 32 logical and 24 physical cores
.NET SDK 10.0.401
.NET 10.0 : .NET 10.0.12 (10.0.12, 10.0.1226.42308), X64 RyuJIT x86-64-v3
.NET 8.0 : .NET 8.0.31 (8.0.31, 8.0.3126.42015), X64 RyuJIT x86-64-v3
.NET 9.0 : .NET 9.0.20 (9.0.20, 9.0.2026.41315), X64 RyuJIT x86-64-v3
```
> **Note:** the .NET Framework 4.7/4.8 jobs were executed but produced no results in this run and are excluded from the tables below.
Generated hot-path methods carry `[MethodImpl(MethodImplOptions.AggressiveInlining)]`, which lets the JIT inline the small generated telemetry methods into callers.
## Activities
**Source:** `ActivityBenchmarks`
Compares the source-generator-produced `ActivityOnlyTelemetryCore` against a hand-written `ManualActivityTelemetry` under two conditions: no listener registered (fast-path) and a full-sampling `ActivityListener` active (production path).
| Method | Runtime | HasListener | Mean | Ratio | Allocated |
| --- | --- | --- | --- | --- | --- |
| Manual: start + complete | .NET 10.0 | False | 0.53 ns | 1.00 | - |
| Generated: start + complete | .NET 10.0 | False | 0.58 ns | 1.11 | - |
| Manual: start + fail | .NET 10.0 | False | 0.75 ns | 1.43 | - |
| Generated: start + fail | .NET 10.0 | False | 0.55 ns | 1.04 | - |
| Manual: start + complete | .NET 8.0 | False | 0.78 ns | 1.00 | - |
| Generated: start + complete | .NET 8.0 | False | 0.88 ns | 1.12 | - |
| Manual: start + fail | .NET 8.0 | False | 0.93 ns | 1.19 | - |
| Generated: start + fail | .NET 8.0 | False | 0.76 ns | 0.97 | - |
| Manual: start + complete | .NET 9.0 | False | 0.55 ns | 1.00 | - |
| Generated: start + complete | .NET 9.0 | False | 0.77 ns | 1.40 | - |
| Manual: start + fail | .NET 9.0 | False | 0.35 ns | 0.63 | - |
| Generated: start + fail | .NET 9.0 | False | 0.59 ns | 1.07 | - |
| **Manual: start + complete** | **.NET 10.0** | **True** | **220.37 ns** | **1.00** | **1008 B** |
| Generated: start + complete | .NET 10.0 | True | 216.69 ns | 0.98 | 1008 B |
| Manual: start + fail | .NET 10.0 | True | 207.55 ns | 0.94 | 920 B |
| Generated: start + fail | .NET 10.0 | True | 203.12 ns | 0.92 | 920 B |
| Manual: start + complete | .NET 8.0 | True | 259.23 ns | 1.00 | 1008 B |
| Generated: start + complete | .NET 8.0 | True | 262.84 ns | 1.01 | 1008 B |
| Manual: start + fail | .NET 8.0 | True | 229.10 ns | 0.88 | 920 B |
| Generated: start + fail | .NET 8.0 | True | 241.07 ns | 0.93 | 920 B |
| Manual: start + complete | .NET 9.0 | True | 226.38 ns | 1.00 | 1008 B |
| Generated: start + complete | .NET 9.0 | True | 229.96 ns | 1.02 | 1008 B |
| Manual: start + fail | .NET 9.0 | True | 219.02 ns | 0.97 | 920 B |
| Generated: start + fail | .NET 9.0 | True | 211.70 ns | 0.94 | 920 B |
**Interpretation:** Generated activities match hand-written code within ~2% and allocate identically across all tested runtimes. When a listener is active the absolute times are dominated by the framework's Activity creation, not the generated code.
## Logging
**Source:** `LoggerBenchmarks`
Compares three logging approaches: hand-written `LoggerMessage.Define` (gold-standard manual), generated v1 (`LoggerMessage.Define` pattern), and generated v2 (state-based `ThreadLocalState` pattern).
| Method | Runtime | HasLogging | Mean | Ratio | Allocated |
| --- | --- | --- | --- | --- | --- |
| **Manual: single Info call** | **.NET 10.0** | **True** | **6.12 ns** | **1.00** | **-** |
| Generated v1 — single Info call | .NET 10.0 | True | 4.43 ns | 0.72 | - |
| Generated v2 — single Info call | .NET 10.0 | True | 3.80 ns | 0.62 | - |
| Manual: full lifecycle (4 calls) | .NET 10.0 | True | 18.88 ns | 3.08 | - |
| Generated v1 — full lifecycle | .NET 10.0 | True | 19.58 ns | 3.20 | - |
| Generated v2 — full lifecycle | .NET 10.0 | True | 15.01 ns | 2.45 | - |
| Manual: single Info call | .NET 8.0 | True | 7.55 ns | 1.00 | - |
| Generated v1 — single Info call | .NET 8.0 | True | 7.51 ns | 0.99 | - |
| Generated v2 — single Info call | .NET 8.0 | True | 5.10 ns | 0.68 | - |
| Manual: full lifecycle | .NET 8.0 | True | 29.98 ns | 3.97 | - |
| Generated v1 — full lifecycle | .NET 8.0 | True | 30.72 ns | 4.07 | - |
| Generated v2 — full lifecycle | .NET 8.0 | True | 22.11 ns | 2.93 | - |
| Manual: single Info call | .NET 9.0 | True | 6.88 ns | 1.00 | - |
| Generated v1 — single Info call | .NET 9.0 | True | 6.50 ns | 0.95 | - |
| Generated v2 — single Info call | .NET 9.0 | True | 5.06 ns | 0.74 | - |
| Manual: full lifecycle | .NET 9.0 | True | 25.55 ns | 3.72 | - |
| Generated v1 — full lifecycle | .NET 9.0 | True | 24.88 ns | 3.62 | - |
| Generated v2 — full lifecycle | .NET 9.0 | True | 22.79 ns | 3.31 | - |
**Interpretation:** Generated v1 and v2 both allocate **zero bytes** per call across all runtimes. On .NET 10.0 the generated variants are **faster** than the hand-written `LoggerMessage.Define` baseline for single calls (v1 0.72x, v2 0.62x) thanks to `[MethodImpl(AggressiveInlining)]`; v2 (state-based) is the fastest path on every runtime. The full-lifecycle cost is dominated by the four `IsEnabled` checks and message-formatting paths that all three implementations share.
## Multi-target
**Source:** `MultiTargetVsSingleTargetBenchmarks`
Measures the overhead of emitting Activity + Logging + Metrics from a single method call (multi-target) vs. Activity-only (single-target), comparing generated and manual code.
| Method | Runtime | HasListener | Mean | Ratio | Allocated | Alloc Ratio |
| --- | --- | --- | --- | --- | --- | --- |
| **Single-target (generated): start + complete** | **.NET 10.0** | **True** | **216.15 ns** | **1.00** | **1008 B** | **1.00** |
| Multi-target (generated): start + complete | .NET 10.0 | True | 230.41 ns | 1.07 | 1032 B | 1.02 |
| Multi-target (manual): start + complete | .NET 10.0 | True | 260.18 ns | 1.20 | 1032 B | 1.02 |
| Multi-target (generated): start + complete + record latency | .NET 10.0 | True | 229.65 ns | 1.06 | 1032 B | 1.02 |
| Multi-target (manual): start + complete + record latency | .NET 10.0 | True | 234.58 ns | 1.09 | 1032 B | 1.02 |
| Single-target (generated): start + complete | .NET 8.0 | True | 258.36 ns | 1.00 | 1008 B | 1.00 |
| Multi-target (generated): start + complete | .NET 8.0 | True | 260.21 ns | 1.01 | 1032 B | 1.02 |
| Multi-target (manual): start + complete | .NET 8.0 | True | 267.22 ns | 1.03 | 1032 B | 1.02 |
| Single-target (generated): start + complete | .NET 9.0 | True | 220.24 ns | 1.00 | 1008 B | 1.00 |
| Multi-target (generated): start + complete | .NET 9.0 | True | 249.15 ns | 1.13 | 1032 B | 1.02 |
| Multi-target (manual): start + complete | .NET 9.0 | True | 257.27 ns | 1.17 | 1032 B | 1.02 |
**Interpretation:** When an Activity listener is active (production path), multi-target generation adds ~7% overhead over single-target Activity-only on .NET 10.0 — the real cost of the extra log call and metric increment, not generated-code overhead. The generated multi-target code is faster than the hand-written multi-target baseline (~1.07x vs ~1.20x of single-target). See the [Generated Output](../generated-output/) page for what the multi-target implementation looks like.
## Logger multi-target
**Source:** `LoggerMultiTargetBenchmarks`
Compares single-target logging-only vs. multi-target (Activity + Logging + Metrics) generated code with a hand-written multi-target baseline.
| Method | Runtime | HasListener | Mean | Ratio | Allocated |
| --- | --- | --- | --- | --- | --- |
| **Multi-target (manual): start + complete** | **.NET 10.0** | **True** | **264.31 ns** | **1.00** | **1032 B** |
| Multi-target (generated v1): start + complete | .NET 10.0 | True | 236.44 ns | 0.89 | 1032 B |
| Multi-target (generated v2): start + complete | .NET 10.0 | True | 227.94 ns | 0.86 | 1032 B |
| Multi-target (manual): full lifecycle | .NET 10.0 | True | 243.84 ns | 0.92 | 1032 B |
| Multi-target (generated v1): full lifecycle | .NET 10.0 | True | 249.32 ns | 0.94 | 1032 B |
| Multi-target (generated v2): full lifecycle | .NET 10.0 | True | 216.86 ns | 0.82 | 1032 B |
| Single-target (generated v1): full lifecycle | .NET 10.0 | True | 18.21 ns | 0.07 | - |
| Single-target (generated v2): full lifecycle | .NET 10.0 | True | 15.69 ns | 0.06 | - |
**Interpretation:** Generated and manual multi-target implementations are within ~6% of each other on .NET 10.0, allocating identically (1032 B with a listener active). Logging-only accounts for a small fraction (~16-18 ns) of the multi-target cost (~230 ns); Activity creation dominates when a listener is active.
## Metrics
**Source:** `MetricsBenchmarks` and `TagListBenchmarks`
All instruments are **0 allocations** on every runtime.
| Scenario | Generated (.NET 10.0) | Generated (.NET 8.0) | Generated (.NET 9.0) |
| --- | --- | --- | --- |
| auto-counter (0 tags) | 0.40 ns | 0.55 ns | 0.21 ns |
| auto-counter (1 tag) | 0.35 ns | 0.56 ns | 0.30 ns |
| up-down counter | 0.37 ns | 0.39 ns | 0.20 ns |
| histogram (0 tags) | 0.42 ns | 0.35 ns | 0.18 ns |
| histogram (1 tag) | 0.34 ns | 0.36 ns | 0.37 ns |
The source generator uses a tag-count optimization: methods with fewer than 4 tags pass inline `KeyValuePair` parameters (no heap allocation), while methods with 4 or more tags use a stack-allocated [`TagList`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.taglist) struct.
| Scenario | .NET 10.0 | .NET 8.0 | .NET 9.0 |
| --- | --- | --- | --- |
| 0 tags: histogram record | 0.38 ns | 0.56 ns | 0.19 ns |
| 1 tag: auto-counter add | 0.41 ns | 0.35 ns | 0.19 ns |
| 3 tags: histogram record | 0.84 ns | 0.74 ns | 0.55 ns |
| 4 tags (TagList): auto-counter add | 7.91 ns | 7.79 ns | 3.62 ns |
| 5 tags (TagList): auto-counter add | 5.46 ns | 8.95 ns | 9.37 ns |
| 6 tags (TagList): histogram record | 6.37 ns | 4.32 ns | 3.99 ns |
The TagList path costs 14–21× more CPU than the inline path on .NET 10.0, but both remain in the single-digit-nanosecond range with zero allocations.
## Observable instruments
`ObservableCounter`, `ObservableGauge`, and `ObservableUpDownCounter` are **not benchmarked** because they have no per-operation hot path to compare. They register a callback once at construction time (via `meter.CreateObservable*`) and are polled by the metrics collection pipeline — the generator produces the identical `CreateObservable*` call you would write by hand, so there is no wrapper layer on the measurement path.
## Raw results
Full CSV and HTML benchmark artifacts are available in [`BenchmarkDotNet.Artifacts/results/`](https://github.com/purview-dev/telemetry-sourcegenerator/tree/main/BenchmarkDotNet.Artifacts/results).
## See also
- [README.md § Performance](https://github.com/purview-dev/telemetry-sourcegenerator#performance) — condensed summary of the key .NET 10.0 numbers
---
# Frequently Asked Questions (FAQ)
Common questions and answers about the Purview Telemetry Source Generator.
## General questions
### What is the Purview Telemetry Source Generator?
A .NET incremental source generator that produces implementation code for Activities (distributed tracing), Logging (structured logs), and Metrics from interface definitions you create. Instead of writing boilerplate telemetry code, you define methods on an interface and the generator creates the implementation, DI registration helpers, and multi-target generation.
### Why use a source generator for telemetry?
- **Zero boilerplate** — no manual implementation code to write or maintain
- **Type safety** — compile-time validation of telemetry code
- **Testability** — easy to mock interfaces in unit tests
- **Consistency** — all telemetry follows the same patterns
- **DI-ready** — automatic dependency injection registration
- **Performance** — generated code is optimized
- **Maintainability** — changes to interfaces propagate automatically
### What .NET versions are supported?
- .NET 8 or higher
- .NET Framework 4.8 or higher
### Is this compatible with OpenTelemetry?
Yes. v4+ defaults to OpenTelemetry semantic conventions for generated names. The generated Activities, Logs, and Metrics work with OpenTelemetry exporters and collectors.
## Installation & setup
### How do I install the package?
Add the NuGet package to your `.csproj`:
```xml
all
analyzers
```
See [Getting Started](../) for more details.
### Why do I need `PrivateAssets` and `IncludeAssets`?
- `PrivateAssets="all"` — prevents the package from being exposed to consuming projects
- `IncludeAssets="analyzers"` — includes only what is needed for source generation
### Do I need any other packages?
Depends on what you generate:
- **Activities** — no additional packages (uses `System.Diagnostics.DiagnosticSource`)
- **Logging** — `Microsoft.Extensions.Logging.Abstractions`; optionally `Microsoft.Extensions.Telemetry.Abstractions` for v2 features
- **Metrics** — no additional packages (uses `System.Diagnostics.Metrics`)
- **DI** — `Microsoft.Extensions.DependencyInjection.Abstractions` (usually already present)
### How do I view the generated code?
```xml
true
```
Generated files appear in `obj/Debug|Release//generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/`.
## Activities
### When should I use Activities vs Logging vs Metrics?
- **Activities** — distributed operations across services, end-to-end latency, trace spans
- **Logging** — events, state changes, debugging with structured data, error details
- **Metrics** — counting occurrences, distributions, gauges for dashboards
**Pro tip:** use [Multi-Targeting](../multi-targeting/) to combine all three.
### Why does my Activity method return `Activity?` instead of `Activity`?
Activities can be `null` when no listeners are subscribed to the ActivitySource or sampling determines the activity should not be recorded. Always return `Activity?` and guard against `null`.
### Should I use `Activity.Current` or pass Activity parameters?
**Always pass Activity parameters explicitly.** `Activity.Current` may not be the activity you expect, especially in async code or with nested activities.
```csharp
// Good
[Event]
void OrderProcessed(Activity? activity, int orderId);
// Avoid
[Event]
void OrderProcessed(int orderId); // uses Activity.Current implicitly
```
### What's the difference between `[Tag]` and `[Baggage]`?
- **`[Tag]`** — added as tags to the Activity or ActivityEvent; recorded with the activity but not automatically propagated to child activities
- **`[Baggage]`** — added as baggage; automatically propagated to child activities and across service boundaries
Use baggage sparingly as it increases overhead; use tags for most properties.
## Logging
### What's the difference between Generation v1 and v2?
See [Logging](../logging/) for the full comparison. In brief:
- **Generation v2** — state-based output resembling the built-in `[LoggerMessage]` generator; supports dynamic message templates, `[ExpandEnumerable]`, and `[LogProperties]`; requires `Microsoft.Extensions.Telemetry.Abstractions`.
- **Generation v1** — `LoggerMessage.Define` high-performance mode; limited to 6 non-exception parameters plus one `Exception`; no `[ExpandEnumerable]`/`[LogProperties]`.
The default `LoggerGenerationMode.Auto` selects the best mode **per method**.
### How do I create scoped logs?
Return `IDisposable?` from a log method:
```csharp
[Logger]
interface IOrderTelemetry
{
[Info]
IDisposable? ProcessingOrder(Guid orderId);
}
using (telemetry.ProcessingOrder(orderId))
{
// Duration logged automatically when disposed
}
```
### Can I customize log message templates?
Yes — `[Log].MessageTemplate` sets the template. Placeholders map to method parameters:
```csharp
[Info("Order {OrderId} placed for {CustomerName}")]
void OrderPlaced(int orderId, string customerName);
```
If no template is specified, one is generated from the method name and parameters.
### How do I disable logging generation?
```xml
EXCLUDE_PURVIEW_TELEMETRY_LOGGING
```
Useful when `Microsoft.Extensions.Logging` types are not available.
## Metrics
### What's the difference between Counter and AutoCounter?
- **`[Counter]`** — you specify the measurement value:
```csharp
[Counter]
void ItemsProcessed([InstrumentMeasurement]int count, [Tag]string type);
```
- **`[AutoCounter]`** — automatically increments by 1 per call:
```csharp
[AutoCounter]
void ItemProcessed([Tag]string type);
```
### When should I use Histogram vs Counter?
- **Counter** — discrete events that only go up (requests, errors, completions)
- **Histogram** — distributions where percentiles matter (latency, size, duration)
### What are observable metrics and when should I use them?
Observable instruments (`[ObservableCounter]`, `[ObservableGauge]`, `[ObservableUpDownCounter]`) are pull-based — the collector calls your `Func` to get the current value. Use them for values that already exist (memory usage, queue depth, cache size) or expensive calculations you don't want to run on every update.
```csharp
[ObservableGauge]
void QueueDepth(Func measurement);
telemetry.QueueDepth(() => _queue.Count);
```
### When do metric names include the meter name?
Instrument names are generated with the meter name as a lowercase dot-separated prefix when using `NamingConvention.OpenTelemetry` combined with `MeterNameGenerationType.OpenTelemetry`:
```csharp
[Meter("MyApp.Orders")]
interface IOrderMetrics
{
[Counter]
void OrderProcessed([InstrumentMeasurement]int count);
}
// Generated name: "myapp.orders.order.processed"
// ^^^^^^^^^^^^^^^ meter prefix (lowercase)
// ^^^^^^^^^^^^^^^^ instrument name
```
The default `MeterNameGenerationType.DotNet` does not add the meter-name prefix. See [Metrics](../metrics/#meter-naming).
## Multi-targeting
### Can I generate Activities, Logging, AND Metrics from one interface?
Yes — this is called [Multi-Targeting](../multi-targeting/):
```csharp
[ActivitySource("MyApp")]
[Logger]
[Meter("MyApp")]
interface IMyTelemetry
{
[Activity]
[Info]
[AutoCounter]
Activity? ProcessingRequest([Baggage]string requestId);
}
```
### What's the difference between single-target and multi-target interfaces?
- **Single-target** — one class-level attribute; method-level inference is supported.
- **Multi-target** — multiple class-level attributes; every method must declare its targets explicitly (no inference).
## Configuration & generation
### How do I control the generated class name?
```csharp
[TelemetryGeneration(ClassName = "MyCustomTelemetry")]
[ActivitySource("MyApp")]
interface IMyTelemetry { }
```
### How do I disable dependency injection generation?
```csharp
[TelemetryGeneration(GenerateDependencyExtension = false)]
[ActivitySource("MyApp")]
interface IMyTelemetry { }
```
### How do I exclude a method from generation?
Use `[Exclude]` and implement it manually in a partial class:
```csharp
[ActivitySource("MyApp")]
interface IMyTelemetry
{
[Activity]
Activity? NormalMethod(int id);
[Exclude]
void CustomMethod(int id);
}
partial class MyTelemetryCore
{
public void CustomMethod(int id)
{
// your custom implementation
}
}
```
## Migration
### What are the breaking changes in v4 and v5?
Two major v4 changes — **namespace consolidation** into `Purview.Telemetry` and **OpenTelemetry naming** — plus v5 changes including the meter default-name resolution. See [Breaking Changes](../breaking-changes/).
### How do I keep v3 naming?
```csharp
[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]
```
See [OpenTelemetry-Aligned Naming](../breaking-changes/#opentelemetry-aligned-naming).
### Can I use v4 alongside v3?
Not in the same project. In a multi-project solution different projects can use different versions, though that is not recommended.
## Troubleshooting
### The generator isn't producing any code
1. The interface has a class-level attribute (`[ActivitySource]`, `[Logger]`, or `[Meter]`)
2. Methods have appropriate method-level attributes
3. The interface is not generic (generics are not supported)
4. Rebuild the project to trigger generation
5. Check the Error List for `TSG` diagnostics
### I'm getting TSG diagnostic errors
See [Diagnostics](../diagnostics/) for the complete list of codes and meanings.
### Generated code doesn't compile
Common causes: missing packages (`Microsoft.Extensions.Logging.Abstractions`, `Microsoft.Extensions.DependencyInjection.Abstractions`, `Microsoft.Extensions.Telemetry.Abstractions` for v2), wrong parameter types, incorrect attribute usage, or generic interfaces/methods.
### Can I use async methods?
The generated methods are synchronous, but your calling code can be async — telemetry operations are lightweight and non-blocking.
## Performance
### Does using a source generator impact performance?
No runtime impact — the code is generated at compile time. The generated code is as fast as hand-written telemetry with minimal allocations. See [Performance](../performance/).
## Advanced topics
### Can I customize the generated code?
Not directly, but you can use `[Exclude]` with partial-class implementations, control naming via `[TelemetryGeneration]`, and configure defaults through the assembly-level generation attributes.
### Does this work with .NET Native AOT?
The source generator itself works with AOT. Generated code uses no reflection and is trimmable; validate your full scenario with AOT analysis enabled.
### Can I use this in a library?
Yes. Generated implementation and DI classes are internal by default, so they don't leak to consumers. Make the DI class public with `[TelemetryGeneration(DependencyInjectionClassIsPublic = true)]` if you need public registration helpers.
### How do I test code that uses telemetry interfaces?
Just mock the interface — see [Testing](../testing/):
```csharp
var mockTelemetry = Substitute.For();
var service = new OrderService(mockTelemetry);
```
## Getting help
- **Documentation** — [purview.dev](https://purview.dev/docs/telemetry-sourcegenerator/)
- **Issues** — [GitHub Issues](https://github.com/purview-dev/telemetry-sourcegenerator/issues)
- **Sample** — [Sample Application](../sample-application/)
When reporting a bug, include the package version, .NET version, a minimal repro, expected vs actual behaviour, and any `TSG` diagnostics.
---
# Breaking Changes
This page documents breaking changes between major versions to help you migrate your code.
## Table of contents
- [v3 to v4](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/docs/wiki#v3-to-v4)
- [Namespace consolidation](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/docs/wiki#namespace-consolidation)
- [OpenTelemetry-aligned naming](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/docs/wiki#opentelemetry-aligned-naming)
- [v4 to v5](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/docs/wiki#v4-to-v5)
- [v1 and v2 to v3](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/docs/wiki#v1-and-v2-to-v3)
## v3 to v4
Version 4 introduced two major breaking changes: namespace consolidation and OpenTelemetry-aligned naming conventions.
### Namespace consolidation
**Impact:** High — requires code changes in all projects using v3
v4 consolidates all attributes into a single namespace to simplify imports.
#### Migration required
**Before (v3):**
```csharp
using Purview.Telemetry.Activities;
using Purview.Telemetry.Logging;
using Purview.Telemetry.Metrics;
```
**After (v4):**
```csharp
using Purview.Telemetry; // single namespace
```
| v3 namespace | v4 namespace | Affected attributes |
| --- | --- | --- |
| `Purview.Telemetry.Activities` | `Purview.Telemetry` | `[ActivitySource]`, `[Activity]`, `[Event]`, `[Context]`, `[Baggage]` |
| `Purview.Telemetry.Logging` | `Purview.Telemetry` | `[Logger]`, `[Log]`, `[Debug]`, `[Info]`, `[Warning]`, `[Error]`, `[Critical]` |
| `Purview.Telemetry.Metrics` | `Purview.Telemetry` | `[Meter]`, `[Counter]`, `[AutoCounter]`, `[Histogram]`, `[Observable*]`, `[UpDownCounter]` |
| `Purview.Telemetry` | `Purview.Telemetry` | `[Tag]`, `[TelemetryGeneration]`, `[Exclude]` |
#### Migration steps
1. Replace `using Purview.Telemetry.Activities;` / `using Purview.Telemetry.Logging;` / `using Purview.Telemetry.Metrics;` with `using Purview.Telemetry;`.
2. Remove duplicate imports.
3. Rebuild and test.
### OpenTelemetry-aligned naming
**Impact:** Medium to High — changes generated telemetry names (may break dashboards/queries)
**Introduced in:** v4.0.0-alpha.5
**Default behaviour:** Enabled
v4 defaults to **OpenTelemetry semantic conventions** for generated telemetry names. This is a breaking change if you rely on specific telemetry names in dashboards, queries, or monitoring tools.
#### What changed
| Telemetry type | v3 behaviour | v4 default (OpenTelemetry) | Example change |
| --- | --- | --- | --- |
| ActivitySource name | Assembly name lowercased | Assembly name casing preserved | `"myapp"` → `"MyApp"` |
| Activity names | Method name lowercased | Method name casing preserved | `"getentity"` → `"GetEntity"` |
| Tag/baggage keys | Lowercased, smashed compounds | snake_case with underscores | `"entityid"` → `"entity_id"` |
| Metric instrument names | Lowercased, smashed | Hierarchical with meter prefix | `"recordcount"` → `"myapp.products.record.count"` |
| Metric tag keys | Lowercased, smashed | snake_case with underscores | `"requestcount"` → `"request_count"` |
#### Examples
**Before (v3):**
```csharp
[ActivitySource("MyApp")]
interface IOrderTelemetry
{
[Activity]
Activity? ProcessingOrder([Baggage]int orderId, [Tag]string customerName);
}
// Generated ActivitySource name: "myapp"
// Generated Activity name: "processingorder"
// Generated tag keys: "orderid", "customername"
```
**After (v4+ OpenTelemetry mode — default):**
```csharp
[ActivitySource("MyApp")]
interface IOrderTelemetry
{
[Activity]
Activity? ProcessingOrder([Baggage]int orderId, [Tag]string customerName);
}
// Generated ActivitySource name: "MyApp"
// Generated Activity name: "ProcessingOrder"
// Generated tag keys: "order_id", "customer_name"
```
#### Migration options
**Option 1 — adopt OpenTelemetry naming (recommended):** upgrade and update dashboards/queries to use snake_case keys and hierarchical metric names.
**Option 2 — revert to v3 legacy naming:**
```csharp
using Purview.Telemetry;
// Revert ALL telemetry to v3 naming (assembly-level)
[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]
// Or set per-interface
[TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]
interface IMyTelemetry { }
```
```csharp
public enum NamingConvention
{
Legacy = 0, // v3 behaviour: lowercase, smashed compounds
OpenTelemetry = 1 // v4+ default: OTel conventions
}
```
**Option 3 — mixed mode:** apply different conventions per interface with `[TelemetryGeneration]`.
:::tip
For high-impact scenarios, consider using `NamingConvention.Legacy` initially, then gradually migrating to OpenTelemetry conventions during a planned maintenance window.
:::
## v4 to v5
**Impact:** Low to Medium — affects the default meter name
### Meter default-name resolution
In v4, when `[Meter(Name = ...)]` was not specified, the meter name defaulted to the interface name without the leading `I` (for example, `ICacheServiceTelemetry` → `CacheServiceTelemetry`).
In v5 the default meter name is resolved in this order:
1. `MeterAttribute.Name` (interface)
2. `MeterGenerationAttribute.MeterName` (assembly)
3. The assembly name
If you rely on the old interface-name meter default, set the name explicitly:
```csharp
[Meter("CacheServiceTelemetry")]
interface ICacheServiceTelemetry { }
```
### `MeterGenerationAttribute` additions
v5 adds `MeterName` and `MeterNameGenerationType` to `[MeterGeneration]`, controlling the default meter name and whether instrument names are prefixed with the meter name. See [Metrics](../metrics/#metergeneration).
### Activity return type should be nullable
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
Previously, the default event name generated for a logging method included a trimmed-down version of the class name combined with the method name.
```csharp
[Logger]
interface IServiceTelemetry
{
void LogAThing(int theThing);
}
```
:::caution
In v1 and v2 the default event name for `LogAThing` was `Service.LogAThing`. As of v3, the default is `LogAThing`.
:::
This is supported by changes to `LogPrefixType`:
| Field | Old behaviour | New behaviour |
| --- | --- | --- |
| `Default` | Generated a prefix based on the generated class name. | Generates no suffix. |
| `NoSuffix` | Generated no suffix. | **Field removed.** |
| `TrimmedClassName` | Previously the default behaviour. | New field; generates a suffix based on the generated class name. |
To return to the previous behaviour, set `LoggerGenerationAttribute.DefaultPrefixType` to `LogPrefixType.TrimmedClassName` (assembly level) or `LoggerAttribute.PrefixType` to `LogPrefixType.TrimmedClassName` (interface level).
---
# Refactorings
The NuGet package ships four Visual Studio code refactorings alongside the source generator. Right-click a class containing hand-written telemetry and choose the relevant conversion — the refactoring creates a generated telemetry interface and rewrites the class to use it.
## Available refactorings
| Refactoring | Converts | Generated interface |
| --- | --- | --- |
| **Convert ILogger to I{ClassName}Logs** | Hand-written `ILogger`/`ILogger` fields, properties, constructor parameters, and `Log*`/`Log` calls | `[Logger]` interface |
| **Convert ActivitySource to I{ClassName}Tracing** | `ActivitySource` fields/properties/parameters and `StartActivity(...)` calls | `[ActivitySource]` interface |
| **Convert Metrics to I{ClassName}Metrics** | `Counter`, `Histogram`, `UpDownCounter` fields/properties and `.Add(...)`/`.Record(...)` calls | `[Meter]` interface |
| **Convert all telemetry to I{ClassName}Telemetry** | Any combination of the above in the same class | Single `[ActivitySource]`+`[Logger]`+`[Meter]` interface |
Each refactoring offers scope options:
- **In this class** — converts the selected class only
- **In this document** — converts all matching classes in the file
- **In this project** — converts all matching classes in the project
- **In this solution** — converts all matching classes in the solution
## Convert ILogger to I{ClassName}Logs
- `LogTrace` → `[Trace]`, `LogDebug` → `[Debug]`, `LogInformation` → `[Info]`, `LogWarning` → `[Warning]`, `LogError` → `[Error]`, `LogCritical` → `[Critical]`; `Log(LogLevel.X, ...)` maps to the matching semantic attribute (unmapped levels fall back to `[Log(LogLevel.X, ...)]`).
- Literal message templates are embedded in the attribute: `[Info("Getting weather for {City}")]`.
- Literal `int` event IDs are embedded: `[Info(42, "...")]`.
- `Exception` parameters become an `exception` parameter of type `System.Exception`.
- Method names derive from the message-template words (PascalCased), deduplicated with numeric suffixes.
- Multiple logger fields/parameters on one constructor are consolidated into a single canonical injection.
The refactoring fires when a class contains at least one `ILogger`/`ILogger` member **and** at least one recognized `Log*`/`Log` call on it.
## Convert ActivitySource to I{ClassName}Tracing
- Each `StartActivity("name")` call becomes an interface method returning `Activity?`, decorated `[Activity]` (for kind `Internal`/unspecified) or `[Activity(ActivityKind.X)]` for explicit kinds.
- Method names derive from the activity-name string, PascalCased (for example, `"get-weather"` → `GetWeather`); duplicates get numeric suffixes.
- `ActivitySource`-typed fields/properties/parameters are rewritten to the interface type, and `StartActivity(...)` invocations become interface method calls.
The refactoring fires when a class has at least one `ActivitySource` member **and** at least one `.StartActivity(...)` call on it.
## Convert Metrics to I{ClassName}Metrics
- `Counter.Add(1)` (literal 1) → `[AutoCounter]` method with no parameters.
- `Counter.Add(value)` → `[Counter] void Method(T value)`.
- `Histogram.Record(value)` → `[Histogram] void Method(T value)`.
- `UpDownCounter.Add(value)` → `[UpDownCounter] void Method(T value)`.
- Additional tag arguments become `string tag1`, `string tag2`, ... parameters.
- Method names derive from the field name with instrument suffixes (`UpDownCounter`, `Histogram`, `Counter`, `Gauge`, `Meter`) stripped, then PascalCased.
- The `Meter` itself is not converted — the constructor's `Meter`/`IMeterFactory` usage and `CreateCounter` registration calls remain.
The refactoring fires when a class has at least one metrics instrument member **and** at least one `.Add(...)`/`.Record(...)` call on it.
:::note
`System.Diagnostics.Metrics` is not available on .NET Framework, so the metrics refactoring is not available for those target frameworks.
:::
## Convert all telemetry to I{ClassName}Telemetry
Composes the per-family conversions into a single interface decorated with `[ActivitySource]`, `[Logger]`, and/or `[Meter]` for only the families actually present (multi-targeting). It fires when a class uses any combination of logger/ActivitySource/metrics members **and** has at least one corresponding call.
## What the refactorings produce
```csharp
using Purview.Telemetry;
[Logger]
public interface IOrderServiceLogs
{
[Info]
void OrderPlaced(int orderId, string customerName);
}
```
The original class is rewritten to use the interface:
```csharp
public class OrderService(IOrderServiceLogs logger)
{
public void PlaceOrder(int orderId, string customerName)
{
logger.OrderPlaced(orderId, customerName);
}
}
```
## Manual fallback
If a refactoring does not cover a call pattern, convert it manually:
1. Identify the telemetry type (Logging, Activity, Metric).
2. Create a new interface with the matching class-level attribute (`[Logger]`, `[ActivitySource]`, `[Meter]`).
3. Add a method for each distinct operation, using the method-level attribute from the mapping below.
4. Replace the hand-written call with the interface method.
5. Register the interface in DI with `services.Add{InterfaceNameWithoutI}()`.
| Hand-written telemetry | Generated attribute |
| --- | --- |
| `ILogger.LogInformation(...)` | `[Info]` |
| `ILogger.LogDebug(...)` | `[Debug]` |
| `ILogger.LogTrace(...)` | `[Trace]` |
| `ILogger.LogWarning(...)` | `[Warning]` |
| `ILogger.LogError(...)` | `[Error]` |
| `ILogger.LogCritical(...)` | `[Critical]` |
| `ActivitySource.StartActivity(...)` | `[Activity]` |
| `activity.AddEvent(...)` | `[Event]` |
| `activity.AddBaggage(...)` / `SetBaggage(...)` | `[Context]` with `[Baggage]` parameter |
| `activity.SetTag(...)` | `[Context]` with `[Tag]` parameter |
| `Counter.Add(...)` | `[Counter]`, or `[AutoCounter]` for `Add(1)` |
| `Histogram.Record(...)` | `[Histogram]` |
| `UpDownCounter.Add(...)` | `[UpDownCounter]` |
## See also
- [Migration from ILogger](../migration-from-ilogger/)
- [Migration from Activities](../migration-from-activities/)
---
# Migration from ILogger
This guide covers converting hand-written `ILogger` usage to generated telemetry interfaces. For the fastest path, use the [IDE refactorings](../refactorings/) — this page documents the manual fallback.
## Before
```csharp
using Microsoft.Extensions.Logging;
public class OrderService(ILogger logger)
{
public void PlaceOrder(int orderId, string customerName)
{
logger.LogInformation("Placing order {OrderId} for {CustomerName}", orderId, customerName);
}
public void CancelOrder(int orderId, Exception ex)
{
logger.LogError(ex, "Order {OrderId} cancelled", orderId);
}
}
```
## After
```csharp
using Purview.Telemetry;
[Logger]
public interface IOrderServiceLogs
{
[Info("Placing order {OrderId} for {CustomerName}")]
void OrderPlaced(int orderId, string customerName);
[Error("Order {OrderId} cancelled")]
void OrderCancelled(Exception ex, int orderId);
}
public class OrderService(IOrderServiceLogs logger)
{
public void PlaceOrder(int orderId, string customerName)
{
logger.OrderPlaced(orderId, customerName);
}
public void CancelOrder(int orderId, Exception ex)
{
logger.OrderCancelled(ex, orderId);
}
}
```
Register the interface in DI:
```csharp
services.AddOrderServiceLogs();
```
## Log-level mapping
| `ILogger` call | Generated attribute |
| --- | --- |
| `LogTrace(...)` | `[Trace]` |
| `LogDebug(...)` | `[Debug]` |
| `LogInformation(...)` | `[Info]` |
| `LogWarning(...)` | `[Warning]` |
| `LogError(...)` | `[Error]` |
| `LogCritical(...)` | `[Critical]` |
| `Log(LogLevel.X, ...)` | `[Log(LogLevel.X, ...)]` |
## Scoped logging
Return `IDisposable?` from the method to generate a scoped log entry:
```csharp
[Logger]
public interface IOrderServiceLogs
{
[Info("Processing order {OrderId}")]
IDisposable? ProcessingOrder(int orderId);
}
```
## `LoggerMessage.Define`
Replace a `LoggerMessage.Define` static field with a generated method:
```csharp
// Before
static readonly Action, int, Exception?> _failed =
LoggerMessage.Define(LogLevel.Error, 0, "Order {OrderId} failed");
_failed(_logger, orderId, ex);
```
```csharp
// After
[Error("Order {OrderId} failed")]
void OrderFailed(int orderId, Exception ex);
logger.OrderFailed(orderId, ex);
```
## Structured data
For collections, use `[ExpandEnumerable]`; for objects, use `[LogProperties]` — see [Logging Generation v2](../logging-generation-v2/).
## Naming
v4+ converts parameter names to snake_case by default (OpenTelemetry convention). To revert to v3 naming, apply `[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]`. See [Naming conventions](../generation/#naming-conventions).
## Checklist
1. Add the `Purview.Telemetry.SourceGenerator` package and `Microsoft.Extensions.Logging.Abstractions`.
2. Create the `[Logger]` interface (or run the [ILogger refactoring](../refactorings/#convert-ilogger-to-iclassnamelogs)).
3. Replace `ILogger` members and calls with the interface.
4. Register with `services.Add{InterfaceNameWithoutI}()`.
5. Rebuild and review the [Diagnostics](../diagnostics/).
## Next steps
- [Refactorings](../refactorings/) — the shipped IDE conversions
- [Logging](../logging/) — v1/v2 generation modes
- [Multi-Targeting](../multi-targeting/) — combine with Activities and Metrics
---
# Migration from Activities
This guide covers converting hand-written `ActivitySource`/`Activity` code to generated telemetry interfaces. For the fastest path, use the [IDE refactorings](../refactorings/) — this page documents the manual fallback.
## Before
```csharp
using System.Diagnostics;
public class OrderService
{
static readonly ActivitySource _source = new("OrderService");
public void PlaceOrder(int orderId)
{
using var activity = _source.StartActivity("PlaceOrder", ActivityKind.Internal);
activity?.AddEvent(new ActivityEvent("Validated"));
}
}
```
## After
```csharp
using System.Diagnostics;
using Purview.Telemetry;
[ActivitySource]
public interface IOrderServiceTracing
{
[Activity]
Activity? PlaceOrder(int orderId);
[Event]
void Validated(Activity? activity);
}
public class OrderService(IOrderServiceTracing tracing)
{
public void PlaceOrder(int orderId)
{
using var activity = tracing.PlaceOrder(orderId);
tracing.Validated(activity);
}
}
```
Register the interface in DI:
```csharp
services.AddOrderServiceTracing();
```
## Common conversions
| Original code | Generated method signature |
| --- | --- |
| `_source.StartActivity("PlaceOrder")` | `[Activity] Activity? PlaceOrder();` |
| `_source.StartActivity("Order", ActivityKind.Internal)` | `[Activity(ActivityKind.Internal)] Activity? Order();` |
| `activity.AddEvent(new ActivityEvent("Loaded"))` | `[Event] void Loaded(Activity? activity);` |
| `activity.SetBaggage("tenant", tenantId)` | `[Context] void SetTenant(Activity? activity, [Baggage] string tenantId);` |
| `activity.SetTag("tenant", tenantId)` | `[Context] void SetTenant(Activity? activity, [Tag] string tenantId);` |
## Return types
- Activity-starting methods should return `Activity?` so callers can dispose and reuse the Activity.
- Event and Context methods should accept the `Activity?` as their first parameter and typically return `void`.
## Tags and baggage
Use `[Tag]` and `[Baggage]` on parameters to control how values are attached — see [Tags, Baggage, and Parameter Attributes](../tags-and-baggage/).
## Naming
The ActivitySource name defaults to the assembly name with casing preserved. Set `[ActivitySource("MyApp")]` to override. v4+ converts tag/baggage keys to snake_case by default; revert with `[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]`.
## Checklist
1. Add the `Purview.Telemetry.SourceGenerator` package.
2. Create the `[ActivitySource]` interface (or run the [ActivitySource refactoring](../refactorings/#convert-activitysource-to-iclassnametracing)).
3. Replace `ActivitySource` fields and `StartActivity`/`AddEvent`/`SetTag`/`SetBaggage` calls with the interface.
4. Register with `services.Add{InterfaceNameWithoutI}()`.
5. Rebuild and review the [Diagnostics](../diagnostics/).
## Next steps
- [Refactorings](../refactorings/) — the shipped IDE conversions
- [Activities](../activities/) — full attribute reference
- [Multi-Targeting](../multi-targeting/) — combine with Logging and Metrics
---
# Testing
Telemetry interfaces are plain interfaces, so they are easy to mock or substitute in unit tests. Because the generated implementation only lives at the interface boundary, standard mocking frameworks (NSubstitute, Moq, TUnitMocks, ...) work directly.
## Inject the interface
```csharp
public class OrderService(IOrderServiceTelemetry telemetry)
{
public void PlaceOrder(int orderId, string customerName)
{
using var activity = telemetry.PlacingOrder(orderId, customerName);
// ...
}
}
```
## Mock it in tests
```csharp
public class OrderServiceTests
{
[Test]
public void PlaceOrder_EmitsTelemetry()
{
var telemetry = Substitute.For();
var service = new OrderService(telemetry);
service.PlaceOrder(42, "Alice", "EMEA");
telemetry.Received().PlacingOrder(42, "Alice", "EMEA");
}
}
```
```csharp
// Moq equivalent
var telemetry = new Mock();
var service = new OrderService(telemetry.Object);
service.PlaceOrder(42, "Alice", "EMEA");
telemetry.Verify(x => x.PlacingOrder(42, "Alice", "EMEA"), Times.Once);
```
## Testing real emission
To assert against real telemetry output, set up a listener or collector:
```csharp
using var listener = new ActivityListener
{
ShouldListenTo = _ => true,
Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData,
};
ActivitySource.AddActivityListener(listener);
var telemetry = Substitute.For();
// Configure the substitute to return a real Activity when the method is called:
telemetry.PlacingOrder(Arg.Any(), Arg.Any())
.Returns(_ => new Activity("placing-order").Start());
```
For full multi-target scenarios, the [sample application](../sample-application/) demonstrates a TUnit + NSubstitute setup.
## Integration tests in this repository
The repository's own test suite lives in `src/tests/SourceGenerator.IntegrationTests` and uses `Purview.SourceGeneratorFramework.Testing.TUnit` (TUnit with the framework's `CodeQuery` syntax-lookup API and assertion extensions). See [Contributing](../contributing/) for running them.
---
# Sample Application
The [.NET Aspire sample](https://github.com/purview-dev/telemetry-sourcegenerator/tree/main/samples/SampleApp) demonstrates Activities, Logs, and Metrics generation working together with the Aspire Dashboard.
## Solution layout
| Project | Purpose |
| --- | --- |
| `SampleApp.AppHost` | The Aspire orchestrator. |
| `SampleApp.APIService` | Backend service exposing the telemetry interfaces and the generated `TelemetryNames` registration. |
| `SampleApp.APIService.UnitTests` | TUnit + NSubstitute unit tests over the generated interfaces. |
| `SampleApp.Shared` | Shared DTOs (e.g. `WeatherForecast`). |
| `SampleApp.Web` | Frontend client that also generates its own telemetry. |
| `SampleApp.ServiceDefaults` | Aspire service defaults; registers the generated meter/activity source names. |
The sample targets `net10.0` and enables `EmitCompilerGeneratedFiles`, so generated output can be inspected under:
```
obj//net10.0/generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/
```
## Telemetry interfaces
### `IEntityStoreTelemetry` (APIService)
The multi-target interface from the README — one method emits an Activity, an Info log, and an AutoCounter simultaneously:
```csharp
[ActivitySource]
[Logger]
[Meter]
interface IEntityStoreTelemetry
{
[Activity]
[Info]
[AutoCounter]
Activity? GettingEntityFromStore(int entityId, [Baggage] string serviceUrl);
[Event]
[Trace]
void GetDuration(Activity? activity, int durationInMS);
[Context]
void RetrievedEntity(Activity? activity, float totalValue, int lastUpdatedByUserId);
[Warning]
void EntityNotFound(int entityId);
[Histogram]
void RecordEntitySize(int sizeInBytes);
}
```
### `IWeatherServiceTelemetry` (APIService)
Demonstrates single-method multi-targeting, Activity status codes, and enumerable expansion:
```csharp
[ActivitySource]
[Logger]
[Meter]
public interface IWeatherServiceTelemetry
{
[Activity(ActivityKind.Client)]
[Trace]
Activity? GettingWeatherForecast([Baggage] string someRandomBaggageInfo, int requestedCount);
[Event]
void ForecastReceived(Activity? activity, int minTempInC, int maxTempInC);
[Event(ActivityStatusCode.Error)]
void FailedToRetrieveForecast(Activity? activity, Exception exception);
[Event(ActivityStatusCode.Ok)]
void TemperaturesReceived(Activity? activity, TimeSpan elapsed);
[AutoCounter]
[Warning]
[Event]
void ItsTooCold(Activity? activity, int minTempInC, int tooColdCount);
[Histogram]
void HistogramOfTemperature(int temperature);
[Error]
[AutoCounter]
void RequestedCountIsOutOfRange(int requestCount);
[Info]
void TemperaturesWithinRange([ExpandEnumerable(maximumValueCount: 100)] int[] temperaturesInC);
}
```
### `IWeatherAPIClientTelemetry` (Web)
Demonstrates `[ExcludeTargets]`, an instrument prefix, and `HttpStatusCode` tags:
```csharp
[ActivitySource]
[Logger]
[Meter(InstrumentPrefix = "weather")]
public interface IWeatherAPIClientTelemetry
{
[Activity(ActivityKind.Client)]
[Info]
[AutoCounter]
Activity? GetWeatherForecasts(int? count);
[Event]
[Error]
[AutoCounter]
void FailedToGetForecast(Activity? activity, Exception ex, [ExcludeTargets(Targets.Activities)] int? count);
[Event]
void RequestComplete(Activity? activity, HttpStatusCode statusCode, bool isSuccessStatusCode);
[AutoCounter]
void RequestSuccess();
[Event]
[Warning]
void NoForecastsRecieved(Activity? activity);
[Event(ActivityStatusCode.Ok)]
[Debug]
void ForecastsRecieved(
Activity? activity,
int forecastCount,
[ExpandEnumerable(100), ExcludeTargets(Targets.Activities)]
WeatherForecast[] weatherForecasts
);
}
```
## Registering names with Aspire
`SampleApp.ServiceDefaults` wires the generated names into Aspire's OpenTelemetry setup:
```csharp
builder.AddServiceDefaults(TelemetryNames.MeterNames, TelemetryNames.ActivitySourceNames);
```
## Unit testing
`SampleApp.APIService.UnitTests` uses TUnit with NSubstitute to verify `WeatherService` behaviour against the generated interfaces without emitting real telemetry. See [Testing](../testing/) for the pattern.
## Running
```bash
just build-s
just test-s
```
With `dotnet run --project samples/SampleApp/SampleApp.AppHost`, the Aspire Dashboard shows the generated Activities, Logs, and Metrics in real time.
## See also
- [Getting Started](../)
- [Multi-Targeting](../multi-targeting/)
- [Generated Output](../generated-output/) — real generated code from this sample
---
# Contributing
Contributions are welcome! This page covers development setup, build commands, and the conventions enforced in this repository.
## Prerequisites
- .NET 10 SDK (projects target `net10.0`; the source generator targets `netstandard2.0`)
- [Bun](https://bun.sh) for the `package.json`/`.build/*.ts` scripts
- The `Purview.BuildSdk` MSBuild SDK (pinned in `global.json` under `msbuild-sdks`)
- `csharpier` dotnet tool (pinned in `.config/dotnet-tools.json`) for linting/formatting
## Clone and set up
```bash
git clone https://github.com/purview-dev/telemetry-sourcegenerator
cd telemetry-sourcegenerator
```
All `just` recipes read the [`Justfile`](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/Justfile); configuration defaults to **Debug**.
## Common commands
| Command | Purpose |
| --- | --- |
| `just build` | Builds `src/Telemetry.SourceGenerator.slnx` (Debug). |
| `just test` | Runs the integration tests (Debug) with a tree-node filter. |
| `just build-s` | Builds `samples/SampleApp/SampleApp.slnx`. |
| `just test-s` | Runs the sample test solution. |
| `just lint` | Runs `dotnet csharpier check .` (no writes). |
| `just lint-fix` | Runs `dotnet csharpier format .` (writes). |
| `just clean` | Cleans the main solution. |
| `just restore` | Restores packages for the main solution. |
| `just scrub` | Removes `bin`/`obj` folders, cleans, restores, and shuts down build servers. |
## Build and validation workflow
After making changes to the source generator:
1. `just build` — build the main solution.
2. `just test` — run the integration tests.
3. `just build-s && just test-s` — build and test the sample application end to end.
4. `just lint` / `just format` — ensure C# formatting compliance before committing.
:::note
Build and test commands can take a long time (tens of minutes) in slow environments. Give them a generous timeout and never cancel them part-way.
:::
## Testing
Integration tests live in `src/tests/SourceGenerator.IntegrationTests` and target `net8.0;net9.0;net10.0`, plus `net48` on Windows only. They use `Purview.SourceGeneratorFramework.Testing.TUnit` (TUnit + the framework's `CodeQuery` syntax-lookup API and assertion extensions). There is no Verify/snapshot library; refactoring tests use the framework's `CodeRefactoringTestBase` snapshot approach.
When generator behaviour changes, add or update tests in `src/tests/SourceGenerator.IntegrationTests/`.
## Conventions
- **Conventional commits** — enforced by commitlint (`.config/lefthook.yml`). Types: `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`.
- **Formatting** — C# is formatted with csharpier; check with `just lint`.
- **Solutions** — `.slnx` solution files are used.
- **Source generator** — targets `netstandard2.0` and uses `Purview.SourceGeneratorFramework` (CodeWriter-based emission, incremental pipeline, value-equatable models).
## Pipelines
The repo mirrors its GitHub CI/CD pipelines locally with the reusable `purview-build` tool:
| Command | Purpose |
| --- | --- |
| `just pipeline-pr` | Restore, build, lint, and tests (the PR gate). |
| `just pipeline-build` | Restore, build, lint (no tests). |
| `just pipeline-tests` | Build with tests enabled. |
| `just pipeline-release` | Full release: build, test, pack, publish, GitHub release. |
| `just pipeline-local-release` | Pack + publish to a local NuGet feed (see the Justfile note on argument quoting). |
## Source-generator skills
Before doing specialist work, load the relevant skill from `.agents/skills/` — see `AGENTS.md` ("Source-generator and testing skills") for the full list and when to load each one.
## Release process
See [`docs/release-process.md`](https://github.com/purview-dev/telemetry-sourcegenerator/blob/main/docs/release-process.md) for the full flow. In short: push a feature branch and open a PR to `main` (`pr.yml` runs build + lint + tests); merging to `main` triggers the reusable release pipeline (build, test, pack, publish, GitHub release). The version lives in `package.json`; after bumping it, run `just update-version` to sync docs/samples, then `just pipeline-local-release` to validate locally.
## Next steps
- [Release Flow](../release-flow/) — how releases are produced
- [Testing](../testing/) — mocking generated telemetry interfaces
---
# Release Flow
Releases are driven by GitHub Actions using the reusable [`purview-dev/build`](https://github.com/purview-dev/build) pipelines. No release steps are performed manually.
```text
Feature branch
→ PR to main (pr.yml runs purview-build.yml: restore + build + lint + tests)
→ merge to main (release.yml runs purview-release.yml: build, test, pack, publish)
→ GitHub Release (NuGet package + changelog, created by the pipeline)
```
## Workflow components
| Component | File | Purpose |
| --- | --- | --- |
| PR gate | `.github/workflows/pr.yml` | Runs the reusable `purview-dev/build` `purview-build.yml` pipeline (restore, build, lint, tests) and builds/tests the sample solution. |
| CD pipeline | `.github/workflows/release.yml` | Runs the reusable `purview-dev/build` `purview-release.yml` pipeline on push to `main`. |
| Local pipeline | `Justfile` `pipeline-*` recipes | Mirror the CI/CD pipelines locally via the `Purview.Build` tool (`.tools/purview-build`). |
## Developer workflow
1. Create a feature branch and make your changes.
2. Validate locally with `just pipeline-pr` (restore, build, lint, tests — the same gate CI enforces).
3. Push and open a PR to `main`. `pr.yml` must pass (build, lint, tests, plus the sample build/test job).
4. Merge the PR into `main`.
5. The release is published automatically — `release.yml` restores and builds the main and sample solutions, runs the integration tests, packs the NuGet package, publishes it, and creates a GitHub Release.
## Versioning
- The version lives in `package.json`. **Current Version:** 5.0.0
- It is applied to `Version`/`PackageVersion` by `Purview.BuildSdk` via package.json version detection.
- `just version` prints the current version.
- After bumping `package.json`, run `just update-version` to sync the version into docs/samples.
## Building the package locally
| Command | Purpose |
| --- | --- |
| `just pack` | Updates the version, then packs the NuGet package into `artifacts/`. |
| `just pipeline-local-release` | Packs and publishes to a local NuGet feed (see the Justfile note on argument quoting for the feed path). |
| `just pipeline-release` | Full release pipeline (build, test, pack, publish, GitHub release). |
## Pre-release validation
Before a release, validate locally:
1. `just pipeline-pr` — restore, build, lint, tests.
2. `just build-s && just test-s` — sample solution build and tests.
3. `just pipeline-local-release` — confirm the pack + local publish succeed.
## Releasing
1. Bump the version in `package.json`.
2. Run `just update-version` to sync docs/samples.
3. Push a PR with the version bump; merge to `main`.
4. `release.yml` publishes the release automatically.
> The legacy `scripts/setup-release.*` files describe a changeset-based (`@changesets/cli`) flow that is **not** used by this repository. They are retained for reference only; the actual release automation lives in `.github/workflows/pr.yml` and `.github/workflows/release.yml`.
## See also
- [Contributing](../contributing/) — development setup and conventions