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\n.NET Aspire Orchestrator"]

    subgraph running["Running Services"]
        Web["SampleApp.Web\nBlazor Server Frontend"]
        API["SampleApp.APIService\nWeather REST API"]
        Scalar["scalar\nAPI Docs"]
    end

    subgraph libs["Shared Libraries"]
        Defaults["SampleApp.ServiceDefaults\nOpenTelemetry · Health · Resilience"]
        SharedLib["SampleApp.Shared\nWeatherForecast 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

📸 [Screenshot needed — AppHostEndpointView] The Aspire dashboard Resources list showing api-service, web, and scalar with running status, health indicators, and endpoint links in the Endpoint column. Replace ./assets/AppHostEndpointView.png.

Navigate to the web endpoint to open the Blazor frontend, or click the scalar endpoint for direct API access.

📸 [Screenshot needed — WebFrontendWeatherPage] The Blazor frontend Weather page showing the "Load Weather" button, the forecast count input, and the error scenario buttons ("Trigger Validation Error", "Trigger Large Count Error"). This is a new screenshot — save as ./assets/WebFrontendWeatherPage.png.

In the Scalar UI, expand the Weather APIs group and click the endpoint you want to test.

📸 [Screenshot needed — ScalarEndpointView] The Scalar API reference showing the /weatherforecast/{requestCount} endpoint expanded with the Test Request panel visible. Replace ./assets/ScalarEndpointView.png.

Click the Send (▶ Play) button several times to generate telemetry data. Use the Blazor error scenario buttons or Scalar with an out-of-range count (e.g. 1 or 25) to see failure paths.

📸 [Screenshot needed — ScalarPlayButton] The Scalar Send/Play button highlighted, ready to fire a request. Replace ./assets/ScalarPlayButton.png and ./assets/ScalarPlayButtonScreenShot.png (the small inline icon).

Telemetry in the Aspire dashboard

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

📸 [Screenshot needed — AspireProjectListFailure] The Aspire dashboard Resources list after triggering failures — red error badge on api-service and/or web, showing that errors are surfaced immediately at the resource level. Replace ./assets/AspireProjectListFailure.png.

📸 [Screenshot needed — AspireConsoleLogsView] The Console Logs view for api-service, showing raw stdout output including startup messages and per-request log lines. Replace ./assets/AspireConsoleLogsView.png.

📸 [Screenshot needed — AspireStructuredLogsView] The Structured Logs view for api-service, showing a table of log entries with Level, Message, and property columns (e.g. RequestedCount, MinTempInC). Replace ./assets/AspireStructuredLogsView.png.

📸 [Screenshot needed — AspireTracesView] The Traces view showing a list of distributed traces. Each row spans both web and api-service hops, showing the full end-to-end duration. Replace ./assets/AspireTracesView.png.

📸 [Screenshot needed — AspireTracesWithEvents] A single trace expanded to show the Activity span hierarchy: the GetWeatherForecasts client span from web wrapping the GettingWeatherForecast span from api-service, with ActivityEvents (ForecastReceived, TemperaturesReceived) and baggage/tag properties visible. Replace ./assets/AspireTracesWithEvents.png.

📸 [Screenshot needed — ApsireMetricsView] The Metrics view showing the generated counters and histograms: getting-weather-forecast, its-too-cold, requested-count-is-out-of-range, and histogram-of-temperature from SampleApp.APIService.Services, plus get-weather-forecasts, failed-to-get-forecast, request-success from SampleApp.Web.Clients. Replace ./assets/ApsireMetricsView.png.

📸 [Screenshot needed — AspireMetricsHistogramView] The histogram-of-temperature histogram expanded to show bucket distribution and percentiles (P50, P95, P99). Replace ./assets/AspireMetricsHistogramView.png.


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