diff --git a/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor b/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor index 76eb72528..12ae00716 100644 --- a/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor +++ b/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor @@ -1,16 +1,29 @@ -@inherits LayoutComponentBase -
- diff --git a/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor.css b/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor.css index ecf25e5b2..6071dacd7 100644 --- a/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor.css +++ b/samples/Metrics/MetricsApp.Client/Layout/MainLayout.razor.css @@ -1,77 +1,64 @@ -.page { - position: relative; +.console { + min-height: 100vh; display: flex; flex-direction: column; } -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; +.appbar { display: flex; align-items: center; + gap: 1rem 1.25rem; + flex-wrap: wrap; + padding: 0.7rem clamp(1rem, 4vw, 2.5rem); + background: var(--surface); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 20; + box-shadow: var(--shadow); } - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } - - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } - - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } - -@media (max-width: 640.98px) { - .top-row { - justify-content: space-between; - } - - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } +.brand { + display: inline-flex; + align-items: center; + gap: 0.65rem; + text-decoration: none; + color: var(--ink); } +.brand:hover { text-decoration: none; } -@media (min-width: 641px) { - .page { - flex-direction: row; - } +.brand__mark { + width: 2.15rem; + height: 2.15rem; + display: grid; + place-items: center; + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + border-radius: 11px; + color: #fff; + box-shadow: var(--shadow); +} +.brand__mark svg { width: 1.35rem; height: 1.35rem; color: #c7f9ff; } - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } +.brand__text { display: flex; flex-direction: column; line-height: 1.12; } +.brand__name { font-weight: 700; font-size: 1.04rem; letter-spacing: -0.01em; } +.brand__sub { + font-family: var(--font-mono); + font-size: 0.66rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-muted); +} - .top-row { - position: sticky; - top: 0; - z-index: 1; - } +.appbar__status { margin-left: auto; } - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } +.content { + flex: 1; + width: 100%; + max-width: 72rem; + margin: 0 auto; + padding: clamp(1.5rem, 4vw, 2.75rem) clamp(1rem, 4vw, 2.5rem) 3.5rem; +} - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } +@media (max-width: 640px) { + .appbar__status { margin-left: 0; } } diff --git a/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor b/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor index 5012a88de..6b0ba783b 100644 --- a/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor +++ b/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor @@ -1,39 +1,25 @@ - - - - -@code { - private bool collapseNavMenu = true; - - private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; - - private void ToggleNavMenu() - { - collapseNavMenu = !collapseNavMenu; - } -} +@* + Primary navigation rendered as horizontal "console tabs" in the app bar. + Each page deliberately exercises a different instrument so the sample + produces a spread of OpenTelemetry metrics. +*@ + diff --git a/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor.css b/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor.css index 6ae2b10fc..a9d6f3220 100644 --- a/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor.css +++ b/samples/Metrics/MetricsApp.Client/Layout/NavMenu.razor.css @@ -1,87 +1,33 @@ -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); +.topnav { + display: flex; + gap: 0.35rem; + flex-wrap: wrap; + align-items: center; } -.top-row { - height: 3.5rem; - background-color: rgba(0,0,0,0.4); +.topnav__link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.85rem; + border-radius: 999px; + color: var(--ink-soft); + text-decoration: none; + font-weight: 600; + font-size: 0.92rem; + border: 1px solid transparent; + transition: background-color 0.12s ease, color 0.12s ease, border-color 0.12s ease; } -.navbar-brand { - font-size: 1.1rem; +.topnav__link:hover { + background: var(--surface-2); + color: var(--ink); + text-decoration: none; } -.bi { - display: inline-block; - position: relative; - width: 1.25rem; - height: 1.25rem; - margin-right: 0.75rem; - top: -1px; - background-size: cover; +.topnav__link.active { + background: var(--accent); + color: var(--accent-ink); } -.bi-house-door-fill-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); -} - -.bi-plus-square-fill-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); -} - -.bi-list-nested-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); -} - -.bi-lock-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); -} - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item ::deep a { - color: #d7d7d7; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - } - -.nav-item ::deep a.active { - background-color: rgba(255,255,255,0.37); - color: white; -} - -.nav-item ::deep a:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} - -@media (min-width: 641px) { - .navbar-toggler { - display: none; - } - - .collapse { - /* Never collapse the sidebar for wide screens */ - display: block; - } - - .nav-scrollable { - /* Allow sidebar to scroll for tall menus */ - height: calc(100vh - 3.5rem); - overflow-y: auto; - } -} +.topnav__link .icon { width: 1.05rem; height: 1.05rem; } diff --git a/samples/Metrics/MetricsApp.Client/Pages/Auth.razor b/samples/Metrics/MetricsApp.Client/Pages/Auth.razor index 89a4081c4..3faf2f088 100644 --- a/samples/Metrics/MetricsApp.Client/Pages/Auth.razor +++ b/samples/Metrics/MetricsApp.Client/Pages/Auth.razor @@ -1,51 +1,65 @@ -@page "/auth" +@page "/auth" @using System.Text.Json.Nodes @using System.ComponentModel.DataAnnotations @inject HttpClient Http @inject IdentityAuthenticationStateProvider AuthStateProvider -Log in - -

Log in

- - - -

Hello, @context.User.Identity?.Name!

- -
- -
-
-
- - - -
- -
- - - -
-
- - - -
-
- -
-
-
- Email: @TestEmail, Password: @TestPassword -
-
-
-
-
-
-
+Telemetry Console · Log in +
+

Protected endpoint

+

Log in

+ + + +

+ Signed in as @context.User.Identity?.Name. The + Weather feed now returns authorized data. +

+ +
+ +

+ Sign in to call the protected api/weather endpoint and + generate authenticated-request metrics. +

+ + + + + + + +
+ + + +
+ +
+ + + +
+ + +
+ + +
+
+
@code { private const string TestEmail = "test@contoso.com"; @@ -67,7 +81,8 @@ public async Task LoginUser() { - var response = await Http.PostAsJsonAsync("/api/login", new { email = Input.Email, password = Input.Password }, CancellationToken.None); + var response = await Http.PostAsJsonAsync("/api/login", new { email = Input.Email, password = Input.Password }, + CancellationToken.None); if (response.IsSuccessStatusCode) { diff --git a/samples/Metrics/MetricsApp.Client/Pages/Auth.razor.css b/samples/Metrics/MetricsApp.Client/Pages/Auth.razor.css new file mode 100644 index 000000000..7e9b4b90f --- /dev/null +++ b/samples/Metrics/MetricsApp.Client/Pages/Auth.razor.css @@ -0,0 +1,21 @@ +.auth { + max-width: 30rem; + padding: clamp(1.4rem, 4vw, 2.2rem); +} + +.auth h1 { margin: 0.35rem 0 0.75rem; } +.auth__lede { margin: 0 0 1.25rem; color: var(--ink-soft); } +.auth__hello { color: var(--ink-soft); margin: 0.5rem 0 1.25rem; } + +.demo-creds { + margin-top: 1.5rem; + padding: 1rem 1.1rem; + border: 1px dashed var(--border-strong); + border-radius: var(--radius-sm); + background: var(--surface); +} +.demo-creds .eyebrow { display: block; margin-bottom: 0.5rem; } +.demo-creds dl { margin: 0; display: grid; gap: 0.3rem; } +.demo-creds dl > div { display: flex; gap: 0.6rem; } +.demo-creds dt { color: var(--ink-soft); min-width: 4.5rem; } +.demo-creds dd { margin: 0; color: var(--ink); font-weight: 600; } diff --git a/samples/Metrics/MetricsApp.Client/Pages/Home.razor b/samples/Metrics/MetricsApp.Client/Pages/Home.razor index 03c5af4ce..45431c259 100644 --- a/samples/Metrics/MetricsApp.Client/Pages/Home.razor +++ b/samples/Metrics/MetricsApp.Client/Pages/Home.razor @@ -1,16 +1,70 @@ -@page "/" +@page "/" @inject HttpClient Http -Home +Telemetry Console · Home -

Hello, metrics!

- -@if (_grafanaUrl != null) -{ -

- View this app's Grafana dashboard at @_grafanaUrl. +

+

Instrumented workload

+

Generate live metrics

+

+ This app is wired up with OpenTelemetry. As you move around it, + it records instruments such as http.server.request.duration and streams + them through Aspire to Prometheus and Grafana for live dashboards.

-} + +
+ @if (_grafanaUrl is not null) + { + + + Open Grafana dashboard + + @_grafanaUrl + } + else + { + + + Grafana dashboard + + The dashboard link appears here when the app runs under Aspire. + } +
+
+ +
+
+ +

Weather feed

+

+ Fetch forecasts — complete with a little random latency and the occasional fault — + to populate request-duration histograms and error counters. +

+ Open Weather +
+ +
+ +

Auth required

+

+ Sign in and call a protected endpoint to generate authentication and + authorized-request metrics alongside the public traffic. +

+ Open Auth +
+
@code { private static string? _grafanaUrl; diff --git a/samples/Metrics/MetricsApp.Client/Pages/Home.razor.css b/samples/Metrics/MetricsApp.Client/Pages/Home.razor.css new file mode 100644 index 000000000..18afe9751 --- /dev/null +++ b/samples/Metrics/MetricsApp.Client/Pages/Home.razor.css @@ -0,0 +1,67 @@ +.hero { + padding: clamp(1.5rem, 4vw, 2.4rem); +} + +.hero__title { + margin: 0.4rem 0 0.6rem; + font-size: clamp(1.9rem, 4vw, 2.6rem); +} + +.hero__lede { + margin: 0; + max-width: 60ch; + color: var(--ink-soft); + font-size: 1.05rem; +} + +.hero__cta { + margin-top: 1.5rem; + display: flex; + align-items: center; + gap: 0.75rem 1rem; + flex-wrap: wrap; +} + +.hero__hint { + color: var(--ink-muted); + font-size: 0.88rem; +} + +.cards { + margin-top: 1.5rem; + display: grid; + gap: 1.25rem; + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); +} + +.card { + padding: 1.4rem; + display: flex; + flex-direction: column; + transition: transform 0.14s ease, box-shadow 0.14s ease; +} +.card:hover { + transform: translateY(-3px); + box-shadow: var(--shadow-lift); +} + +.card__icon { + width: 2.6rem; + height: 2.6rem; + display: grid; + place-items: center; + border-radius: 12px; + background: var(--surface-inset); + color: var(--accent-strong); + border: 1px solid var(--border); +} +.card__icon svg { width: 1.4rem; height: 1.4rem; } + +.card__title { margin: 1rem 0 0.4rem; font-size: 1.2rem; } +.card__text { margin: 0; color: var(--ink-soft); } +.card__link { + margin-top: 1rem; + font-weight: 600; + text-decoration: none; +} +.card__link:hover { text-decoration: underline; } diff --git a/samples/Metrics/MetricsApp.Client/Pages/Weather.razor b/samples/Metrics/MetricsApp.Client/Pages/Weather.razor index 653520f51..68cc1754d 100644 --- a/samples/Metrics/MetricsApp.Client/Pages/Weather.razor +++ b/samples/Metrics/MetricsApp.Client/Pages/Weather.razor @@ -1,61 +1,76 @@ -@page "/weather" +@page "/weather" @using System.Net @inject HttpClient Http @implements IDisposable -Weather +Telemetry Console · Weather -

Weather

+
+
+
+

Request histograms

+

Weather feed

+

+ Each refresh calls api/weather on the server, feeding latency + and fault metrics back to the live dashboards. +

+
+
+ + +
+
-

This component demonstrates fetching data from the server.

- -
-
- -
-
- - +
+ @if (errorMessage == null) + { + @if (forecasts == null) + { +

+ Loading forecast… +

+ } + else + { + + + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
Five-day forecast — refreshed on demand
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+ } + } + else + { + + }
-
- -
- -@if (errorMessage == null) -{ - @if (forecasts == null) - { -

Loading...

- } - else - { - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
- } -} -else -{ - -} +
@code { private static readonly TimeSpan s_refreshInterval = TimeSpan.FromSeconds(2); diff --git a/samples/Metrics/MetricsApp.Client/Pages/Weather.razor.css b/samples/Metrics/MetricsApp.Client/Pages/Weather.razor.css new file mode 100644 index 000000000..b9fd06211 --- /dev/null +++ b/samples/Metrics/MetricsApp.Client/Pages/Weather.razor.css @@ -0,0 +1,39 @@ +.weather { padding: clamp(1.4rem, 4vw, 2.2rem); } + +.weather__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1.25rem; + flex-wrap: wrap; + margin-bottom: 1.25rem; +} +.weather__head h1 { margin: 0.35rem 0 0.5rem; font-size: clamp(1.5rem, 3vw, 2rem); } +.weather__lede { margin: 0; max-width: 46ch; color: var(--ink-soft); } + +.weather__controls { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.weather__body { min-height: 6rem; } + +.weather__loading { + display: inline-flex; + align-items: center; + gap: 0.6rem; + color: var(--ink-muted); + font-family: var(--font-mono); +} + +.spinner { + width: 1.05rem; + height: 1.05rem; + border-radius: 50%; + border: 2px solid var(--surface-inset); + border-top-color: var(--accent); + animation: spin 0.7s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } diff --git a/samples/Metrics/MetricsApp.Client/Shared/StatusMessage.razor b/samples/Metrics/MetricsApp.Client/Shared/StatusMessage.razor index 8f2724a45..1b5accd94 100644 --- a/samples/Metrics/MetricsApp.Client/Shared/StatusMessage.razor +++ b/samples/Metrics/MetricsApp.Client/Shared/StatusMessage.razor @@ -1,4 +1,4 @@ -@if (!string.IsNullOrEmpty(Message)) +@if (!string.IsNullOrEmpty(Message)) { var statusMessageClass = Message.StartsWith("Error") ? "danger" : "success";