Skip to content

Sample Application

Kieron Lanning edited this page Mar 16, 2026 · 7 revisions

Sample Application

The sample app is a complete weather forecast application demonstrating the Purview Telemetry Source Generator in a real-world .NET Aspire setup. It covers all three telemetry targets — Activities, Logging, and Metrics — including multi-target methods that generate multiple telemetry types from a single method call.

The solution lives in samples/SampleApp/.

Project Structure

Project Description
SampleApp.AppHost .NET Aspire orchestrator — wires up all resources and the Aspire dashboard
SampleApp.APIService RESTful weather API backend; contains the primary telemetry interfaces
SampleApp.Web Blazor Server frontend that calls the API with its own HTTP client telemetry
SampleApp.Shared Shared WeatherForecast DTO used by both API and frontend
SampleApp.ServiceDefaults Common Aspire service defaults (OpenTelemetry, health checks, resilience)
SampleApp.APIService.UnitTests Unit tests demonstrating how to mock and assert generated telemetry interfaces
flowchart TD
    AppHost["SampleApp.AppHost<br/>.NET Aspire Orchestrator"]

    subgraph running["Running Services"]
        Web["SampleApp.Web<br/>Blazor Server Frontend"]
        API["SampleApp.APIService<br/>Weather REST API"]
        Scalar["scalar<br/>API Docs"]
    end

    subgraph libs["Shared Libraries"]
        Defaults["SampleApp.ServiceDefaults<br/>OpenTelemetry · Health · Resilience"]
        SharedLib["SampleApp.Shared<br/>WeatherForecast DTO"]
    end

    AppHost --> Web
    AppHost --> API
    AppHost --> Scalar
    Web -->|HTTP + service discovery| API
    Scalar -.->|API reference| API
    Web --> Defaults
    API --> Defaults
    Web --> SharedLib
    API --> SharedLib
Loading

Telemetry Interfaces

There are three generated telemetry interfaces across the solution:


IEntityStoreTelemetry (SampleApp.APIService, global namespace) — a standalone demo interface matching the Quick Start guide. It is included in the project as a reference example and is not wired into the app's HTTP endpoints. See Generated Output for the code it produces.

[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 (SampleApp.APIService.Services) — the primary telemetry for the weather service backend. Registered via builder.Services.AddWeatherServiceTelemetry() and injected into WeatherService. Demonstrates the full range of multi-target patterns:

[ActivitySource]
[Logger]
[Meter]
public interface IWeatherServiceTelemetry
{
    // MULTI-TARGET: starts Activity + logs Trace
    [Activity(ActivityKind.Client)]
    [Trace]
    Activity? GettingWeatherForecast([Baggage] string someRandomBaggageInfo, int requestedCount);

    // SINGLE-TARGET: adds ActivityEvent
    [Event]
    void ForecastReceived(Activity? activity, int minTempInC, int maxTempInC);

    // SINGLE-TARGET: adds ActivityEvent with Error status
    [Event(ActivityStatusCode.Error)]
    void FailedToRetrieveForecast(Activity? activity, Exception ex);

    // SINGLE-TARGET: adds ActivityEvent with Ok status
    [Event(ActivityStatusCode.Ok)]
    void TemperaturesReceived(Activity? activity, TimeSpan elapsed);

    // MULTI-TARGET: increments counter + logs Warning + adds ActivityEvent
    [AutoCounter]
    [Warning]
    [Event]
    void ItsTooCold(Activity? activity, int minTempInC, int tooColdCount);

    // SINGLE-TARGET: histogram per temperature reading
    [Histogram]
    void HistogramOfTemperature(int temperature);

    // MULTI-TARGET: logs Error + increments counter (no Activity parameter)
    [Error]
    [AutoCounter]
    void RequestedCountIsOutOfRange(int requestCount);

    // SINGLE-TARGET: Info log with enumerable expansion (up to 100 items)
    [Info]
    void TemperaturesWithinRange([ExpandEnumerable(maximumValueCount: 100)] int[] temperaturesInC);
}

IWeatherAPIClientTelemetry (SampleApp.Web.Clients) — telemetry for the Blazor frontend's typed HttpClient. Registered via builder.Services.AddWeatherAPIClientTelemetry() and injected into WeatherAPIClient. Uses [ExcludeTargets] to prevent parameters appearing in specific telemetry targets, and [ExpandEnumerable] to log response items individually:

[ActivitySource]
[Logger]
[Meter(InstrumentPrefix = "weather")]
public interface IWeatherAPIClientTelemetry
{
    // MULTI-TARGET: starts Activity (Client kind) + logs Info + increments counter
    [Activity(ActivityKind.Client)]
    [Info]
    [AutoCounter]
    Activity? GetWeatherForecasts(int? count);

    // MULTI-TARGET: adds ActivityEvent + logs Error + increments counter
    // count excluded from Activities (Exception already carries the context)
    [Event]
    [Error]
    [AutoCounter]
    void FailedToGetForecast(Activity? activity, Exception ex,
        [ExcludeTargets(Targets.Activities)] int? count);

    // SINGLE-TARGET: adds ActivityEvent with HTTP status details
    [Event]
    void RequestComplete(Activity? activity, HttpStatusCode statusCode, bool isSuccessStatusCode);

    // SINGLE-TARGET: increments success counter
    [AutoCounter]
    void RequestSuccess();

    // MULTI-TARGET: adds ActivityEvent + logs Warning
    [Event]
    [Warning]
    void NoForecastsRecieved(Activity? activity);

    // MULTI-TARGET: adds ActivityEvent with Ok status + logs Debug
    // forecasts excluded from Activities (large enumerable, not suitable for tags)
    [Event(ActivityStatusCode.Ok)]
    [Debug]
    void ForecastsRecieved(Activity? activity, int forecastCount,
        [ExpandEnumerable(100), ExcludeTargets(Targets.Activities)] WeatherForecast[] weatherForecasts);
}

Request Flow

When the user clicks Load Weather in the Blazor frontend, telemetry is generated at two hops — once in SampleApp.Web as the HTTP client call is made, and once in SampleApp.APIService as the request is handled.

Happy path

sequenceDiagram
    actor User
    participant Web as SampleApp.Web
    participant CT as IWeatherAPIClientTelemetry
    participant API as SampleApp.APIService
    participant ST as IWeatherServiceTelemetry

    User->>Web: Load Weather (count=10)
    activate Web
    Web->>CT: GetWeatherForecasts(count)
    Note over CT: Activity started (Client)<br/>Info logged · Counter++

    Web->>+API: GET /weatherforecast/10

    API->>ST: GettingWeatherForecast(guid, count)
    Note over ST: Activity started (Client)<br/>Trace logged

    loop for each forecast
        API->>ST: HistogramOfTemperature(tempC)
    end

    API->>ST: ForecastReceived(activity, min, max)
    Note over ST: ActivityEvent added

    alt min < -10°C
        API->>ST: ItsTooCold(activity, min, coldCount)
        Note over ST: ActivityEvent · Warning logged · Counter++
    else temperatures within range
        API->>ST: TemperaturesWithinRange(temps[])
        Note over ST: Info logged (array expanded)
    end

    API->>ST: TemperaturesReceived(activity, elapsed)
    Note over ST: ActivityEvent (Ok status)

    API-->>-Web: 200 OK WeatherForecast[10]

    Web->>CT: RequestComplete(activity, 200, true)
    Note over CT: ActivityEvent added
    Web->>CT: RequestSuccess()
    Note over CT: Counter++
    Web->>CT: ForecastsRecieved(activity, 10, forecasts)
    Note over CT: ActivityEvent (Ok) · Debug logged

    Web-->>User: display forecasts
    deactivate Web
Loading

Error and failure paths

sequenceDiagram
    actor User
    participant Web as SampleApp.Web
    participant CT as IWeatherAPIClientTelemetry
    participant API as SampleApp.APIService
    participant ST as IWeatherServiceTelemetry

    Note over User,ST: Validation failure — count out of range (e.g. count=1 or count=25)

    User->>Web: Load Weather (count=1)
    activate Web
    Web->>CT: GetWeatherForecasts(count)
    Note over CT: Activity started · Info logged · Counter++
    Web->>+API: GET /weatherforecast/1
    API->>ST: RequestedCountIsOutOfRange(1)
    Note over ST: Error logged · Counter++
    API-->>-Web: 422 Validation Error
    Web->>CT: RequestComplete(activity, 422, false)
    Web->>CT: FailedToGetForecast(activity, ex, count)
    Note over CT: ActivityEvent · Error logged · Counter++
    Web-->>User: display error message
    deactivate Web

    Note over User,ST: Simulated upstream failure (random ~10% of valid requests)

    User->>Web: Load Weather (count=10)
    activate Web
    Web->>CT: GetWeatherForecasts(count)
    Web->>+API: GET /weatherforecast/10
    API->>ST: GettingWeatherForecast(guid, count)
    Note over ST: Activity started · Trace logged
    API->>ST: FailedToRetrieveForecast(activity, ex)
    Note over ST: ActivityEvent (Error status) · Critical logged
    API-->>-Web: 502 Problem Details
    Web->>CT: RequestComplete(activity, 502, false)
    Web->>CT: FailedToGetForecast(activity, ex, count)
    Note over CT: ActivityEvent · Error logged · Counter++
    Web-->>User: display error message
    deactivate Web
Loading

Unit Testing

The SampleApp.APIService.UnitTests project demonstrates how to test code that depends on generated telemetry. Using TUnit + NSubstitute, you mock the interface and verify calls:

// Create a mock with NSubstitute
static IWeatherServiceTelemetry CreateTelemetry() =>
    Substitute.For<IWeatherServiceTelemetry>();

// Configure a method return value
telemetry.GettingWeatherForecast(Arg.Any<string>(), requestCount)
         .Returns(activity);

// Verify a call was made exactly once
telemetry.Received(1).FailedToRetrieveForecast(Arg.Is(activity), Arg.Any<Exception>());

Tests are split across:

  • WeatherServiceTests.Success.cs — happy path (valid count, results returned)
  • WeatherServiceTests.Failure.cs — simulated upstream exception
  • WeatherServiceTests.Validation.cs — invalid requestCount range

Running the Dashboard and Viewing Telemetry

  1. Open the solution (samples/SampleApp/SampleApp.slnx) and run the SampleApp.AppHost project.
  2. The .NET Aspire dashboard opens automatically. It lists three running resources: api-service, web, and scalar.
  3. To generate telemetry, use either approach:
    • Web Frontend — click the web resource endpoint, navigate to the Weather page, and click Load Weather (or use the error scenario buttons).
    • Scalar API Docs — click the scalar resource endpoint to open the Scalar UI and call the API directly.

Step-by-step walkthrough

Step Image
Open the solution and run SampleApp.AppHost. The Aspire dashboard shows all three running resources — api-service, web, and scalar — with status indicators and endpoint links. .NET Aspire Dashboard showing api-service, web, and scalar resources
Navigate to the web endpoint to open the Blazor frontend. Go to the Weather page. Use the count input and Load Weather button, or use the error scenario buttons to trigger failure paths. Blazor frontend Weather page
Alternatively, click the scalar endpoint to open the Scalar API docs. Expand the Weather APIs group and click the endpoint you want to test. Scalar API reference with weatherforecast endpoint expanded
Press the Send (▶ Play) button several times to generate telemetry. Use an out-of-range count (e.g. 1 or 25) to trigger validation failures. Scalar Send button ready to fire a request

Telemetry in the Aspire dashboard

After generating some traffic, explore each tab in the Aspire dashboard:

Dashboard view Image
Resources — failed requests surface immediately as error badges on api-service and/or web. Failures visible in the Aspire resource list
Console Logs — raw stdout output per service, including startup messages and per-request lines. Console logs view
Structured Logs — log entries with Level, Message, and all structured properties (e.g. RequestedCount, MinTempInC). Structured logs view
Traces — distributed traces spanning both web and api-service hops, with full end-to-end duration. Traces view
Trace detail — Activity span hierarchy with ActivityEvents (ForecastReceived, TemperaturesReceived), baggage, and tag properties. Activity and ActivityEvent details
Metrics — generated counters and histograms from both services: getting-weather-forecast, its-too-cold, histogram-of-temperature, get-weather-forecasts, etc. Metrics dashboard view
Histogram detailhistogram-of-temperature bucket distribution and percentiles (P50, P95, P99). Metrics histogram view

Viewing Generated Code

Both projects have <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> enabled, so generated files appear in your IDE.

SampleApp.APIService generates files for both interfaces:

SampleApp.APIService/obj/Release/net10.0/generated/
  Purview.Telemetry.SourceGenerator/
    Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/
      EntityStoreTelemetryCore.Activity.g.cs
      EntityStoreTelemetryCore.Logging.g.cs
      EntityStoreTelemetryCore.Metric.g.cs
      EntityStoreTelemetryCoreDIExtension.DependencyInjection.g.cs
      SampleApp.APIService.Services.WeatherServiceTelemetryCore.Activity.g.cs
      SampleApp.APIService.Services.WeatherServiceTelemetryCore.Logging.g.cs
      SampleApp.APIService.Services.WeatherServiceTelemetryCore.Metric.g.cs
      SampleApp.APIService.Services.WeatherServiceTelemetryCoreDIExtension.DependencyInjection.g.cs
      SampleApp.APIService.TelemetryNames.g.cs

SampleApp.Web generates files for IWeatherAPIClientTelemetry:

SampleApp.Web/obj/Release/net10.0/generated/
  Purview.Telemetry.SourceGenerator/
    Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/
      SampleApp.Web.Clients.WeatherAPIClientTelemetryCore.Activity.g.cs
      SampleApp.Web.Clients.WeatherAPIClientTelemetryCore.Logging.g.cs
      SampleApp.Web.Clients.WeatherAPIClientTelemetryCore.Metric.g.cs
      SampleApp.Web.Clients.WeatherAPIClientTelemetryCoreDIExtension.DependencyInjection.g.cs
      SampleApp.Web.TelemetryNames.g.cs

The TelemetryNames.g.cs files expose static arrays of all meter and activity source names, used in Program.cs to register them with OpenTelemetry:

builder.AddServiceDefaults(TelemetryNames.MeterNames, TelemetryNames.ActivitySourceNames);

See Generated Output for full annotated examples of the generated code.

Clone this wiki locally