Generates ActivitySource, ILogger, and Metrics based telemetry from methods you define on an interface.
- Zero boilerplate - define methods on an interface, get full telemetry implementation generated
- Multi-target generation - generate Activities, Logging, and Metrics from a single interface
- Testable - easy mocking/substitution for unit testing
- DI-ready - automatic dependency injection registration helpers
Consumer runtime targets:
- .NET Framework 4.8
- .NET 8 or higher
Build toolchain requirement:
- Visual Studio 2026 (18.x) or .NET 10 SDK (Roslyn 5.9.0+)
Add to your Directory.Build.props or .csproj file:
<PackageReference Include="Purview.Telemetry.SourceGenerator" Version="5.0.0-prerelease.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>analyzers</IncludeAssets>
</PackageReference>Define an interface with telemetry methods and the generator creates the implementation:
using Purview.Telemetry;
// Multi-target interface: generates Activities, Logging, AND Metrics from combined methods
[ActivitySource]
[Logger]
[Meter]
interface IEntityStoreTelemetry
{
// MULTI-TARGET: Creates Activity + Logs Info + Increments Counter - all from one method!
[Activity]
[Info]
[AutoCounter]
Activity? GettingEntityFromStore(int entityId, [Baggage]string serviceUrl);
// MULTI-TARGET: Adds ActivityEvent + Logs the duration as Trace.
[Event]
[Trace]
void GetDuration(Activity? activity, int durationInMS);
// Single-target examples (when you only need one telemetry type):
// Activity-only: Adds Baggage to the Activity
[Context]
void RetrievedEntity(Activity? activity, float totalValue, int lastUpdatedByUserId);
// Log-only: Structured log message
[Warning]
void EntityNotFound(int entityId);
// Metric-only: Histogram for tracking values
[Histogram]
void RecordEntitySize(int sizeInBytes);
}Register with dependency injection:
// Generated extension method
services.AddEntityStoreTelemetry();Then inject and use - a single method call emits an Activity, Log, and Metric simultaneously:
public class EntityService(IEntityStoreTelemetry telemetry)
{
public async Task<Entity> GetEntityAsync(int id, string serviceUrl, CancellationToken cancellationToken)
{
// Single call creates Activity AND logs AND increments counter
using var activity = telemetry.GettingEntityFromStore(id, serviceUrl);
var entity = await _repository.GetAsync(id, cancellationToken);
// Adds event to activity AND logs duration
telemetry.GetDuration(activity, stopwatch.ElapsedMilliseconds);
if (entity == null)
{
// Logs warning if entity not found
telemetry.EntityNotFound(id);
return null;
}
// Activity context addition
telemetry.RetrievedEntity(activity, entity.TotalValue, entity.LastUpdatedByUserId);
// Histogram only records size
telemetry.RecordEntitySize(entity.SizeInBytes);
return entity;
}
}| Attribute | Generation Type | Description |
|---|---|---|
[ActivitySource] |
Class-level | Marks interface for Activity generation |
[Activity] |
Method | Creates and starts a new Activity |
[Event] |
Method | Adds an ActivityEvent to an Activity |
[Context] |
Method | Adds Baggage to an Activity |
[Logger] |
Class-level | Marks interface for ILogger generation |
[Log] |
Method | Generates structured log message |
[Trace], [Debug], [Info], [Warning], [Error], [Critical] |
Method | Log with specific level |
[Meter] |
Class-level | Marks interface for Metrics generation |
[Counter], [AutoCounter] |
Method | Counter instrument |
[UpDownCounter] |
Method | Up-down counter instrument |
[Histogram] |
Method | Histogram instrument |
[ObservableCounter], [ObservableGauge], [ObservableUpDownCounter] |
Method | Observable instruments |
Tip
For single-target interfaces (only Activities, only Logging, or only Metrics), the generator automatically infers the necessary attributes. See the wiki for details.
- Homepage
- Documentation
- Full Wiki
- Generated Output Examples
- Multi-Targeting Guide
- Logging Configuration
This repository ships with Agent Skills under .agents/skills/ covering the source generator, its test framework, and the Purview.DotNetProjectSdk build SDK. See AGENTS.md — "Source-generator and testing skills" for the full list and when to load each one.
The .NET Aspire Sample demonstrates Activities, Logs, and Metrics generation working together with the Aspire Dashboard.
Tip
The sample project has EmitCompilerGeneratedFiles enabled so you can inspect the generated output.
Benchmarked on 13th Gen Intel Core i9-13900KF, .NET SDK 10.0.401. See the Performance wiki page for full cross-runtime results.
| Scenario | HasListener | Manual | Generated | Ratio |
|---|---|---|---|---|
| start + complete | False | 0.53 ns | 0.58 ns | 1.11x |
| start + complete | True | 220 ns / 1008 B | 217 ns / 1008 B | 0.98x |
| start + fail | True | 208 ns / 920 B | 203 ns / 920 B | 0.92x |
Generated activities match hand-written code with identical allocations.
| Scenario | HasLogging | LoggerMessage.Define | Generated v1 | Generated v2 |
|---|---|---|---|---|
| single Info call | True | 6.12 ns | 4.43 ns (0.72x) | 3.80 ns (0.62x) |
| full lifecycle (4 calls) | True | 18.88 ns | 19.58 ns (1.04x) | 15.01 ns (0.80x) |
Both v1 and v2 allocate 0 bytes per call on all runtimes. Generated methods are emitted with [MethodImpl(AggressiveInlining)]; generated v1 and v2 both beat the manual LoggerMessage.Define baseline for single calls on .NET 10.0.
| Scenario | HasListener | Single-target | Multi-target generated | Multi-target manual |
|---|---|---|---|---|
| start + complete | True | 216 ns / 1008 B | 230 ns / 1032 B (1.07x) | 260 ns / 1032 B (1.20x) |
Adding full Activity+Logging+Metrics multi-target generation costs ~7% over Activity-only on .NET 10.0, and the generated multi-target code is faster than the hand-written equivalent.
| Scenario | Generated | Notes |
|---|---|---|
| auto-counter (0 tags) | 0.40 ns | - |
| auto-counter (1 tag) | 0.35 ns | - |
| up-down counter | 0.37 ns | - |
| histogram (0 tags) | 0.42 ns | - |
| histogram (1 tag) | 0.34 ns | - |
| 4+ tags (TagList) | 5-8 ns | Stack-allocated TagList |
All instruments are 0 allocations on all runtimes.
v4 consolidates all attributes into a single namespace. Update your using statements:
Before (v3):
using Purview.Telemetry.Activities;
using Purview.Telemetry.Logging;
using Purview.Telemetry.Metrics;After (v4):
using Purview.Telemetry;All attributes ([ActivitySource], [Logger], [Meter], [Activity], [Event], [Log], [Counter], etc.) are now in the unified Purview.Telemetry namespace.
v4 defaults to OpenTelemetry semantic conventions for generated telemetry names, improving observability and cross-platform compatibility. This is a breaking change if you rely on specific telemetry names.
| Telemetry Type | v3 Behavior | v4 Default | Impact |
|---|---|---|---|
| ActivitySource Name | Assembly name lowercased: "myapp" |
Assembly name preserved: "MyApp" |
ActivitySource names change casing |
| Tag/Baggage Keys | Lowercased, smashed: "entityid" |
snake_case: "entity_id" |
Tag keys have underscores for word boundaries |
| Metric Instrument Names | Lowercased, smashed: "recordhistogram" |
Hierarchical dot.separated: "myapp.products.record.histogram" |
Includes meter name prefix + word boundaries |
| Metric Tag Keys | Lowercased, smashed: "requestcount" |
snake_case: "request_count" |
Metric tag keys have underscores |
Before (v3/Legacy):
// Generated code:
new ActivitySource("myapp") // lowercase
activity.SetTag("entityid", ...) // smashed compound
var meter = meterFactory.Create("MyApp.Products");
meter.CreateCounter<int>("recordcount") // smashed compound, no meter prefixAfter (v4 OpenTelemetry mode - DEFAULT):
// Generated code:
new ActivitySource("MyApp") // preserves casing
activity.SetTag("entity_id", ...) // snake_case
var meter = meterFactory.Create("MyApp.Products");
meter.CreateCounter<int>("myapp.products.record.count") // hierarchical: meter + instrumentNote: In OpenTelemetry mode, instrument names automatically include the meter name prefix (converted to lowercase dot.separated), following OpenTelemetry best practices for hierarchical metric naming.
If you need to maintain v3-style naming for backward compatibility, set NamingConvention = Legacy on the [TelemetryGeneration] attribute:
using Purview.Telemetry;
// Revert ALL telemetry to v3 naming (assembly-level)
[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]Or set per-interface:
// Legacy naming for this interface only
[TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]
interface IMyTelemetry { }public enum NamingConvention
{
Legacy = 0, // v3 behaviour: lowercase, smashed compounds
OpenTelemetry = 1 // v4 default: OTel conventions (dot.separated, snake_case)
}Recommendation: Use NamingConvention.OpenTelemetry (default) for new projects. Only use Legacy if you need exact v3 compatibility.
Contributions are welcome! See the Contributing guide for development setup and testing instructions.
See docs/release-process.md for the release flow, and AGENTS.md
for the build, validation, and convention rules. Bump the version in package.json and run
just update-version to sync it into docs/samples before packaging.