-
Notifications
You must be signed in to change notification settings - Fork 65
Add Blazor WASM telemetry docs and post-deployment observability guidance #455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
11
commits into
main
Choose a base branch
from
copilot/add-telemetry-dashboard-documentation
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
28889dc
Initial plan
Copilot 23efef3
Add Blazor WASM telemetry section and post-deployment telemetry docs
Copilot a88f5f8
Fix doc-tester issues: remove fictional API, fix package attribution,…
Copilot a44b510
Apply suggestions from code review
IEvangelist 2fdf287
Apply @alistairmatthews review suggestions: wording, proxy fix, and p…
Copilot 3aa68ca
Specified trivy.v0.35.0 to resolve security scan check failure.
alistairmatthews aa6e085
Resolved conflict by removing security-scan.yml.
alistairmatthews aaa9d61
Update src/frontend/src/content/docs/dashboard/enable-browser-telemet…
alistairmatthews b5e26e6
Update src/frontend/src/content/docs/dashboard/telemetry-after-deploy…
alistairmatthews 2edce45
Split the browser telemetry doc into three, implementing feedback fro…
alistairmatthews a04eb99
Resolved conflict.
alistairmatthews File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
...tend/src/content/docs/dashboard/enable-browser-telemetry/blazor-webassembly.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| --- | ||
| title: Blazor WebAssembly integration | ||
| description: Learn how to integrate browser telemetry in a Blazor WebAssembly app using JavaScript interop. | ||
| --- | ||
|
|
||
| import LearnMore from '@components/LearnMore.astro'; | ||
| import { Aside } from '@astrojs/starlight/components'; | ||
|
|
||
| Blazor WebAssembly (WASM) apps run entirely in the browser using a .NET runtime compiled to WebAssembly. Like other browser apps, Blazor WASM apps use the [JavaScript OTEL SDK](https://opentelemetry.io/docs/languages/js/getting-started/browser/) to send telemetry to the Aspire dashboard via JavaScript interop. The dashboard configuration for [OTLP HTTP](/dashboard/enable-browser-telemetry/#otlp-configuration) and [CORS](/dashboard/enable-browser-telemetry/#cors-configuration) described in [Enable browser telemetry](/dashboard/enable-browser-telemetry/) applies to Blazor WASM apps as well. | ||
|
|
||
| ## Provide OTEL configuration to the browser | ||
|
|
||
| Blazor WASM apps can't read server-side environment variables directly. When the app is hosted by an ASP.NET Core server (for example, a Blazor Web App or hosted Blazor WASM project), expose the OTEL configuration through an API endpoint on the server: | ||
|
|
||
| ```csharp title="C# — Program.cs (Server)" | ||
| using Microsoft.AspNetCore.Authorization; | ||
|
|
||
| app.MapGet("/api/telemetry-config", [Authorize] (IConfiguration config) => new | ||
| { | ||
| Endpoint = config.GetValue<string>("OTEL_EXPORTER_OTLP_ENDPOINT", ""), | ||
| Headers = config.GetValue<string>("OTEL_EXPORTER_OTLP_HEADERS", ""), | ||
| ResourceAttributes = config.GetValue<string>("OTEL_RESOURCE_ATTRIBUTES", "") | ||
| }); | ||
| ``` | ||
|
|
||
| <Aside type="caution"> | ||
| This approach exposes the OTLP API key to the browser in the HTTP response. For environments where the API key must remain secret, use an [authenticated OTEL proxy](#authenticated-otel-proxy) instead. In production, consider adding authentication or authorization to restrict access to this endpoint. | ||
| </Aside> | ||
|
|
||
| ## Initialize OTEL from a Blazor component | ||
|
|
||
| In the Blazor WASM app, call the JavaScript `initializeTelemetry` function using `IJSRuntime` after the component has rendered. Add the [JavaScript OTEL SDK initialization code](/dashboard/enable-browser-telemetry/browser-app-configuration/#example-browser-telemetry-code) to a JavaScript file bundled with your app, then call it from a Razor component: | ||
|
|
||
| ```razor title="Razor — TelemetryInitializer.razor (Client)" | ||
| @inject IJSRuntime JS | ||
| @inject HttpClient Http | ||
|
|
||
| @code { | ||
| protected override async Task OnAfterRenderAsync(bool firstRender) | ||
| { | ||
| if (firstRender) | ||
| { | ||
| var config = await Http.GetFromJsonAsync<TelemetryConfig>("/api/telemetry-config"); | ||
| if (config is { Endpoint.Length: > 0 }) | ||
| { | ||
| await JS.InvokeVoidAsync( | ||
| "initializeTelemetry", | ||
| config.Endpoint, | ||
| config.Headers, | ||
| config.ResourceAttributes); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private sealed record TelemetryConfig( | ||
| string Endpoint, | ||
| string Headers, | ||
| string ResourceAttributes); | ||
| } | ||
| ``` | ||
|
|
||
| Include this component in your `App.razor` or another top-level component so that telemetry is initialized when the app loads. | ||
|
|
||
| ## Backend-to-frontend trace correlation | ||
|
|
||
| To correlate browser spans with server-side traces, include the current trace context in the server-rendered HTML. When the Blazor WASM app is hosted within a server-rendered page, such as a Blazor Web App with prerendering, the server can write the `traceparent` meta tag during prerender: | ||
|
|
||
| ```razor title="Razor — App.razor (Server prerender)" | ||
| @using System.Diagnostics | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| @if (Activity.Current is { } currentActivity) | ||
| { | ||
| <meta name="traceparent" content="@currentActivity.Id" /> | ||
| } | ||
| <!-- Other elements omitted for brevity... --> | ||
| </head> | ||
| ``` | ||
|
|
||
| The JavaScript OTEL SDK reads this `traceparent` value automatically when `DocumentLoadInstrumentation` is registered, linking the browser spans to the originating server trace. | ||
|
|
||
| ## Authenticated OTEL proxy | ||
|
|
||
| When the Aspire dashboard's OTLP API key must not be exposed to the browser, route telemetry through a server-side proxy endpoint. The browser sends telemetry to the proxy, and the proxy forwards it to the dashboard with the API key included as a server-side secret: | ||
|
|
||
| ```csharp title="C# — Program.cs (Server proxy endpoint)" | ||
| // Register an HttpClient for the OTEL proxy | ||
| builder.Services.AddHttpClient("otel-proxy"); | ||
|
|
||
| // ... | ||
|
|
||
| app.MapPost("/api/telemetry/{**path}", async ( | ||
| string path, | ||
| HttpContext context, | ||
| IHttpClientFactory httpClientFactory) => | ||
| { | ||
| var dashboardEndpoint = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT"); | ||
| if (string.IsNullOrEmpty(dashboardEndpoint)) | ||
| { | ||
| return Results.NotFound(); | ||
| } | ||
|
|
||
| var client = httpClientFactory.CreateClient("otel-proxy"); | ||
|
|
||
| using var requestBody = new StreamContent(context.Request.Body); | ||
| requestBody.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue | ||
| .Parse(context.Request.ContentType ?? "application/x-protobuf"); | ||
|
|
||
| using var request = new HttpRequestMessage( | ||
| HttpMethod.Post, | ||
| $"{dashboardEndpoint.TrimEnd('/')}/{path}") | ||
| { | ||
| Content = requestBody | ||
| }; | ||
|
|
||
| // Copy OTLP API key from server environment to the forwarded request | ||
| var headersEnv = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_HEADERS") ?? string.Empty; | ||
| foreach (var header in headersEnv.Split(',', StringSplitOptions.RemoveEmptyEntries)) | ||
| { | ||
| var parts = header.Split('=', 2); | ||
| if (parts.Length == 2) | ||
| { | ||
| request.Headers.TryAddWithoutValidation(parts[0].Trim(), parts[1].Trim()); | ||
| } | ||
| } | ||
|
|
||
| var response = await client.SendAsync(request); | ||
|
|
||
| context.Response.StatusCode = (int)response.StatusCode; | ||
| await response.Content.CopyToAsync(context.Response.Body); | ||
| return Results.Empty; | ||
| }); | ||
| ``` | ||
|
|
||
| Configure the JavaScript OTEL SDK in the Blazor WASM app to point to the proxy endpoint instead of the dashboard directly: | ||
|
|
||
| ```javascript title="JavaScript — telemetry.js (Client)" | ||
| export function initializeTelemetry(resourceAttributes) { | ||
| const otlpOptions = { | ||
| url: '/api/telemetry/v1/traces' // Proxy endpoint, same origin - no CORS needed | ||
| }; | ||
| // ... rest of SDK initialization | ||
| } | ||
| ``` | ||
|
|
||
| This pattern eliminates the need for CORS configuration on the dashboard because the browser communicates only with the same-origin server. The API key stays on the server and is never visible to the browser. | ||
|
|
||
| ## See also | ||
|
|
||
| - [Enable browser telemetry overview](/dashboard/enable-browser-telemetry/) | ||
| - [Browser app configuration](/dashboard/enable-browser-telemetry/browser-app-configuration/) | ||
| - [Aspire dashboard configuration](/dashboard/configuration/) | ||
| - [Standalone Aspire dashboard](/dashboard/standalone/) | ||
| - [Telemetry after deployment](/dashboard/telemetry-after-deployment/) |
152 changes: 152 additions & 0 deletions
152
...c/content/docs/dashboard/enable-browser-telemetry/browser-app-configuration.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| --- | ||
| title: Browser app configuration | ||
| description: Learn how to configure the JavaScript OTEL SDK in your browser app to send telemetry to the Aspire dashboard. | ||
| --- | ||
|
|
||
| import LearnMore from '@components/LearnMore.astro'; | ||
| import { Aside } from '@astrojs/starlight/components'; | ||
|
|
||
| A browser app uses the [JavaScript OTEL SDK](https://opentelemetry.io/docs/languages/js/getting-started/browser/) to send telemetry to the dashboard. Successfully sending telemetry to the dashboard requires the SDK to be correctly configured. | ||
|
|
||
| Before configuring the browser app, ensure the dashboard is configured with an [OTLP HTTP endpoint and CORS](/dashboard/enable-browser-telemetry/). For Blazor WebAssembly apps, see [Blazor WebAssembly integration](/dashboard/enable-browser-telemetry/blazor-webassembly/). | ||
|
|
||
| ## OTLP exporter | ||
|
|
||
| OTLP exporters must be included in the browser app and configured with the SDK. For example, exporting distributed tracing with OTLP uses the [@opentelemetry/exporter-trace-otlp-proto](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-proto) package. | ||
|
|
||
| When OTLP is added to the SDK, OTLP options must be specified. OTLP options includes: | ||
|
|
||
| - `url`: The address that HTTP OTLP requests are made to. The address should be the dashboard HTTP OTLP endpoint and the path to the OTLP HTTP API. For example, `https://localhost:4318/v1/traces` for the trace OTLP exporter. If the browser app is launched by the AppHost then the HTTP OTLP endpoint is available from the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable. | ||
|
|
||
| - `headers`: The headers sent with requests. If OTLP endpoint API key authentication is enabled the `x-otlp-api-key` header must be sent with OTLP requests. If the browser app is launched by the AppHost then the API key is available from the `OTEL_EXPORTER_OTLP_HEADERS` environment variable. | ||
|
|
||
| ## Browser metadata | ||
|
|
||
| When a browser app is configured to collect distributed traces, the browser app can set the trace parent a browser's spans using the `meta` element in the HTML. The value of the `name="traceparent"` meta element should correspond to the current trace. | ||
|
|
||
| In a .NET app, for example, the trace parent value would likely be assigned from the `Activity.Current` and passing its `Activity.Id` value as the `content`. For example, consider the following Razor code: | ||
|
|
||
| ```razor | ||
| <head> | ||
| @if (Activity.Current is { } currentActivity) | ||
| { | ||
| <meta name="traceparent" content="@currentActivity.Id" /> | ||
| } | ||
| <!-- Other elements omitted for brevity... --> | ||
| </head> | ||
| ``` | ||
|
|
||
| The preceding code sets the `traceparent` meta element to the current activity ID. | ||
|
|
||
| ## Example browser telemetry code | ||
|
|
||
| The following JavaScript code demonstrates the initialization of the OpenTelemetry JavaScript SDK and the sending of telemetry data to the dashboard: | ||
|
|
||
| ```javascript | ||
| import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; | ||
| import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; | ||
| import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'; | ||
| import { registerInstrumentations } from '@opentelemetry/instrumentation'; | ||
| import { resourceFromAttributes } from '@opentelemetry/resources'; | ||
| import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; | ||
| import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; | ||
| import { ZoneContextManager } from '@opentelemetry/context-zone'; | ||
|
|
||
| export function initializeTelemetry(otlpUrl, headers, resourceAttributes) { | ||
| const otlpOptions = { | ||
| url: `${otlpUrl}/v1/traces`, | ||
| headers: parseDelimitedValues(headers) | ||
| }; | ||
|
|
||
| const attributes = parseDelimitedValues(resourceAttributes); | ||
| attributes[SemanticResourceAttributes.SERVICE_NAME] = 'browser'; | ||
|
|
||
| const provider = new WebTracerProvider({ | ||
| resource: resourceFromAttributes(attributes), | ||
| }); | ||
| provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); | ||
| provider.addSpanProcessor(new SimpleSpanProcessor(new OTLPTraceExporter(otlpOptions))); | ||
|
|
||
| provider.register({ | ||
| // Prefer ZoneContextManager: supports asynchronous operations | ||
| contextManager: new ZoneContextManager(), | ||
| }); | ||
|
|
||
| // Registering instrumentations | ||
| registerInstrumentations({ | ||
| instrumentations: [new DocumentLoadInstrumentation()], | ||
| }); | ||
| } | ||
|
|
||
| function parseDelimitedValues(s) { | ||
| const headers = s.split(','); // Split by comma | ||
| const result = {}; | ||
|
|
||
| headers.forEach(header => { | ||
| const [key, value] = header.split('='); // Split by equal sign | ||
| result[key.trim()] = value.trim(); // Add to the object, trimming spaces | ||
| }); | ||
|
|
||
| return result; | ||
| } | ||
| ``` | ||
|
|
||
| The preceding JavaScript code defines an `initializeTelemetry` function that expects the OTLP endpoint URL, the headers, and the resource attributes. These parameters are provided by the consuming browser app that pulls them from the environment variables set by the app host. Consider the following Razor code: | ||
|
|
||
| ```razor {32-39} | ||
| @using System.Diagnostics | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>@ViewData["Title"] - BrowserTelemetry</title> | ||
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" | ||
| integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> | ||
| <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> | ||
|
|
||
| @if (Activity.Current is { } currentActivity) | ||
| { | ||
| <meta name="traceparent" content="@currentActivity.Id" /> | ||
| } | ||
| </head> | ||
| <body> | ||
| <header> | ||
| <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> | ||
| <div class="container"> | ||
| <a class="navbar-brand" asp-area="" asp-page="/Index">BrowserTelemetry</a> | ||
| </div> | ||
| </nav> | ||
| </header> | ||
| <div class="container"> | ||
| <main role="main" class="pb-3"> | ||
| @RenderBody() | ||
| </main> | ||
| </div> | ||
| @await RenderSectionAsync("Scripts", required: false) | ||
| <script src="scripts/bundle.js"></script> | ||
| @if (Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_ENDPOINT") is { Length: > 0 } endpointUrl) | ||
| { | ||
| var headers = Environment.GetEnvironmentVariable("OTEL_EXPORTER_OTLP_HEADERS"); | ||
| var attributes = Environment.GetEnvironmentVariable("OTEL_RESOURCE_ATTRIBUTES"); | ||
| <script> | ||
| BrowserTelemetry.initializeTelemetry('@endpointUrl', '@headers', '@attributes'); | ||
| </script> | ||
| } | ||
| </body> | ||
| </html> | ||
| ``` | ||
|
|
||
| <Aside type="tip"> | ||
| The bundling and minification of the JavaScript code is beyond the scope of this article. | ||
| </Aside> | ||
|
|
||
| For the complete working example of how to configure the JavaScript OTEL SDK to send telemetry to the dashboard, see the [browser telemetry sample](https://github.com/microsoft/aspire/tree/main/playground/BrowserTelemetry). | ||
|
|
||
| ## See also | ||
|
|
||
| - [Enable browser telemetry overview](/dashboard/enable-browser-telemetry/) | ||
| - [Blazor WebAssembly integration](/dashboard/enable-browser-telemetry/blazor-webassembly/) | ||
| - [Aspire dashboard configuration](/dashboard/configuration/) | ||
| - [Standalone Aspire dashboard](/dashboard/standalone/) | ||
| - [Telemetry after deployment](/dashboard/telemetry-after-deployment/) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.