-
Notifications
You must be signed in to change notification settings - Fork 3
FAQ
Common questions and answers about Purview Telemetry Source Generator.
Purview Telemetry Source Generator is a .NET incremental source generator that generates implementation code for Activities (distributed tracing), Logging (structured logs), and Metrics based on interface definitions you create. Instead of writing boilerplate telemetry code, you define methods on an interface and the generator creates the implementation automatically.
Benefits:
- 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 and uses aggressive inlining
- Maintainability - Changes to interfaces automatically propagate to implementation
- .NET Framework 4.8 or higher
- .NET 8 or higher
Yes! Version 4 defaults to OpenTelemetry semantic conventions for generated names. The generated Activities, Logs, and Metrics work seamlessly with OpenTelemetry exporters and collectors.
Add the NuGet package to your .csproj:
<PackageReference Include="Purview.Telemetry.SourceGenerator" Version="4.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>analyzers</IncludeAssets>
</PackageReference>See Getting Started for more details.
These settings ensure the source generator runs at compile time without adding runtime dependencies:
-
PrivateAssets="all"- Prevents the package from being exposed to consuming projects -
IncludeAssets="analyzers"- Includes only what's needed for source generation
Depends on what you're generating:
For Activities:
- No additional packages required (uses
System.Diagnostics.Activity)
For Logging:
-
Microsoft.Extensions.Logging.Abstractions(usually already in your project) - Optionally:
Microsoft.Extensions.Telemetry.Abstractionsfor Generation v2 features
For Metrics:
- No additional packages required (uses
System.Diagnostics.Metrics)
For Dependency Injection:
-
Microsoft.Extensions.DependencyInjection.Abstractions(usually already in your project)
Add this to your .csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>Generated files appear in obj/Debug|Release/generated/Purview.Telemetry.SourceGenerator/.
Use Activities when:
- Tracking distributed operations across services
- Measuring end-to-end request latency
- Creating trace spans for observability
- Correlating related operations
Use Logging when:
- Recording events or state changes
- Debugging issues with structured data
- Auditing user actions
- Recording error details and stack traces
Use Metrics when:
- Counting occurrences (requests, errors, events)
- Measuring distributions (latency, size, duration)
- Tracking gauges (queue depth, memory usage)
- Aggregating data for dashboards
Pro tip: Use Multi-Targeting to combine all three!
Activities can be null if:
- No listeners are subscribed to the ActivitySource
- Sampling determined the activity shouldn't be recorded
Always return Activity? (nullable) and check for null before using the activity. The generated code handles this correctly with the null-conditional operator.
Always pass Activity parameters explicitly. Don't rely on Activity.Current as it may not be the activity you expect, especially in async code or when multiple activities are nested.
// Good
[Event]
void OrderProcessed(Activity? activity, int orderId);
// Avoid
[Event]
void OrderProcessed(int orderId); // Uses Activity.Current implicitly-
[Tag]- Added as tags to the Activity or ActivityEvent. Tags are recorded with the activity but not automatically propagated to child activities. -
[Baggage]- Added as baggage to the Activity. Baggage is automatically propagated to all child activities and across service boundaries.
Use baggage sparingly as it increases overhead. Use tags for most properties.
Generation v2 (default when Microsoft.Extensions.Telemetry.Abstractions is available):
- Uses newer logging telemetry abstractions
- Supports dynamic message template generation
- Can enumerate arrays/IEnumerables as log properties
- More closely matches
Microsoft.Gen.Loggingoutput
Generation v1 (legacy):
- Uses older high-performance logging pattern
- Simpler generated code
- Doesn't require
Microsoft.Extensions.Telemetry.Abstractions
See Logging for details.
Return IDisposable? from your log method:
[Logger]
interface IOrderTelemetry
{
[Info]
IDisposable? ProcessingOrder(Guid orderId);
}
// Usage - automatically logs duration
using (telemetry.ProcessingOrder(orderId))
{
// Processing logic
// Duration logged automatically when disposed
}Not directly in v4. The message template is generated from the method name and parameters. If you need custom templates, you can:
- Use the
[Exclude]attribute and implement the method manually - Name your methods descriptively to generate meaningful messages
Add this to your .csproj or Directory.Build.props:
<PropertyGroup>
<DefineConstants>EXCLUDE_PURVIEW_TELEMETRY_LOGGING</DefineConstants>
</PropertyGroup>This is useful when Microsoft.Extensions.Logging types aren't available in your project.
[Counter] - You specify the measurement value:
[Counter]
void ItemsProcessed([InstrumentMeasurement]int count, [Tag]string type);
// Usage
telemetry.ItemsProcessed(5, "widget"); // Adds 5 to counter[AutoCounter] - Automatically increments by 1:
[AutoCounter]
void ItemProcessed([Tag]string type);
// Usage
telemetry.ItemProcessed("widget"); // Adds 1 to counterUse Counter when:
- Counting discrete events (requests, errors, completions)
- Values only go up (monotonically increasing)
Use Histogram when:
- Recording distributions (latency, size, duration)
- Want percentiles (p50, p95, p99)
- Values can vary widely
[AutoCounter]
void RequestCompleted([Tag]string endpoint); // Count completions
[Histogram]
void RequestDuration([InstrumentMeasurement]int ms, [Tag]string endpoint); // Track latency distributionObservable metrics (Counter, Gauge, UpDownCounter) are "pull-based" - the collector calls your function to get the current value. Use them for:
- Values that already exist (memory usage, queue depth, cache size)
- Expensive calculations you don't want to perform on every update
- Periodic sampling of state
[ObservableGauge]
void QueueDepth(Func<int> measurement);
// Setup once
telemetry.QueueDepth(() => _queue.Count);
// Collector calls this function periodicallyIn OpenTelemetry mode (v4 default), metric instrument names include the meter name as a prefix for hierarchical naming:
[Meter("MyApp.Orders")]
interface IOrderMetrics
{
[Counter]
void OrderProcessed([InstrumentMeasurement]int count);
}
// Generated name: "myapp.orders.order.processed"
// ^^^^^^^^^^^^^^^ meter prefix (lowercase)
// ^^^^^^^^^^^^^^^^ instrument name (snake_case)This follows OpenTelemetry best practices. To revert to v3 naming, use [TelemetryGeneration(NamingConvention = NamingConvention.Legacy)].
Yes! This is called Multi-Targeting:
[ActivitySource("MyApp")]
[Logger]
[Meter("MyApp")]
interface IMyTelemetry
{
// Creates Activity + Logs + Increments Counter - all from one method!
[Activity]
[Info]
[AutoCounter]
Activity? ProcessingRequest([Baggage]string requestId);
}Single-target - Interface has only one class-level attribute ([ActivitySource], [Logger], or [Meter]):
- Can infer some method-level attributes
- Simpler method signatures
Multi-target - Interface has multiple class-level attributes:
- Must explicitly specify all method-level attributes
- No inference (to avoid ambiguity)
- More powerful (one method generates multiple telemetry types)
In multi-target interfaces, you can have methods that target only one telemetry type:
[ActivitySource("MyApp")]
[Logger]
[Meter]
interface IMyTelemetry
{
// Multi-target: Activity + Log + Metric
[Activity]
[Info]
[AutoCounter]
Activity? ProcessRequest(int requestId);
// Single-target: Log only
[Warning]
void SlowRequest(int requestId, int durationMs);
// Single-target: Metric only
[Histogram]
void ResponseSize([InstrumentMeasurement]int bytes);
}Use the [TelemetryGeneration] attribute:
[TelemetryGeneration(ClassName = "MyCustomTelemetry")]
[ActivitySource("MyApp")]
interface IMyTelemetry
{
// Generated class will be named "MyCustomTelemetry" instead of "MyTelemetryCore"
}[TelemetryGeneration(GenerateDependencyExtension = false)]
[ActivitySource("MyApp")]
interface IMyTelemetry
{
// No AddMyTelemetry() extension method will be generated
}Yes:
[TelemetryGeneration(DependencyInjectionClassIsPublic = true)]
[ActivitySource("MyApp")]
interface IMyTelemetry
{
// Generated DI class will be public instead of internal
}Use the [Exclude] attribute:
[ActivitySource("MyApp")]
interface IMyTelemetry
{
[Activity]
Activity? NormalMethod(int id);
[Exclude]
void CustomMethod(int id); // Not generated, you implement it manually
}
// Implement excluded methods in a partial class
partial class MyTelemetryCore
{
public void CustomMethod(int id)
{
// Your custom implementation
}
}Two major changes:
-
Namespace Consolidation - All attributes now in
Purview.Telemetry - OpenTelemetry Naming - New default naming conventions (snake_case, hierarchical)
See Breaking Changes for detailed migration guide.
Use the [TelemetryGeneration] attribute:
[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]This reverts to v3-style naming (lowercase, smashed compounds). See OpenTelemetry-Aligned Naming.
Only if you use the default OpenTelemetry naming mode. Telemetry names will change:
-
"entityid"→"entity_id"(tags/properties) -
"recordcount"→"myapp.products.record.count"(metrics) -
"myapp"→"MyApp"(ActivitySource names)
Options:
- Update dashboards to use new names (recommended)
- Use
NamingConvention.Legacyto keep v3 names
Not in the same project. Choose one version per project. However, in a multi-project solution, different projects can use different versions (though this isn't recommended for consistency).
Check:
- Interface has a class-level attribute (
[ActivitySource],[Logger], or[Meter]) - Methods have appropriate method-level attributes
- Interface is not generic (generics aren't supported)
- Rebuild the project to trigger generation
- Check the Error List for diagnostics starting with
TSG
See the Diagnostics page for a complete list of diagnostic codes and their meanings.
Common causes:
- Missing NuGet packages (
Microsoft.Extensions.Logging.Abstractions,Microsoft.Extensions.DependencyInjection) - Wrong parameter types on methods
- Incorrect use of attributes
- Generic interfaces or methods (not supported)
Check the Error List for specific compilation errors.
Solutions:
- Rebuild the project
- Restart Visual Studio / IDE
- Close and reopen files
- Clean solution (
dotnet clean) then rebuild
Source generators sometimes require a rebuild for IntelliSense to update.
The generated methods are synchronous, but your calling code can be async. The telemetry operations are lightweight and non-blocking:
public async Task ProcessAsync(int id)
{
using var activity = telemetry.StartProcessing(id);
await DoWorkAsync(); // Async work is fine
telemetry.Completed(activity);
}No runtime performance impact! The code is generated at compile time, not runtime. The generated code:
- Uses aggressive inlining (
[MethodImpl(MethodImplOptions.AggressiveInlining)]) - Avoids allocations where possible
- Is as fast as hand-written code
Minimal allocations:
- Activities are pooled by the framework
- Log state is struct-based in Generation v2
- Metric instruments are created once and reused
Not directly, but you can:
- Use
[Exclude]and implement methods manually in a partial class - Control naming via
[TelemetryGeneration]attribute - Request features via GitHub issues
The source generator itself works with AOT. However, generated code may use reflection or DI patterns that need AOT compatibility testing. Full AOT support is on the roadmap.
Yes! The generated classes are internal by default, so they won't leak to consumers. You can expose the interface and consumers can mock it in tests.
If you need public DI registration:
[TelemetryGeneration(DependencyInjectionClassIsPublic = true)]Easy - just mock the interface:
// Using Moq
var mockTelemetry = new Mock<IOrderTelemetry>();
var service = new OrderService(mockTelemetry.Object);
// Using NSubstitute
var mockTelemetry = Substitute.For<IOrderTelemetry>();
var service = new OrderService(mockTelemetry);
// Verify calls
mockTelemetry.Verify(x => x.ProcessingOrder(orderId, customerName), Times.Once);- Documentation: GitHub Wiki
- Issues: GitHub Issues
- Sample: Sample Application
Open an issue on GitHub Issues with:
- Version of the package
- .NET version
- Minimal reproducible example
- Expected vs actual behavior
- Any diagnostic errors (TSG codes)
Open an issue on GitHub Issues with:
- Description of the feature
- Use case / problem it solves
- Example of how you'd like to use it
- Any alternatives you've considered
Important
Consider helping children around the world affected by conflict. You can donate any amount to War Child here - any amount can help save a life.
Purview Telemetry Source Generator v4.0.0-prerelease.1 | Home | Getting Started | FAQ | Breaking Changes | GitHub