Skip to content
Purview Dev logo

Sample Application

Stable Telemetry SourceGenerator Reviewed 2026-10-07 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

The .NET Aspire sample demonstrates Activities, Logs, and Metrics generation working together with the Aspire Dashboard.

ProjectPurpose
SampleApp.AppHostThe Aspire orchestrator.
SampleApp.APIServiceBackend service exposing the telemetry interfaces and the generated TelemetryNames registration.
SampleApp.APIService.UnitTestsTUnit + NSubstitute unit tests over the generated interfaces.
SampleApp.SharedShared DTOs (e.g. WeatherForecast) plus IWeatherForecastTelemetry — a library-owned ActivitySource and Meter, aggregated by each host.
SampleApp.WebFrontend client that also generates its own telemetry.
SampleApp.ServiceDefaultsAspire 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/<Configuration>/net10.0/generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/

The multi-target interface from the README — one method emits an Activity, an Info log, and an AutoCounter simultaneously:

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

Demonstrates single-method multi-targeting, Activity status codes, and enumerable expansion:

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

Demonstrates [ExcludeTargets], an instrument prefix, and HttpStatusCode tags:

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

SampleApp.ServiceDefaults is referenced by the projects that own the telemetry, so it cannot discover their names itself. Instead, each host project opts into the generated TelemetryNames class and passes the arrays in. In SampleApp.APIService/Properties/AssemblyInfo.cs:

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

The class aggregates the host’s own names with the names recorded by every assembly it references. SampleApp.Shared ships its own IWeatherForecastTelemetry (an ActivitySource and a Meter, both named after that assembly), consumed by its WeatherForecastValidator which WeatherService calls. The library registers its services with builder.Services.AddWeatherForecastValidation() but never registers its names with OpenTelemetry — those arrive through the project reference:

public static readonly string[] MeterNames = new string[] { "SampleApp.APIService", "SampleApp.Shared" };
public static readonly string[] ActivitySourceNames = new string[] { "SampleApp.APIService", "SampleApp.Shared" };

SampleApp.Web references SampleApp.Shared for the DTOs only, so it aggregates the same names without emitting from them — the aggregate follows the reference graph, not usage, and registering a source that stays quiet costs nothing.

See Generation.

samples/SampleApp.MultiProject is a smaller console solution dedicated to this aggregation, for solutions with many projects and differing telemetry requirements. Four assemblies own telemetry — two of them reachable from the host only transitively — and a ServiceDefaults-style project that every one of them references receives the complete set of names:

Terminal window
just run-mp

See its README for the reference graph and the generated output.

SampleApp.APIService.UnitTests uses TUnit with NSubstitute to verify WeatherService behaviour against the generated interfaces without emitting real telemetry. See Testing for the pattern.

Terminal window
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.