Skip to content
Purview Dev logo

Frequently Asked Questions (FAQ)

Stable Telemetry SourceGenerator Reviewed 2026-10-28 purview-dev/telemetry-sourcegenerator Star on GitHub 36 activity c-sharp distributed-tracing dotnet events high-performance-logging instrumentation logging melt metrics nuget observability open-telemetry open-telemetry-csharp otel roslyn source-generator spans telemetry tracing

Common questions and answers about the Purview Telemetry Source Generator.

What is the Purview Telemetry Source Generator?

Section titled “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.

  • 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
  • .NET 8 or higher
  • .NET Framework 4.8 or higher

Yes. v4+ defaults to OpenTelemetry semantic conventions for generated names. The generated Activities, Logs, and Metrics work with OpenTelemetry exporters and collectors.

Add the NuGet package to your .csproj:

<PackageReference Include="Purview.Telemetry.SourceGenerator">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>analyzers</IncludeAssets>
</PackageReference>

See Getting Started for more details.

Why do I need PrivateAssets and IncludeAssets?

Section titled “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

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)
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Generated files appear in obj/Debug|Release/<tfm>/generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/.

When should I use Activities vs Logging vs Metrics?

Section titled “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 to combine all three.

Why does my Activity method return Activity? instead of Activity?

Section titled “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?

Section titled “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.

// 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]?

Section titled “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.

What’s the difference between Generation v1 and v2?

Section titled “What’s the difference between Generation v1 and v2?”

See 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.

Return IDisposable? from a log method:

[Logger]
interface IOrderTelemetry
{
[Info]
IDisposable? ProcessingOrder(Guid orderId);
}
using (telemetry.ProcessingOrder(orderId))
{
// Duration logged automatically when disposed
}

Yes — [Log].MessageTemplate sets the template. Placeholders map to method parameters:

[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.

<PropertyGroup>
<DefineConstants>EXCLUDE_PURVIEW_TELEMETRY_LOGGING</DefineConstants>
</PropertyGroup>

Useful when Microsoft.Extensions.Logging types are not available.

What’s the difference between Counter and AutoCounter?

Section titled “What’s the difference between Counter and AutoCounter?”
  • [Counter] — you specify the measurement value:

    [Counter]
    void ItemsProcessed([InstrumentMeasurement]int count, [Tag]string type);
  • [AutoCounter] — automatically increments by 1 per call:

    [AutoCounter]
    void ItemProcessed([Tag]string type);
  • 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?

Section titled “What are observable metrics and when should I use them?”

Observable instruments ([ObservableCounter], [ObservableGauge], [ObservableUpDownCounter]) are pull-based — the collector calls your Func<T> 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.

[ObservableGauge]
void QueueDepth(Func<int> measurement);
telemetry.QueueDepth(() => _queue.Count);

When do metric names include the meter name?

Section titled “When do metric names include the meter name?”

Only when you opt in with [assembly: MeterGeneration(MeterNameGenerationType = MeterNameGenerationType.OpenTelemetry)]. The default, MeterNameGenerationType.DotNet, prefixes instrument names with the interface name instead:

[Meter("MyApp.Products")]
interface IProductTelemetry
{
[Counter]
void RecordCount([InstrumentMeasurement] int count);
}
// Default: "product.record_count"
// MeterNameGenerationType.OpenTelemetry: "my_app.products.product.record_count"
// ^^^^^^^^^^^^^^^ meter name (snake_case), outermost
// ^^^^^^^ interface prefix (IProductTelemetry)
// ^^^^^^^^^^^^ instrument name

See Instrument naming for the full rules.

How do I register the names from every project in a large solution?

Section titled “How do I register the names from every project in a large solution?”

Each assembly records its own generated names in metadata ([assembly: GeneratedTelemetryNames]), and the TelemetryNames class aggregates them across the reference graph. Opt the project that registers telemetry into the class:

[assembly: TelemetryGeneration(GenerateTelemetryNamesClass = true)]
builder.AddServiceDefaults(TelemetryNames.MeterNames, TelemetryNames.ActivitySourceNames);

That project needs no telemetry interfaces of its own — the assembly-level attribute is enough, and the class is filled from what its references recorded.

Libraries need no configuration at all, however many of them there are and however complex their telemetry. The aggregate is built at compile time, so it is safe under trimming and native AOT, and it covers transitive references too.

samples/SampleApp.MultiProject is a runnable example: four assemblies own telemetry, two of them only transitively referenced by the host, and one ServiceDefaults-style project receives the complete set of names (just run-mp).

Why can’t my ServiceDefaults project generate the names itself?

Section titled “Why can’t my ServiceDefaults project generate the names itself?”

Because of the reference direction: an Aspire-style ServiceDefaults is referenced by the projects that own the telemetry, so their assemblies are not in its reference graph. Generate the class in each host (executable) project and pass the arrays into ServiceDefaults, as the sample application does.

Can I generate Activities, Logging, AND Metrics from one interface?

Section titled “Can I generate Activities, Logging, AND Metrics from one interface?”

Yes — this is called Multi-Targeting:

[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?

Section titled “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).

How do I control the generated class name?

Section titled “How do I control the generated class name?”
[TelemetryGeneration(ClassName = "MyCustomTelemetry")]
[ActivitySource("MyApp")]
interface IMyTelemetry { }

How do I disable dependency injection generation?

Section titled “How do I disable dependency injection generation?”
[TelemetryGeneration(GenerateDependencyExtension = false)]
[ActivitySource("MyApp")]
interface IMyTelemetry { }

How do I exclude a method from generation?

Section titled “How do I exclude a method from generation?”

Use [Exclude] and implement it manually in a partial class:

[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
}
}

What are the breaking changes in v4 and v5?

Section titled “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.

[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]

See OpenTelemetry-Aligned Naming.

Not in the same project. In a multi-project solution different projects can use different versions, though that is not recommended.

  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

See Diagnostics for the complete list of codes and meanings.

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.

The generated methods are synchronous, but your calling code can be async — telemetry operations are lightweight and non-blocking.

Does using a source generator impact performance?

Section titled “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.

Not directly, but you can use [Exclude] with partial-class implementations, control naming via [TelemetryGeneration], and configure defaults through the assembly-level generation attributes.

The source generator itself works with AOT. Generated code uses no reflection and is trimmable; validate your full scenario with AOT analysis enabled.

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?

Section titled “How do I test code that uses telemetry interfaces?”

Just mock the interface — see Testing:

var mockTelemetry = Substitute.For<IOrderTelemetry>();
var service = new OrderService(mockTelemetry);

When reporting a bug, include the package version, .NET version, a minimal repro, expected vs actual behaviour, and any TSG diagnostics.