@juice-js/observability 1.1.0-alpha.20 → 1.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,6 +62,7 @@ like juice-layout's dark-mode toggle) repaints the charts and the graph.
62
62
  - The trace page's resource usage and a service's Capacity tab read .NET's `System.Runtime` meter (.NET 9+): add
63
63
  `AddMeter("System.Runtime")` to the host's `WithMetrics`. If traces are sampled, also add `AddAspNetCoreInstrumentation()`
64
64
  there: the Capacity tab then counts requests from `http.server.request.duration`, not from spans, which undercount.
65
+ - The Load runs page reads windows a load test marks through the API: see "Load test runs" in the platform's README.
65
66
  - The router needs `bindToComponentInputs: true`. The pages take their route and query parameters as inputs.
66
67
  - The host's bearer-token interceptor (`@juice-js/auth`) must cover `apiBase`. The token carries the `platform_tenant`
67
68
  claim and a platform role (`viewer`, `operator` or `admin`).
@@ -0,0 +1,349 @@
1
+ import { ApiService, ContextService, LinkPipe } from "./juice-js-observability.mjs";
2
+ import { S as apiResource, o as problemText, u as ContextBar } from "./juice-js-observability-telemetry-BftWSOaQ.mjs";
3
+ import * as i0 from "@angular/core";
4
+ import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
5
+ import { forkJoin, of } from "rxjs";
6
+ import { Router, RouterLink } from "@angular/router";
7
+ import * as i1 from "@angular/material/progress-bar";
8
+ import { MatProgressBarModule } from "@angular/material/progress-bar";
9
+ const MAX_COMPARED = 4;
10
+ function perSecond(value) {
11
+ return value >= 10 ? value.toFixed(0) : value.toFixed(2);
12
+ }
13
+ function limitText(limit) {
14
+ const at = `from ${perSecond(limit.perSecond)}/s`;
15
+ switch (limit.reason) {
16
+ case "latency": return `Latency ${at}: p95 ${limit.value.toFixed(0)} ms, ${limit.baseline?.toFixed(0)} ms when quiet`;
17
+ case "consumer-busy": return `Message handling busy ${at}: ${(limit.value * 100).toFixed(0)}% of the time`;
18
+ case "queue": return `Thread pool queue ${at}: ${limit.value.toFixed(0)} waiting`;
19
+ case "errors": return `Errors ${at}: ${(limit.value * 100).toFixed(1)}%`;
20
+ case "backlog": return `Outbox backlog ${at}: ${limit.value.toFixed(0)} undelivered`;
21
+ default: return `${limit.reason} ${at}`;
22
+ }
23
+ }
24
+ var LoadRunsPage = class LoadRunsPage {
25
+ ctx = inject(ContextService);
26
+ api = inject(ApiService);
27
+ router = inject(Router);
28
+ compare = input(...ngDevMode ? [void 0, { debugName: "compare" }] : /* istanbul ignore next */ []);
29
+ maxCompared = MAX_COMPARED;
30
+ selected = computed(() => (this.compare() ?? "").split(",").filter(Boolean).slice(0, MAX_COMPARED), ...ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []);
31
+ runs = apiResource({
32
+ params: () => this.ctx.environment(),
33
+ stream: ({ params }) => this.api.loadRuns({ environment: params })
34
+ });
35
+ results = apiResource({
36
+ params: () => ({ ids: this.selected() }),
37
+ stream: ({ params }) => params.ids.length ? forkJoin(params.ids.map((id) => this.api.loadRunResults(id))) : of([])
38
+ });
39
+ error = computed(() => problemText(this.runs.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
40
+ resultsError = computed(() => problemText(this.results.error()), ...ngDevMode ? [{ debugName: "resultsError" }] : /* istanbul ignore next */ []);
41
+ serviceNames = computed(() => {
42
+ const services = (this.results.value() ?? []).flatMap((c) => c.services);
43
+ const limited = new Set(services.filter((s) => s.limit).map((s) => s.name));
44
+ return [...new Set(services.map((s) => s.name))].sort((a, b) => Number(limited.has(b)) - Number(limited.has(a)) || a.localeCompare(b));
45
+ }, ...ngDevMode ? [{ debugName: "serviceNames" }] : /* istanbul ignore next */ []);
46
+ limit = limitText;
47
+ rate = perSecond;
48
+ service(results, name) {
49
+ return results.services.find((s) => s.name === name) ?? null;
50
+ }
51
+ time(at) {
52
+ return new Date(at).toLocaleString();
53
+ }
54
+ length(run) {
55
+ const seconds = Math.round((new Date(run.endedAt).getTime() - new Date(run.startedAt).getTime()) / 1e3);
56
+ return seconds >= 120 ? `${Math.round(seconds / 60)} min` : `${seconds} s`;
57
+ }
58
+ window(run) {
59
+ return this.ctx.keep({
60
+ from: run.startedAt,
61
+ to: run.endedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
62
+ range: null,
63
+ env: run.environment ?? (this.ctx.environment() || null)
64
+ });
65
+ }
66
+ toggle(id) {
67
+ const current = this.selected();
68
+ const next = current.includes(id) ? current.filter((c) => c !== id) : [...current, id];
69
+ this.router.navigate([], {
70
+ queryParams: { compare: next.join(",") || null },
71
+ queryParamsHandling: "merge",
72
+ replaceUrl: true
73
+ });
74
+ }
75
+ static ɵfac = i0.ɵɵngDeclareFactory({
76
+ minVersion: "12.0.0",
77
+ version: "22.2.4",
78
+ ngImport: i0,
79
+ type: LoadRunsPage,
80
+ deps: [],
81
+ target: i0.ɵɵFactoryTarget.Component
82
+ });
83
+ static ɵcmp = i0.ɵɵngDeclareComponent({
84
+ minVersion: "17.0.0",
85
+ version: "22.2.4",
86
+ type: LoadRunsPage,
87
+ isStandalone: true,
88
+ selector: "ng-component",
89
+ inputs: { compare: {
90
+ classPropertyName: "compare",
91
+ publicName: "compare",
92
+ isSignal: true,
93
+ isRequired: false,
94
+ transformFunction: null
95
+ } },
96
+ ngImport: i0,
97
+ template: `
98
+ <jc-context-bar><h1>Load runs</h1></jc-context-bar>
99
+ @if (runs.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
100
+ @if (runs.error()) { <p class="jc-error">{{ error() }}</p> }
101
+ @if (runs.value(); as page) {
102
+ @if (page.items.length === 0) {
103
+ <p class="jc-muted">No load runs yet. A load test marks its start and end with <code>POST /load-runs</code> and <code>POST /load-runs/{{ '{' }}id{{ '}' }}/end</code>; see the README.</p>
104
+ } @else {
105
+ <table class="jc-table" data-testid="load-runs">
106
+ <thead><tr><th>Compare</th><th>Run</th><th>Environment</th><th>Started</th><th>Length</th><th>By</th></tr></thead>
107
+ <tbody>
108
+ @for (r of page.items; track r.id) {
109
+ <tr>
110
+ <td><input type="checkbox" [attr.aria-label]="'Compare ' + r.name" [checked]="selected().includes(r.id)"
111
+ [disabled]="!selected().includes(r.id) && selected().length >= maxCompared" (change)="toggle(r.id)" /></td>
112
+ <td><strong>{{ r.name }}</strong></td>
113
+ <td>{{ r.environment ?? 'all' }}</td>
114
+ <td>{{ time(r.startedAt) }}</td>
115
+ <td>@if (r.endedAt) { {{ length(r) }} } @else { <span class="jc-muted">not ended</span> }</td>
116
+ <td>{{ r.startedBy }}</td>
117
+ </tr>
118
+ }
119
+ </tbody>
120
+ </table>
121
+ }
122
+ }
123
+
124
+ @if (results.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
125
+ @if (results.error()) { <p class="jc-error">{{ resultsError() }}</p> }
126
+ @if (results.value(); as compared) {
127
+ @if (compared.length) {
128
+ <section class="jc-card" aria-labelledby="jc-load-compare" data-testid="load-compare">
129
+ <h3 id="jc-load-compare">{{ compared.length === 1 ? 'Results' : 'Comparison' }}</h3>
130
+ @if (serviceNames().length === 0) { <p class="jc-muted">No telemetry in these windows yet. Only whole time steps count, so a short run may show nothing.</p> }
131
+ @else {
132
+ <table class="jc-table">
133
+ <thead><tr><th>Service</th>@for (c of compared; track c.run.id) { <th>{{ c.run.name }}</th> }</tr></thead>
134
+ <tbody>
135
+ @for (name of serviceNames(); track name) {
136
+ <tr>
137
+ <th>{{ name }}</th>
138
+ @for (c of compared; track c.run.id) {
139
+ <td>
140
+ @if (service(c, name); as s) {
141
+ <div>Peak {{ rate(s.peakPerSecond) }}/s · p95 {{ s.p95AtPeakMs !== null ? s.p95AtPeakMs.toFixed(0) + ' ms' : '—' }} · errors {{ (s.errorRateAtPeak * 100).toFixed(1) }}%</div>
142
+ @if (s.limit; as l) { <div class="jc-bad" data-testid="load-limit">{{ limit(l) }}</div> }
143
+ @else { <div class="jc-muted">No limit reached</div> }
144
+ @if (s.serviceId) { <a class="jc-small" [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="window(c.run)">Open the window</a> }
145
+ } @else { <span class="jc-muted">No traffic</span> }
146
+ </td>
147
+ }
148
+ </tr>
149
+ }
150
+ </tbody>
151
+ </table>
152
+ @for (c of compared; track c.run.id) {
153
+ @for (s of c.services; track s.name) {
154
+ <details class="jc-load-steps">
155
+ <summary>{{ c.run.name }}: {{ s.name }}, each {{ c.stepSeconds }} s</summary>
156
+ <table class="jc-table">
157
+ <thead><tr><th>Time</th><th>Load/s</th><th>p95</th><th>Errors</th><th>Thread pool queue</th><th>Consumer busy</th><th>Outbox backlog</th></tr></thead>
158
+ <tbody>
159
+ @for (p of s.points; track p.at) {
160
+ <tr [class.jc-bad]="s.limit?.at === p.at">
161
+ <td>{{ time(p.at) }}</td>
162
+ <td>{{ rate(p.perSecond) }}</td>
163
+ <td>{{ p.p95Ms !== null ? p.p95Ms.toFixed(0) + ' ms' : '—' }}</td>
164
+ <td>{{ (p.errorRate * 100).toFixed(1) }}%</td>
165
+ <td>{{ p.queue ?? '—' }}</td>
166
+ <td>{{ (p.consumerBusy * 100).toFixed(0) }}%</td>
167
+ <td>{{ p.backlog }}</td>
168
+ </tr>
169
+ }
170
+ </tbody>
171
+ </table>
172
+ </details>
173
+ }
174
+ }
175
+ <ul class="jc-capacity-notes">
176
+ <li>Counted from spans, which undercount if traces are sampled. A message's wait in its queue isn't in its span: look at how busy the consumer was.</li>
177
+ <li>The outbox backlog includes messages of rolled-back transactions. Figures from other hardware carry over only roughly.</li>
178
+ </ul>
179
+ }
180
+ </section>
181
+ }
182
+ }`,
183
+ isInline: true,
184
+ dependencies: [
185
+ {
186
+ kind: "directive",
187
+ type: RouterLink,
188
+ selector: "[routerLink]",
189
+ inputs: [
190
+ "target",
191
+ "queryParams",
192
+ "fragment",
193
+ "queryParamsHandling",
194
+ "state",
195
+ "info",
196
+ "relativeTo",
197
+ "preserveFragment",
198
+ "skipLocationChange",
199
+ "replaceUrl",
200
+ "browserUrl",
201
+ "routerLink"
202
+ ]
203
+ },
204
+ {
205
+ kind: "ngmodule",
206
+ type: MatProgressBarModule
207
+ },
208
+ {
209
+ kind: "component",
210
+ type: i1.MatProgressBar,
211
+ selector: "mat-progress-bar",
212
+ inputs: [
213
+ "color",
214
+ "value",
215
+ "bufferValue",
216
+ "mode"
217
+ ],
218
+ outputs: ["animationEnd"],
219
+ exportAs: ["matProgressBar"]
220
+ },
221
+ {
222
+ kind: "component",
223
+ type: ContextBar,
224
+ selector: "jc-context-bar"
225
+ },
226
+ {
227
+ kind: "pipe",
228
+ type: LinkPipe,
229
+ name: "jcLink"
230
+ }
231
+ ],
232
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
233
+ });
234
+ };
235
+ i0.ɵɵngDeclareClassMetadata({
236
+ minVersion: "12.0.0",
237
+ version: "22.2.4",
238
+ ngImport: i0,
239
+ type: LoadRunsPage,
240
+ decorators: [{
241
+ type: Component,
242
+ args: [{
243
+ changeDetection: ChangeDetectionStrategy.OnPush,
244
+ imports: [
245
+ LinkPipe,
246
+ RouterLink,
247
+ MatProgressBarModule,
248
+ ContextBar
249
+ ],
250
+ template: `
251
+ <jc-context-bar><h1>Load runs</h1></jc-context-bar>
252
+ @if (runs.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
253
+ @if (runs.error()) { <p class="jc-error">{{ error() }}</p> }
254
+ @if (runs.value(); as page) {
255
+ @if (page.items.length === 0) {
256
+ <p class="jc-muted">No load runs yet. A load test marks its start and end with <code>POST /load-runs</code> and <code>POST /load-runs/{{ '{' }}id{{ '}' }}/end</code>; see the README.</p>
257
+ } @else {
258
+ <table class="jc-table" data-testid="load-runs">
259
+ <thead><tr><th>Compare</th><th>Run</th><th>Environment</th><th>Started</th><th>Length</th><th>By</th></tr></thead>
260
+ <tbody>
261
+ @for (r of page.items; track r.id) {
262
+ <tr>
263
+ <td><input type="checkbox" [attr.aria-label]="'Compare ' + r.name" [checked]="selected().includes(r.id)"
264
+ [disabled]="!selected().includes(r.id) && selected().length >= maxCompared" (change)="toggle(r.id)" /></td>
265
+ <td><strong>{{ r.name }}</strong></td>
266
+ <td>{{ r.environment ?? 'all' }}</td>
267
+ <td>{{ time(r.startedAt) }}</td>
268
+ <td>@if (r.endedAt) { {{ length(r) }} } @else { <span class="jc-muted">not ended</span> }</td>
269
+ <td>{{ r.startedBy }}</td>
270
+ </tr>
271
+ }
272
+ </tbody>
273
+ </table>
274
+ }
275
+ }
276
+
277
+ @if (results.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
278
+ @if (results.error()) { <p class="jc-error">{{ resultsError() }}</p> }
279
+ @if (results.value(); as compared) {
280
+ @if (compared.length) {
281
+ <section class="jc-card" aria-labelledby="jc-load-compare" data-testid="load-compare">
282
+ <h3 id="jc-load-compare">{{ compared.length === 1 ? 'Results' : 'Comparison' }}</h3>
283
+ @if (serviceNames().length === 0) { <p class="jc-muted">No telemetry in these windows yet. Only whole time steps count, so a short run may show nothing.</p> }
284
+ @else {
285
+ <table class="jc-table">
286
+ <thead><tr><th>Service</th>@for (c of compared; track c.run.id) { <th>{{ c.run.name }}</th> }</tr></thead>
287
+ <tbody>
288
+ @for (name of serviceNames(); track name) {
289
+ <tr>
290
+ <th>{{ name }}</th>
291
+ @for (c of compared; track c.run.id) {
292
+ <td>
293
+ @if (service(c, name); as s) {
294
+ <div>Peak {{ rate(s.peakPerSecond) }}/s · p95 {{ s.p95AtPeakMs !== null ? s.p95AtPeakMs.toFixed(0) + ' ms' : '—' }} · errors {{ (s.errorRateAtPeak * 100).toFixed(1) }}%</div>
295
+ @if (s.limit; as l) { <div class="jc-bad" data-testid="load-limit">{{ limit(l) }}</div> }
296
+ @else { <div class="jc-muted">No limit reached</div> }
297
+ @if (s.serviceId) { <a class="jc-small" [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="window(c.run)">Open the window</a> }
298
+ } @else { <span class="jc-muted">No traffic</span> }
299
+ </td>
300
+ }
301
+ </tr>
302
+ }
303
+ </tbody>
304
+ </table>
305
+ @for (c of compared; track c.run.id) {
306
+ @for (s of c.services; track s.name) {
307
+ <details class="jc-load-steps">
308
+ <summary>{{ c.run.name }}: {{ s.name }}, each {{ c.stepSeconds }} s</summary>
309
+ <table class="jc-table">
310
+ <thead><tr><th>Time</th><th>Load/s</th><th>p95</th><th>Errors</th><th>Thread pool queue</th><th>Consumer busy</th><th>Outbox backlog</th></tr></thead>
311
+ <tbody>
312
+ @for (p of s.points; track p.at) {
313
+ <tr [class.jc-bad]="s.limit?.at === p.at">
314
+ <td>{{ time(p.at) }}</td>
315
+ <td>{{ rate(p.perSecond) }}</td>
316
+ <td>{{ p.p95Ms !== null ? p.p95Ms.toFixed(0) + ' ms' : '—' }}</td>
317
+ <td>{{ (p.errorRate * 100).toFixed(1) }}%</td>
318
+ <td>{{ p.queue ?? '—' }}</td>
319
+ <td>{{ (p.consumerBusy * 100).toFixed(0) }}%</td>
320
+ <td>{{ p.backlog }}</td>
321
+ </tr>
322
+ }
323
+ </tbody>
324
+ </table>
325
+ </details>
326
+ }
327
+ }
328
+ <ul class="jc-capacity-notes">
329
+ <li>Counted from spans, which undercount if traces are sampled. A message's wait in its queue isn't in its span: look at how busy the consumer was.</li>
330
+ <li>The outbox backlog includes messages of rolled-back transactions. Figures from other hardware carry over only roughly.</li>
331
+ </ul>
332
+ }
333
+ </section>
334
+ }
335
+ }`
336
+ }]
337
+ }],
338
+ propDecorators: { compare: [{
339
+ type: i0.Input,
340
+ args: [{
341
+ isSignal: true,
342
+ alias: "compare",
343
+ required: false
344
+ }]
345
+ }] }
346
+ });
347
+ export { LoadRunsPage };
348
+
349
+ //# sourceMappingURL=juice-js-observability-load-runs.page-C2rk2bz9.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice-js-observability-load-runs.page-C2rk2bz9.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/load-runs.page.ts"],"sourcesContent":["import { LinkPipe } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { Router, RouterLink } from '@angular/router';\nimport { forkJoin, of } from 'rxjs';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { LoadRun, LoadRunLimit, LoadRunResults, LoadRunService } from '../core/models';\nimport { apiResource } from '../shared/resource';\nimport { problemText } from '../shared/telemetry';\nimport { ContextBar } from '../shared/ui';\n\n/** Comparing more runs than this doesn't fit on a page. */\nconst MAX_COMPARED = 4;\n\nfunction perSecond(value: number): string { return value >= 10 ? value.toFixed(0) : value.toFixed(2); }\n\n/** Where a service started to struggle, in words. */\nexport function limitText(limit: LoadRunLimit): string {\n const at = `from ${perSecond(limit.perSecond)}/s`;\n switch (limit.reason) {\n case 'latency': return `Latency ${at}: p95 ${limit.value.toFixed(0)} ms, ${limit.baseline?.toFixed(0)} ms when quiet`;\n case 'consumer-busy': return `Message handling busy ${at}: ${(limit.value * 100).toFixed(0)}% of the time`;\n case 'queue': return `Thread pool queue ${at}: ${limit.value.toFixed(0)} waiting`;\n case 'errors': return `Errors ${at}: ${(limit.value * 100).toFixed(1)}%`;\n case 'backlog': return `Outbox backlog ${at}: ${limit.value.toFixed(0)} undelivered`;\n default: return `${limit.reason} ${at}`;\n }\n}\n\n/**\n * Load test runs: each is the window a test marked through the API (the platform generates no load). Selected runs are compared per\n * service, with where each started to struggle, which everyday traffic can't show. The selection is in the URL.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, MatProgressBarModule, ContextBar],\n template: `\n <jc-context-bar><h1>Load runs</h1></jc-context-bar>\n @if (runs.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (runs.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (runs.value(); as page) {\n @if (page.items.length === 0) {\n <p class=\"jc-muted\">No load runs yet. A load test marks its start and end with <code>POST /load-runs</code> and <code>POST /load-runs/{{ '{' }}id{{ '}' }}/end</code>; see the README.</p>\n } @else {\n <table class=\"jc-table\" data-testid=\"load-runs\">\n <thead><tr><th>Compare</th><th>Run</th><th>Environment</th><th>Started</th><th>Length</th><th>By</th></tr></thead>\n <tbody>\n @for (r of page.items; track r.id) {\n <tr>\n <td><input type=\"checkbox\" [attr.aria-label]=\"'Compare ' + r.name\" [checked]=\"selected().includes(r.id)\"\n [disabled]=\"!selected().includes(r.id) && selected().length >= maxCompared\" (change)=\"toggle(r.id)\" /></td>\n <td><strong>{{ r.name }}</strong></td>\n <td>{{ r.environment ?? 'all' }}</td>\n <td>{{ time(r.startedAt) }}</td>\n <td>@if (r.endedAt) { {{ length(r) }} } @else { <span class=\"jc-muted\">not ended</span> }</td>\n <td>{{ r.startedBy }}</td>\n </tr>\n }\n </tbody>\n </table>\n }\n }\n\n @if (results.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (results.error()) { <p class=\"jc-error\">{{ resultsError() }}</p> }\n @if (results.value(); as compared) {\n @if (compared.length) {\n <section class=\"jc-card\" aria-labelledby=\"jc-load-compare\" data-testid=\"load-compare\">\n <h3 id=\"jc-load-compare\">{{ compared.length === 1 ? 'Results' : 'Comparison' }}</h3>\n @if (serviceNames().length === 0) { <p class=\"jc-muted\">No telemetry in these windows yet. Only whole time steps count, so a short run may show nothing.</p> }\n @else {\n <table class=\"jc-table\">\n <thead><tr><th>Service</th>@for (c of compared; track c.run.id) { <th>{{ c.run.name }}</th> }</tr></thead>\n <tbody>\n @for (name of serviceNames(); track name) {\n <tr>\n <th>{{ name }}</th>\n @for (c of compared; track c.run.id) {\n <td>\n @if (service(c, name); as s) {\n <div>Peak {{ rate(s.peakPerSecond) }}/s · p95 {{ s.p95AtPeakMs !== null ? s.p95AtPeakMs.toFixed(0) + ' ms' : '—' }} · errors {{ (s.errorRateAtPeak * 100).toFixed(1) }}%</div>\n @if (s.limit; as l) { <div class=\"jc-bad\" data-testid=\"load-limit\">{{ limit(l) }}</div> }\n @else { <div class=\"jc-muted\">No limit reached</div> }\n @if (s.serviceId) { <a class=\"jc-small\" [routerLink]=\"['services', s.serviceId] | jcLink\" [queryParams]=\"window(c.run)\">Open the window</a> }\n } @else { <span class=\"jc-muted\">No traffic</span> }\n </td>\n }\n </tr>\n }\n </tbody>\n </table>\n @for (c of compared; track c.run.id) {\n @for (s of c.services; track s.name) {\n <details class=\"jc-load-steps\">\n <summary>{{ c.run.name }}: {{ s.name }}, each {{ c.stepSeconds }} s</summary>\n <table class=\"jc-table\">\n <thead><tr><th>Time</th><th>Load/s</th><th>p95</th><th>Errors</th><th>Thread pool queue</th><th>Consumer busy</th><th>Outbox backlog</th></tr></thead>\n <tbody>\n @for (p of s.points; track p.at) {\n <tr [class.jc-bad]=\"s.limit?.at === p.at\">\n <td>{{ time(p.at) }}</td>\n <td>{{ rate(p.perSecond) }}</td>\n <td>{{ p.p95Ms !== null ? p.p95Ms.toFixed(0) + ' ms' : '—' }}</td>\n <td>{{ (p.errorRate * 100).toFixed(1) }}%</td>\n <td>{{ p.queue ?? '—' }}</td>\n <td>{{ (p.consumerBusy * 100).toFixed(0) }}%</td>\n <td>{{ p.backlog }}</td>\n </tr>\n }\n </tbody>\n </table>\n </details>\n }\n }\n <ul class=\"jc-capacity-notes\">\n <li>Counted from spans, which undercount if traces are sampled. A message's wait in its queue isn't in its span: look at how busy the consumer was.</li>\n <li>The outbox backlog includes messages of rolled-back transactions. Figures from other hardware carry over only roughly.</li>\n </ul>\n }\n </section>\n }\n }`,\n})\nexport class LoadRunsPage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n private readonly router = inject(Router);\n\n /** The selected run ids, comma separated. */\n readonly compare = input<string>();\n\n protected readonly maxCompared = MAX_COMPARED;\n protected readonly selected = computed(() => (this.compare() ?? '').split(',').filter(Boolean).slice(0, MAX_COMPARED));\n protected readonly runs = apiResource({ params: () => this.ctx.environment(), stream: ({ params }) => this.api.loadRuns({ environment: params }) });\n protected readonly results = apiResource({\n params: () => ({ ids: this.selected() }),\n stream: ({ params }) => params.ids.length ? forkJoin(params.ids.map(id => this.api.loadRunResults(id))) : of<LoadRunResults[]>([]),\n });\n protected readonly error = computed(() => problemText(this.runs.error()));\n protected readonly resultsError = computed(() => problemText(this.results.error()));\n /** Services of all compared runs, those that reached a limit first. */\n protected readonly serviceNames = computed(() => {\n const services = (this.results.value() ?? []).flatMap(c => c.services);\n const limited = new Set(services.filter(s => s.limit).map(s => s.name));\n return [...new Set(services.map(s => s.name))].sort((a, b) => Number(limited.has(b)) - Number(limited.has(a)) || a.localeCompare(b));\n });\n\n protected readonly limit = limitText;\n protected readonly rate = perSecond;\n\n protected service(results: LoadRunResults, name: string): LoadRunService | null {\n return results.services.find(s => s.name === name) ?? null;\n }\n\n protected time(at: string): string { return new Date(at).toLocaleString(); }\n\n protected length(run: LoadRun): string {\n const seconds = Math.round((new Date(run.endedAt!).getTime() - new Date(run.startedAt).getTime()) / 1000);\n return seconds >= 120 ? `${Math.round(seconds / 60)} min` : `${seconds} s`;\n }\n\n /** A service's pages over exactly this run's window. */\n protected window(run: LoadRun) {\n return this.ctx.keep({ from: run.startedAt, to: run.endedAt ?? new Date().toISOString(), range: null, env: run.environment ?? (this.ctx.environment() || null) });\n }\n\n protected toggle(id: string) {\n const current = this.selected();\n const next = current.includes(id) ? current.filter(c => c !== id) : [...current, id];\n void this.router.navigate([], { queryParams: { compare: next.join(',') || null }, queryParamsHandling: 'merge', replaceUrl: true });\n }\n}\n"],"mappings":"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"}
@@ -177,7 +177,7 @@ var ServiceCapacity = class ServiceCapacity {
177
177
  <li class="jc-bad">Response time already rises with load: {{ r.low }} ms when quiet, {{ r.high }} ms when busy. Something may be near its limit, so the real capacity may be lower.</li>
178
178
  }
179
179
  @if (busyConsumer()) {
180
- <li class="jc-bad" data-testid="capacity-busy-consumer">Message processing was busy nearly all the time, at about {{ perSecond(m.peakMessagesPerSecond) }} messages/s per instance. If it handles one message at a time, that is its limit whatever CPU says, and its queue may be growing. The estimate uses that rate. See which step takes the time in a trace.</li>
180
+ <li class="jc-bad" data-testid="capacity-busy-consumer">Message processing was busy nearly all the time, at about {{ perSecond(m.peakMessagesPerSecond) }} messages/s per instance. If it handles one message at a time, that is its limit whatever CPU says, and its queue may be growing. The estimate uses that rate, but a saturated consumer's rate can fall, so it may not hold. See which step takes the time in a trace.</li>
181
181
  }
182
182
  @if (m.outbox?.fallingBehind) {
183
183
  <li class="jc-bad" data-testid="capacity-outbox-behind">Outbox delivery is falling behind: {{ count(m.outbox!.undelivered) }} messages created in this window weren't delivered. CPU and requests can look healthy while this grows, and the estimate doesn't include it. If another service delivers this outbox, this doesn't apply.</li>
@@ -185,6 +185,9 @@ var ServiceCapacity = class ServiceCapacity {
185
185
  @if (m.peakQueue !== null && m.peakQueue >= notableQueue) {
186
186
  <li class="jc-bad">The thread pool queue reached {{ m.peakQueue }}: work was waiting for a thread.</li>
187
187
  }
188
+ @if (m.cpuModel === 'average') {
189
+ <li data-testid="capacity-load-hint">Load didn't vary enough here to separate idle CPU from the cost of each request, so this is rough. A <a [routerLink]="['load-runs'] | jcLink" [queryParams]="ctx.keep()">load test</a> gives a better estimate.</li>
190
+ }
188
191
  <li>Assumes cost grows in a straight line with load. A database, lock or connection limit can be reached long before CPU runs out.</li>
189
192
  <li>Covers this service only, not its databases, brokers or the services it calls.</li>
190
193
  </ul>
@@ -344,6 +347,30 @@ var ServiceCapacity = class ServiceCapacity {
344
347
  ],
345
348
  outputs: ["animationEnd"],
346
349
  exportAs: ["matProgressBar"]
350
+ },
351
+ {
352
+ kind: "directive",
353
+ type: RouterLink,
354
+ selector: "[routerLink]",
355
+ inputs: [
356
+ "target",
357
+ "queryParams",
358
+ "fragment",
359
+ "queryParamsHandling",
360
+ "state",
361
+ "info",
362
+ "relativeTo",
363
+ "preserveFragment",
364
+ "skipLocationChange",
365
+ "replaceUrl",
366
+ "browserUrl",
367
+ "routerLink"
368
+ ]
369
+ },
370
+ {
371
+ kind: "pipe",
372
+ type: LinkPipe,
373
+ name: "jcLink"
347
374
  }
348
375
  ],
349
376
  changeDetection: i0.ChangeDetectionStrategy.OnPush
@@ -363,7 +390,9 @@ i0.ɵɵngDeclareClassMetadata({
363
390
  FormsModule,
364
391
  MatFormFieldModule,
365
392
  MatInputModule,
366
- MatProgressBarModule
393
+ MatProgressBarModule,
394
+ RouterLink,
395
+ LinkPipe
367
396
  ],
368
397
  template: `
369
398
  @if (capacity.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
@@ -409,7 +438,7 @@ i0.ɵɵngDeclareClassMetadata({
409
438
  <li class="jc-bad">Response time already rises with load: {{ r.low }} ms when quiet, {{ r.high }} ms when busy. Something may be near its limit, so the real capacity may be lower.</li>
410
439
  }
411
440
  @if (busyConsumer()) {
412
- <li class="jc-bad" data-testid="capacity-busy-consumer">Message processing was busy nearly all the time, at about {{ perSecond(m.peakMessagesPerSecond) }} messages/s per instance. If it handles one message at a time, that is its limit whatever CPU says, and its queue may be growing. The estimate uses that rate. See which step takes the time in a trace.</li>
441
+ <li class="jc-bad" data-testid="capacity-busy-consumer">Message processing was busy nearly all the time, at about {{ perSecond(m.peakMessagesPerSecond) }} messages/s per instance. If it handles one message at a time, that is its limit whatever CPU says, and its queue may be growing. The estimate uses that rate, but a saturated consumer's rate can fall, so it may not hold. See which step takes the time in a trace.</li>
413
442
  }
414
443
  @if (m.outbox?.fallingBehind) {
415
444
  <li class="jc-bad" data-testid="capacity-outbox-behind">Outbox delivery is falling behind: {{ count(m.outbox!.undelivered) }} messages created in this window weren't delivered. CPU and requests can look healthy while this grows, and the estimate doesn't include it. If another service delivers this outbox, this doesn't apply.</li>
@@ -417,6 +446,9 @@ i0.ɵɵngDeclareClassMetadata({
417
446
  @if (m.peakQueue !== null && m.peakQueue >= notableQueue) {
418
447
  <li class="jc-bad">The thread pool queue reached {{ m.peakQueue }}: work was waiting for a thread.</li>
419
448
  }
449
+ @if (m.cpuModel === 'average') {
450
+ <li data-testid="capacity-load-hint">Load didn't vary enough here to separate idle CPU from the cost of each request, so this is rough. A <a [routerLink]="['load-runs'] | jcLink" [queryParams]="ctx.keep()">load test</a> gives a better estimate.</li>
451
+ }
420
452
  <li>Assumes cost grows in a straight line with load. A database, lock or connection limit can be reached long before CPU runs out.</li>
421
453
  <li>Covers this service only, not its databases, brokers or the services it calls.</li>
422
454
  </ul>
@@ -1220,4 +1252,4 @@ i0.ɵɵngDeclareClassMetadata({
1220
1252
  });
1221
1253
  export { ServicePage };
1222
1254
 
1223
- //# sourceMappingURL=juice-js-observability-service.page-rGAvUK_k.mjs.map
1255
+ //# sourceMappingURL=juice-js-observability-service.page-BrcID0Bs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"juice-js-observability-service.page-BrcID0Bs.mjs","names":["i1","i3","i2"],"sources":["../../../../projects/juice-js/observability/src/lib/shared/capacity.ts","../../../../projects/juice-js/observability/src/lib/pages/service.page.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, input, linkedSignal, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { LinkPipe } from '../core/options';\nimport { Capacity } from '../core/models';\nimport { apiResource } from './resource';\nimport { problemText } from './telemetry';\n\nexport interface CapacityInputs {\n users: number;\n /** Seconds between one user's actions. */\n thinkSeconds: number;\n /** Requests or messages one user action causes in this service. */\n perAction: number;\n /** The CPU use to stay under, from 0 to 1. */\n targetCpu: number;\n coresPerInstance: number;\n}\n\nexport interface CapacityPlan {\n perSecond: number;\n instances: number;\n cores: number;\n /** Per instance; null without memory data. */\n memoryBytes: number | null;\n /** The load each instance would carry. */\n perInstance: number;\n /** That load over the highest measured: above 1 is beyond what was measured. */\n beyondMeasured: number | null;\n /** What set the number of instances: CPU, or a consumer that was already busy all the time. */\n limitedBy: 'cpu' | 'messages';\n}\n\n/**\n * The calculator: `null` when the inputs or the measurements can't give an answer, such as a target that idle CPU alone\n * already exceeds. Each instance can carry (target × cores − idle) ÷ CPU per unit, unless a consumer was seen busy all the\n * time: more CPU doesn't help one that handles a message at a time, so an instance carries only the rate it reached.\n */\nexport function planCapacity(m: Capacity, i: CapacityInputs): CapacityPlan | null {\n if (!m.cpuSecondsPerUnit || m.cpuSecondsPerUnit <= 0) return null;\n if (!(i.users > 0 && i.perAction > 0 && i.targetCpu > 0 && i.targetCpu <= 1 && i.coresPerInstance > 0 && i.thinkSeconds >= 0)) return null;\n const response = (m.responseMs ?? 0) / 1000;\n if (i.thinkSeconds + response <= 0) return null;\n\n const byCpu = (i.targetCpu * i.coresPerInstance - (m.idleCores ?? 0)) / m.cpuSecondsPerUnit;\n if (byCpu <= 0) return null;\n const limitedBy = consumerBusy(m) && m.peakMessagesPerSecond < byCpu ? 'messages' : 'cpu';\n const carried = limitedBy === 'messages' ? m.peakMessagesPerSecond : byCpu;\n const perSecond = i.users * i.perAction / (i.thinkSeconds + response);\n const instances = Math.max(1, Math.ceil(perSecond / carried));\n const perInstance = perSecond / instances;\n const memoryBytes = m.memoryModel === 'fitted'\n ? m.idleMemoryBytes! + m.memoryBytesPerInFlight! * perInstance * response\n : m.peakMemoryBytes;\n // A consumer held back by message handling never used its cores: count what it used, at the target, not the machine's.\n const coresEach = limitedBy === 'messages' && m.peakCores !== null\n ? Math.min(i.coresPerInstance, Math.ceil(m.peakCores / i.targetCpu))\n : i.coresPerInstance;\n return {\n perSecond, instances, cores: instances * coresEach, memoryBytes, perInstance,\n beyondMeasured: m.peakPerSecond > 0 ? perInstance / m.peakPerSecond : null,\n limitedBy,\n };\n}\n\n/** A consumer counts as busy all the time from this much message processing per second of time. */\nconst BUSY_CONSUMER = 0.9;\nfunction consumerBusy(m: Capacity): boolean { return m.peakMessagesInFlight >= BUSY_CONSUMER && m.peakMessagesPerSecond > 0; }\n\n/** A thread pool queue of a few items comes and goes on a healthy host; from this many, work was really waiting. */\nconst NOTABLE_QUEUE = 10;\n\n/** Response time counts as rising with load from this ratio between the busiest and quietest samples. */\nconst RISING_RESPONSE = 1.5;\n\nfunction bytes(value: number | null): string {\n if (value === null) return '—';\n return value >= 1024 ** 3 ? `${(value / 1024 ** 3).toFixed(1)} GB` : `${Math.round(value / 1024 ** 2)} MB`;\n}\nfunction rate(value: number): string { return value >= 10 ? value.toFixed(0) : value.toFixed(2); }\n\n/**\n * A service's Capacity tab: what it was measured to need in the selected window, and a calculator from users to instances,\n * cores and memory. The estimate says where it comes from and where it stops holding: scaling isn't linear, and the\n * service's own databases and brokers are not in it.\n */\n@Component({\n selector: 'jc-service-capacity',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [FormsModule, MatFormFieldModule, MatInputModule, MatProgressBarModule, RouterLink, LinkPipe],\n template: `\n @if (capacity.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (capacity.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (mixedEnvironments()) {\n <p class=\"jc-notice\">This service runs in more than one environment, and their hardware may differ. Choose an environment for an estimate that means something.</p>\n }\n @if (capacity.value(); as m) {\n @if (m.samples === 0) {\n <p class=\"jc-muted\">No telemetry from this service in this window.</p>\n } @else {\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-estimate\">\n <h3 id=\"jc-capacity-estimate\">Estimate</h3>\n <div class=\"jc-capacity-inputs\">\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Concurrent users</mat-label>\n <input matInput type=\"number\" min=\"1\" name=\"users\" [ngModel]=\"users()\" (ngModelChange)=\"users.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Seconds between actions</mat-label>\n <input matInput type=\"number\" min=\"0\" name=\"thinkSeconds\" [ngModel]=\"thinkSeconds()\" (ngModelChange)=\"thinkSeconds.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Requests per action</mat-label>\n <input matInput type=\"number\" min=\"0\" name=\"perAction\" [ngModel]=\"perAction()\" (ngModelChange)=\"perAction.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Target CPU use (%)</mat-label>\n <input matInput type=\"number\" min=\"1\" max=\"100\" name=\"targetCpu\" [ngModel]=\"targetCpu()\" (ngModelChange)=\"targetCpu.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Cores per instance</mat-label>\n <input matInput type=\"number\" min=\"1\" name=\"coresPerInstance\" [ngModel]=\"coresPerInstance()\" (ngModelChange)=\"coresPerInstance.set($event)\" /></mat-form-field>\n </div>\n @if (plan(); as p) {\n <div class=\"jc-capacity-result\" data-testid=\"capacity-plan\">\n <div><strong>{{ p.instances }}</strong><span class=\"jc-muted\">instance{{ p.instances === 1 ? '' : 's' }}</span></div>\n <div><strong>{{ p.cores }}</strong><span class=\"jc-muted\">cores in total</span></div>\n <div><strong>{{ memory(p.memoryBytes) }}</strong><span class=\"jc-muted\">memory per instance{{ m.memoryModel === 'peak' ? ' (highest seen)' : '' }}</span></div>\n <div><strong>{{ perSecond(p.perSecond) }}</strong><span class=\"jc-muted\">requests and messages/s</span></div>\n </div>\n @if (p.beyondMeasured !== null && p.beyondMeasured > 1) {\n <p class=\"jc-notice\" data-testid=\"capacity-beyond\">Each instance would carry {{ perSecond(p.perInstance) }}/s, {{ p.beyondMeasured.toFixed(1) }}× the highest measured ({{ perSecond(m.peakPerSecond) }}/s). This is an extrapolation, and only a load test can confirm it.</p>\n }\n } @else if (m.cpuModel === 'none') {\n <p class=\"jc-notice\">No CPU data, so no estimate. CPU needs two readings at least a minute apart: choose a longer window, or wait. If it stays empty, add <code>AddMeter(\"System.Runtime\")</code> to the host's metrics (.NET 9+).</p>\n } @else {\n <p class=\"jc-notice\">No estimate for these values. Check them: idle CPU alone may already exceed the target.</p>\n }\n <ul class=\"jc-capacity-notes\">\n @if (responseRising(); as r) {\n <li class=\"jc-bad\">Response time already rises with load: {{ r.low }} ms when quiet, {{ r.high }} ms when busy. Something may be near its limit, so the real capacity may be lower.</li>\n }\n @if (busyConsumer()) {\n <li class=\"jc-bad\" data-testid=\"capacity-busy-consumer\">Message processing was busy nearly all the time, at about {{ perSecond(m.peakMessagesPerSecond) }} messages/s per instance. If it handles one message at a time, that is its limit whatever CPU says, and its queue may be growing. The estimate uses that rate, but a saturated consumer's rate can fall, so it may not hold. See which step takes the time in a trace.</li>\n }\n @if (m.outbox?.fallingBehind) {\n <li class=\"jc-bad\" data-testid=\"capacity-outbox-behind\">Outbox delivery is falling behind: {{ count(m.outbox!.undelivered) }} messages created in this window weren't delivered. CPU and requests can look healthy while this grows, and the estimate doesn't include it. If another service delivers this outbox, this doesn't apply.</li>\n }\n @if (m.peakQueue !== null && m.peakQueue >= notableQueue) {\n <li class=\"jc-bad\">The thread pool queue reached {{ m.peakQueue }}: work was waiting for a thread.</li>\n }\n @if (m.cpuModel === 'average') {\n <li data-testid=\"capacity-load-hint\">Load didn't vary enough here to separate idle CPU from the cost of each request, so this is rough. A <a [routerLink]=\"['load-runs'] | jcLink\" [queryParams]=\"ctx.keep()\">load test</a> gives a better estimate.</li>\n }\n <li>Assumes cost grows in a straight line with load. A database, lock or connection limit can be reached long before CPU runs out.</li>\n <li>Covers this service only, not its databases, brokers or the services it calls.</li>\n </ul>\n </section>\n\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-measured\">\n <h3 id=\"jc-capacity-measured\">Measured in this window</h3>\n <table class=\"jc-table jc-capacity-measured\">\n <tbody>\n <tr><th>CPU per request or message</th>\n <td>\n @if (m.cpuSecondsPerUnit !== null) { {{ (m.cpuSecondsPerUnit * 1000).toFixed(1) }} ms }\n @else { <span class=\"jc-muted\">No CPU data yet</span> }\n @if (m.cpuModel === 'average') {\n <div class=\"jc-muted jc-small\">Measured in the busiest samples and includes idle CPU, so it can read high. Load didn't vary enough, or CPU didn't follow it in a straight line, to separate the two.</div>\n }\n </td></tr>\n <tr><th>Idle CPU</th><td>@if (m.idleCores !== null) { {{ m.idleCores.toFixed(2) }} cores } @else { <span class=\"jc-muted\">Not separated</span> }</td></tr>\n <tr><th>Idle memory</th><td>@if (m.idleMemoryBytes !== null) { {{ memory(m.idleMemoryBytes) }} } @else { <span class=\"jc-muted\">Not separated</span> }</td></tr>\n <tr><th>Memory growth under load</th><td>@if (m.memoryBytesPerInFlight !== null) { {{ memory(m.memoryBytesPerInFlight) }} per request in flight } @else { <span class=\"jc-muted\">Not separated</span> }</td></tr>\n <tr><th>Highest load, one instance</th><td>{{ perSecond(m.peakPerSecond) }} requests and messages/s</td></tr>\n <tr><th>Highest use, one instance</th><td>\n {{ m.peakCores !== null ? m.peakCores.toFixed(2) + ' cores' : 'CPU unknown' }} · {{ m.peakMemoryBytes !== null ? memory(m.peakMemoryBytes) : 'memory unknown' }}</td></tr>\n <tr><th>Average response</th><td>{{ m.responseMs !== null ? m.responseMs.toFixed(0) + ' ms' : '—' }}</td></tr>\n <tr><th>Measured on</th><td>\n {{ m.instances }} instance{{ m.instances === 1 ? '' : 's' }}@if (m.coresPerInstance) {, {{ m.coresPerInstance }} cores each}@if (m.hosts.length) {, on {{ m.hosts.join(', ') }}}.\n <div class=\"jc-muted jc-small\">Figures from other hardware carry over only roughly.</div></td></tr>\n <tr><th>Based on</th><td>\n {{ m.samples }} samples of {{ m.stepSeconds }} s.\n <div class=\"jc-muted jc-small\">{{ sourceNote() }}</div>\n @if (m.truncated) { <div class=\"jc-bad jc-small\">Too many samples in this window, so only the newest were used. Choose a shorter one.</div> }</td></tr>\n </tbody>\n </table>\n </section>\n\n @if (m.outbox; as o) {\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-outbox\" data-testid=\"capacity-outbox\">\n <h3 id=\"jc-capacity-outbox\">Outbox delivery</h3>\n <table class=\"jc-table jc-capacity-measured\">\n <tbody>\n <tr><th>Messages created</th><td>{{ perSecond(o.createdPerSecond) }}/s</td></tr>\n <tr><th>Messages delivered</th><td>{{ perSecond(o.deliveredPerSecond) }}/s</td></tr>\n <tr><th>Created but not delivered</th><td [class.jc-bad]=\"o.fallingBehind\">{{ count(o.undelivered) }} in this window\n <div class=\"jc-muted jc-small\">Includes messages of rolled-back transactions, which are never delivered.</div></td></tr>\n <tr><th>Time to delivery, p95</th><td [class.jc-bad]=\"o.fallingBehind\">@if (o.recentDelaySeconds !== null) { {{ o.recentDelaySeconds.toFixed(1) }} s in the last 5 minutes } @else { <span class=\"jc-muted\">Nothing delivered in the last 5 minutes</span> }</td></tr>\n </tbody>\n </table>\n </section>\n }\n }\n }`,\n})\nexport class ServiceCapacity {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n readonly serviceId = input.required<string>();\n /** The environments the service runs in: figures across several mix their hardware. */\n readonly environments = input<string[]>([]);\n\n protected readonly capacity = apiResource({\n params: () => ({ id: this.serviceId(), env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute() }),\n stream: ({ params }) => this.api.capacity({ serviceId: params.id, environment: params.env, ...this.ctx.window() }),\n });\n protected readonly users = signal(1000);\n protected readonly thinkSeconds = signal(10);\n protected readonly perAction = signal(1);\n protected readonly targetCpu = signal(60);\n protected readonly coresPerInstance = linkedSignal(() => this.capacity.value()?.coresPerInstance ?? 2);\n\n protected readonly error = computed(() => problemText(this.capacity.error()));\n protected readonly mixedEnvironments = computed(() => !this.ctx.environment() && new Set(this.environments()).size > 1);\n protected readonly plan = computed(() => {\n const m = this.capacity.value();\n return m ? planCapacity(m, {\n users: this.users(), thinkSeconds: this.thinkSeconds(), perAction: this.perAction(), targetCpu: this.targetCpu() / 100, coresPerInstance: this.coresPerInstance(),\n }) : null;\n });\n protected readonly busyConsumer = computed(() => { const m = this.capacity.value(); return !!m && consumerBusy(m); });\n protected readonly responseRising = computed(() => {\n const m = this.capacity.value();\n return m?.responseLowLoadMs && m.responseHighLoadMs && m.responseHighLoadMs >= RISING_RESPONSE * m.responseLowLoadMs\n ? { low: Math.round(m.responseLowLoadMs), high: Math.round(m.responseHighLoadMs) } : null;\n });\n protected readonly sourceNote = computed(() => this.capacity.value()?.requestSource === 'metrics'\n ? 'Requests are counted from the http.server.request.duration metric, messages from spans.'\n : 'Requests and messages are counted from spans, which undercount if traces are sampled. That makes the cost per request read high. Exporting http.server.request.duration fixes the request count.');\n\n protected readonly notableQueue = NOTABLE_QUEUE;\n protected readonly memory = bytes;\n protected readonly perSecond = rate;\n protected readonly count = (value: number) => Math.round(value).toLocaleString('en-US');\n}\n","import { LinkPipe, ObservabilityLinks } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { apiResource } from '../shared/resource';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { MatTabsModule } from '@angular/material/tabs';\nimport { MatButtonModule } from '@angular/material/button';\nimport { Router, RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { AgoPipe, DurationPipe, PercentPipe, ReasonPipe } from '../shared/format';\nimport { ConfidenceBadge, ContextBar, HealthBadge } from '../shared/ui';\nimport { LogTable, MetricExplorer, ServiceStatsCharts, TraceTable, problemText } from '../shared/telemetry';\nimport { ServiceCapacity } from '../shared/capacity';\n\nconst TABS = ['overview', 'operations', 'dependencies', 'traces', 'logs', 'metrics', 'capacity'] as const;\n\n/**\n * One service across its environments (Console requirements §8): health with reasons, instances, operations,\n * dependencies, traces, logs and metrics, without leaving the service. The tab and the selected operation are in the\n * URL, so the investigation can be shared and the back button returns to it.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, MatTabsModule, MatProgressBarModule, MatButtonModule, ContextBar, HealthBadge, ConfidenceBadge,\n TraceTable, LogTable, MetricExplorer, ServiceStatsCharts, ServiceCapacity, AgoPipe, DurationPipe, PercentPipe, ReasonPipe],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['services'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Services</a>\n <span class=\"jc-crumb-sep\">›</span>\n <h1>{{ service.value()?.key ?? '…' }}</h1>\n @for (l of service.value()?.links ?? []; track l.url) { <a mat-button [href]=\"l.url\" target=\"_blank\" rel=\"noopener\">{{ l.title }} ↗</a> }\n </jc-context-bar>\n @if (service.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (service.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n\n @if (service.value(); as s) {\n <div class=\"jc-health-strip\">\n @for (h of health(); track h.environment) {\n <div class=\"jc-card jc-health-card\" [class.jc-selected]=\"h.environment === ctx.environment()\">\n <div class=\"jc-row\"><strong>{{ h.environment ?? 'no environment' }}</strong><jc-health [state]=\"h.state\" [stale]=\"h.stale\" /></div>\n <ul class=\"jc-reasons\">\n @for (r of h.reasons; track r) {\n <li>\n @if (dependencyOf(r); as dep) {\n {{ r | reason }} — <a [routerLink]=\"['services', dep.id] | jcLink\" [queryParams]=\"ctx.keep()\">open</a>\n } @else { {{ r | reason }} }\n </li>\n }\n </ul>\n <div class=\"jc-muted jc-small\">since {{ h.since | ago }} · evaluated {{ h.evaluatedAt | ago }}</div>\n @if (h.environment && h.environment !== ctx.environment()) {\n <button mat-button (click)=\"ctx.set({ env: h.environment })\">Investigate {{ h.environment }}</button>\n }\n </div>\n } @empty { <p class=\"jc-muted\">No health evaluated yet: no data.</p> }\n </div>\n\n <mat-tab-group [selectedIndex]=\"tabIndex()\" (selectedIndexChange)=\"openTab($event)\" animationDuration=\"0ms\" mat-stretch-tabs=\"false\">\n <mat-tab label=\"Overview\">\n @if (tabIndex() === 0) {\n <jc-service-stats [query]=\"{ serviceId: id() }\" />\n <h3>Instances</h3>\n <table class=\"jc-table\">\n <thead><tr><th>Instance</th><th>Environment</th><th>Health</th><th>Version</th><th>State</th><th>Last seen</th><th></th></tr></thead>\n <tbody>\n @for (i of instances(); track i.id) {\n <tr>\n <td>{{ i.instanceKey }}@if (i.keySource !== 'Observed') { <span class=\"jc-muted jc-small\" title=\"No service.instance.id: derived from host and process\"> (derived)</span> }</td>\n <td>{{ i.environment }}</td>\n <td><jc-health [state]=\"i.health\" /></td>\n <td>{{ i.serviceVersion ?? '—' }}</td>\n <td>{{ i.state }}</td>\n <td>{{ i.lastSeen | ago }}</td>\n <td class=\"jc-nowrap\">\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', instance: i.id })\">traces</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'logs', instance: i.id })\">logs</a>\n @if (i.hostResourceId) { · <a [routerLink]=\"['resources', i.hostResourceId] | jcLink\" [queryParams]=\"ctx.keep()\">host</a> }\n </td>\n </tr>\n } @empty { <tr><td colspan=\"7\" class=\"jc-muted\">No instances.</td></tr> }\n </tbody>\n </table>\n }\n </mat-tab>\n\n <mat-tab label=\"Operations\">\n @if (tabIndex() === 1) {\n @if (operations.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n <table class=\"jc-table\">\n <thead><tr><th>Operation</th><th>Kind</th><th>Requests</th><th title=\"Average over the selected time range. The chart shows each moment, so a burst reads higher there.\">Avg/s</th><th>Errors</th><th>p50</th><th>p95</th><th>p99</th><th>Last seen</th><th></th></tr></thead>\n <tbody>\n @for (o of operations.value()?.items ?? []; track o.name + o.kind) {\n <tr [class.jc-selected]=\"o.name === operation()\">\n <td><a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'operations', op: o.name })\">{{ o.name }}</a></td>\n <td>{{ o.kind }}</td>\n <td>{{ o.count }}</td>\n <td>{{ o.requestsPerSecond.toFixed(2) }}</td>\n <td [class.jc-bad]=\"o.errorRate >= 0.05\">{{ o.errorRate | percent2 }}</td>\n <td>{{ o.p50Ms | duration }}</td><td>{{ o.p95Ms | duration }}</td><td>{{ o.p99Ms | duration }}</td>\n <td>{{ o.lastSeen | ago }}</td>\n <td class=\"jc-nowrap\">\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', op: o.name, status: 'error' })\">error traces</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', op: o.name, slow: o.p95Ms })\">slow traces</a>\n </td>\n </tr>\n } @empty { <tr><td colspan=\"10\" class=\"jc-muted\">No operations in this window.</td></tr> }\n </tbody>\n </table>\n @if (operation(); as op) {\n <h3>{{ op }}</h3>\n <jc-service-stats [query]=\"{ serviceId: id(), operation: op }\" />\n }\n }\n </mat-tab>\n\n <mat-tab label=\"Dependencies\">\n @if (tabIndex() === 2) {\n <h3>Calls and uses</h3>\n <table class=\"jc-table\">\n <thead><tr><th>Target</th><th>Relation</th><th>Evidence</th><th>Spans seen</th><th>Last seen</th><th></th></tr></thead>\n <tbody>\n @for (d of downstream(); track d.edge.id) {\n <tr>\n <td><a [routerLink]=\"d.link\" [queryParams]=\"ctx.keep()\">{{ d.name }}</a>&nbsp;<span class=\"jc-muted jc-small\">{{ d.type }}</span></td>\n <td>{{ d.edge.relation }}</td>\n <td><jc-confidence [confidence]=\"d.edge.confidence\" /></td>\n <td>{{ d.edge.spanCount }}</td>\n <td>{{ d.edge.lastSeen | ago }}</td>\n <td><a [routerLink]=\"['topology'] | jcLink\" [queryParams]=\"ctx.keep({ edge: d.edge.id })\">evidence</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', dependency: d.edge.id })\">traces</a></td>\n </tr>\n } @empty { <tr><td colspan=\"6\" class=\"jc-muted\">No downstream dependencies seen.</td></tr> }\n </tbody>\n </table>\n <h3>Called by</h3>\n <table class=\"jc-table\">\n <tbody>\n @for (d of upstream(); track d.edge.id) {\n <tr><td><a [routerLink]=\"d.link\" [queryParams]=\"ctx.keep()\">{{ d.name }}</a></td><td>{{ d.edge.relation }}</td>\n <td><jc-confidence [confidence]=\"d.edge.confidence\" /></td><td>{{ d.edge.lastSeen | ago }}</td>\n <td><a [routerLink]=\"['topology'] | jcLink\" [queryParams]=\"ctx.keep({ edge: d.edge.id })\">evidence</a></td></tr>\n } @empty { <tr><td class=\"jc-muted\">No callers seen.</td></tr> }\n </tbody>\n </table>\n }\n </mat-tab>\n\n <mat-tab label=\"Traces\">\n @if (tabIndex() === 3) {\n <div class=\"jc-filters\">\n @for (f of traceFilters(); track f.label) { <span class=\"jc-chip\">{{ f.label }} <a [routerLink]=\"[]\" [queryParams]=\"f.clear\" queryParamsHandling=\"merge\" [attr.aria-label]=\"'Remove ' + f.label\">×</a></span> }\n </div>\n <jc-trace-table [query]=\"traceQuery()\" />\n }\n </mat-tab>\n\n <mat-tab label=\"Logs\">\n @if (tabIndex() === 4) { <jc-log-table [query]=\"{ serviceId: id(), instanceId: instanceFilter() }\" /> }\n </mat-tab>\n\n <mat-tab label=\"Metrics\">\n @if (tabIndex() === 5) { <jc-metric-explorer [scope]=\"{ serviceId: id(), instanceId: instanceFilter() }\" /> }\n </mat-tab>\n\n <mat-tab label=\"Capacity\">\n @if (tabIndex() === 6) { <jc-service-capacity [serviceId]=\"id()\" [environments]=\"environments()\" /> }\n </mat-tab>\n </mat-tab-group>\n }`,\n})\nexport class ServicePage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n private readonly router = inject(Router);\n private readonly links = inject(ObservabilityLinks);\n\n readonly id = input.required<string>();\n readonly tab = input<string>();\n readonly op = input<string>();\n readonly status = input<string>();\n readonly slow = input<string>();\n readonly instance = input<string>();\n readonly dependency = input<string>();\n\n protected readonly tabIndex = computed(() => Math.max(0, TABS.indexOf((this.tab() ?? 'overview') as typeof TABS[number])));\n protected readonly operation = computed(() => this.op() ?? null);\n protected readonly instanceFilter = computed(() => this.instance() ?? null);\n\n protected readonly service = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.resource(params) });\n protected readonly topology = apiResource({ params: () => this.ctx.environment(), stream: ({ params }) => this.api.topology(params || null) });\n protected readonly operations = apiResource({\n params: () => ({ id: this.id(), env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute(), on: this.tabIndex() === 1 }),\n stream: ({ params }) => this.api.operations({ serviceId: params.id, environment: params.env, ...this.ctx.window(), limit: 200 }),\n });\n\n protected readonly error = computed(() => problemText(this.service.error()));\n protected readonly health = computed(() => (this.service.value()?.health ?? []).filter(h => h.subjectType === 'service'));\n protected readonly instances = computed(() => {\n const env = this.ctx.environment();\n return (this.service.value()?.instances ?? []).filter(i => !env || i.environment === env);\n });\n\n protected readonly environments = computed(() => (this.service.value()?.instances ?? []).map(i => i.environment));\n\n private readonly nodes = computed(() => new Map((this.topology.value()?.nodes ?? []).map(n => [n.id, n])));\n private edgesWhere(match: 'source' | 'target') {\n const other = match === 'source' ? 'target' : 'source';\n return (this.topology.value()?.edges ?? []).filter(e => e[match] === this.id()).map(edge => {\n const node = this.nodes().get(edge[other]);\n return {\n edge, name: node?.displayName ?? edge[other], type: node?.type ?? '',\n link: this.links.to(node?.type === 'service' ? 'services' : 'resources', edge[other]),\n };\n });\n }\n protected readonly downstream = computed(() => this.edgesWhere('source'));\n protected readonly upstream = computed(() => this.edgesWhere('target'));\n\n /** `dependency_failed:shop/payments` links to that service when it is in the topology. */\n dependencyOf(reason: string) {\n const name = reason.match(/^(?:critical_)?dependency_(?:failed|unknown):(.+)$/)?.[1];\n if (!name) return null;\n const node = [...this.nodes().values()].find(n => n.type === 'service' && (n.displayName === name || name.endsWith(`/${n.displayName}`)));\n return node ? { id: node.id } : null;\n }\n\n protected readonly traceQuery = computed(() => ({\n serviceId: this.id(), operation: this.op(), status: this.status(), minDurationMs: this.slow(),\n instanceId: this.instance(), dependencyId: this.dependency(),\n }));\n /** The trace filters in effect, each removable on its own. */\n protected readonly traceFilters = computed(() => {\n const filters: { label: string; clear: Record<string, null> }[] = [];\n if (this.op()) filters.push({ label: `operation: ${this.op()}`, clear: { op: null } });\n if (this.status()) filters.push({ label: `status: ${this.status()}`, clear: { status: null } });\n if (this.slow()) filters.push({ label: `slower than ${Math.round(Number(this.slow()))} ms`, clear: { slow: null } });\n if (this.instance()) filters.push({ label: 'one instance', clear: { instance: null } });\n if (this.dependency()) filters.push({ label: 'through one dependency', clear: { dependency: null } });\n return filters;\n });\n\n openTab(index: number) {\n void this.router.navigate([], { queryParams: { tab: TABS[index] }, queryParamsHandling: 'merge' });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA2CA,SAAgB,aAAa,GAAa,GAAiB;CACzD,IAAI,CAAC,EAAE,qBAAqB,EAAE,qBAAqB,GAAG,OAAO;CAC7D,IAAI,EAAE,EAAE,QAAQ,KAAK,EAAE,YAAY,KAAK,EAAE,YAAY,KAAK,EAAE,aAAa,KAAK,EAAE,mBAAmB,KAAK,EAAE,gBAAgB,IAAI,OAAO;CACtI,MAAM,YAAY,EAAE,cAAc,KAAK;CACvC,IAAI,EAAE,eAAe,YAAY,GAAG,OAAO;CAE3C,MAAM,SAAS,EAAE,YAAY,EAAE,oBAAoB,EAAE,aAAa,MAAM,EAAE;CAC1E,IAAI,SAAS,GAAG,OAAO;CACvB,MAAM,YAAY,aAAa,CAAC,KAAK,EAAE,wBAAwB,QAAQ,aAAa;CACpF,MAAM,UAAU,cAAc,aAAa,EAAE,wBAAwB;CACrE,MAAM,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,eAAe;CAC5D,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,YAAY,OAAO,CAAC;CAC5D,MAAM,cAAc,YAAY;CAChC,MAAM,cAAc,EAAE,gBAAgB,WAClC,EAAE,kBAAmB,EAAE,yBAA0B,cAAc,WAC/D,EAAE;CAKN,OAAO;EACL;EAAW;EAAW,OAAO,aAJb,cAAc,cAAc,EAAE,cAAc,OAC1D,KAAK,IAAI,EAAE,kBAAkB,KAAK,KAAK,EAAE,YAAY,EAAE,SAAS,CAAC,IACjE,EAAE;EAEgD;EAAa;EACjE,gBAAgB,EAAE,gBAAgB,IAAI,cAAc,EAAE,gBAAgB;EACtE;CACD;AACH;AAGA,MAAM,gBAAgB;AACtB,SAAS,aAAa,GAAW;CAAa,OAAO,EAAE,wBAAwB,iBAAiB,EAAE,wBAAwB;AAAG;AAG7H,MAAM,gBAAgB;AAGtB,MAAM,kBAAkB;AAExB,SAAS,MAAM,OAAoB;CACjC,IAAI,UAAU,MAAM,OAAO;CAC3B,OAAO,SAAS,QAAQ,IAAI,IAAI,QAAQ,QAAQ,EAAA,CAAG,QAAQ,CAAC,EAAC,OAAQ,GAAG,KAAK,MAAM,QAAQ,QAAQ,CAAC,EAAC;AACvG;AACA,SAAS,KAAK,OAAa;CAAY,OAAO,SAAS,KAAK,MAAM,QAAQ,CAAC,IAAI,MAAM,QAAQ,CAAC;AAAG;AAsHjG,IAAa,kBAAb,MAAa,gBAAe;CACP,MAAM,OAAO,cAAc;CAC7B,MAAM,OAAO,UAAU;CAC/B,YAAY,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEpC,eAAe,MAAgB,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAExB,WAAW,YAAY;EACxC,eAAe;GAAE,IAAI,KAAK,UAAS;GAAI,KAAK,KAAK,IAAI,YAAW;GAAI,OAAO,KAAK,IAAI,MAAK;GAAI,KAAK,KAAK,IAAI,SAAQ;EAAE;EACrH,SAAS,EAAE,aAAa,KAAK,IAAI,SAAS;GAAE,WAAW,OAAO;GAAI,aAAa,OAAO;GAAK,GAAG,KAAK,IAAI,OAAM;EAAE,CAAE;CAClH,CAAA;CACkB,QAAQ,OAAO,KAAK,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACpB,eAAe,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CACzB,YAAY,OAAO,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CACrB,YAAY,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CACtB,mBAAmB,mBAAmB,KAAK,SAAS,MAAK,CAAE,EAAE,oBAAoB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,mBAAA,CAAA,+BAAA,CAAA,CAAA;CAEnF,QAAQ,eAAe,YAAY,KAAK,SAAS,MAAK,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAC1D,oBAAoB,eAAe,CAAC,KAAK,IAAI,YAAW,KAAM,IAAI,IAAI,KAAK,aAAY,CAAE,CAAC,CAAC,OAAO,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,oBAAA,CAAA,+BAAA,CAAA,CAAA;CACpG,OAAO,eAAc;EACtC,MAAM,IAAI,KAAK,SAAS,MAAK;EAC7B,OAAO,IAAI,aAAa,GAAG;GACzB,OAAO,KAAK,MAAK;GAAI,cAAc,KAAK,aAAY;GAAI,WAAW,KAAK,UAAS;GAAI,WAAW,KAAK,UAAS,IAAK;GAAK,kBAAkB,KAAK,iBAAgB;EAChK,CAAA,IAAI;CACP,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACiB,eAAe,eAAc;EAAG,MAAM,IAAI,KAAK,SAAS,MAAK;EAAI,OAAO,CAAC,CAAC,KAAK,aAAa,CAAC;CAAG,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAClG,iBAAiB,eAAc;EAChD,MAAM,IAAI,KAAK,SAAS,MAAK;EAC7B,OAAO,GAAG,qBAAqB,EAAE,sBAAsB,EAAE,sBAAsB,kBAAkB,EAAE,oBAC/F;GAAE,KAAK,KAAK,MAAM,EAAE,iBAAiB;GAAG,MAAM,KAAK,MAAM,EAAE,kBAAkB;EAAC,IAAK;CACzF,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CACiB,aAAa,eAAe,KAAK,SAAS,MAAK,CAAE,EAAE,kBAAkB,YACpF,4FACA,oMAAmM,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEpL,eAAe;CACf,SAAS;CACT,YAAY;CACZ,SAAS,UAAkB,KAAK,MAAM,KAAK,CAAC,CAAC,eAAe,OAAO;CAtC3E,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAe,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAf,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAe,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,WAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UA3GhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyGN,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MA1GM;GAAW;GAAA;IAAA,MAAA;IAAA,MAAAA,KAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAAA,KAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAAA,KAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAAA,KAAA;IAAA,UAAA;IAAA,QAAA,CAAA,KAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAAA,KAAA;IAAA,UAAA;IAAA,QAAA,CAAA,KAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAAA,KAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,eAAA;IAAA,UAAA,CAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAkB;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,cAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAc;GAAA;IAAA,MAAA;IAAA,MAAAC,KAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAoB;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,cAAA;IAAA,UAAA,CAAA,gBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAQ,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AA4G1F,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAe,YAAA,CAAA;EA/G3B,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,iBAAiB,wBAAwB;GACzC,SAAS;IAAC;IAAa;IAAoB;IAAgB;IAAsB;IAAY;GAAQ;GACrG,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0GX,CAAA;;;;;;;;;;;;;;;;;;;;;AC3LD,MAAM,OAAO;CAAC;CAAY;CAAc;CAAgB;CAAU;CAAQ;CAAW;AAAU;AA4J/F,IAAa,cAAb,MAAa,YAAW;CACH,MAAM,OAAO,cAAc;CAC7B,MAAM,OAAO,UAAU;CACvB,SAAS,OAAO,MAAM;CACtB,QAAQ,OAAO,kBAAkB;CAEzC,KAAK,MAAM,SAAkB,GAAA,YAAA,CAAA,EAAA,WAAA,KAAA,CAAA,+BAAA,CAAA,CAAA;CAC7B,MAAM,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,MAAA,CAAA,+BAAA,CAAA,CAAA;CACrB,KAAK,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,KAAA,CAAA,+BAAA,CAAA,CAAA;CACpB,SAAS,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CACxB,OAAO,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACtB,WAAW,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAC1B,aAAa,MAAe,GAAA,YAAA,CAAA,QAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAElB,WAAW,eAAe,KAAK,IAAI,GAAG,KAAK,QAAS,KAAK,IAAG,KAAM,UAAkC,CAAC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CACvG,YAAY,eAAe,KAAK,GAAE,KAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAC7C,iBAAiB,eAAe,KAAK,SAAQ,KAAM,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAExD,UAAU,YAAY;EAAE,cAAc,KAAK,GAAE;EAAI,SAAS,EAAE,aAAa,KAAK,IAAI,SAAS,MAAM;CAAC,CAAE;CACpG,WAAW,YAAY;EAAE,cAAc,KAAK,IAAI,YAAW;EAAI,SAAS,EAAE,aAAa,KAAK,IAAI,SAAS,UAAU,IAAI;CAAC,CAAE;CAC1H,aAAa,YAAY;EAC1C,eAAe;GAAE,IAAI,KAAK,GAAE;GAAI,KAAK,KAAK,IAAI,YAAW;GAAI,OAAO,KAAK,IAAI,MAAK;GAAI,KAAK,KAAK,IAAI,SAAQ;GAAI,IAAI,KAAK,SAAQ,MAAO;EAAC;EACzI,SAAS,EAAE,aAAa,KAAK,IAAI,WAAW;GAAE,WAAW,OAAO;GAAI,aAAa,OAAO;GAAK,GAAG,KAAK,IAAI,OAAM;GAAI,OAAO;EAAG,CAAE;CAChI,CAAA;CAEkB,QAAQ,eAAe,YAAY,KAAK,QAAQ,MAAK,CAAE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACzD,SAAS,gBAAgB,KAAK,QAAQ,MAAK,CAAE,EAAE,UAAU,CAAA,EAAA,CAAI,QAAO,MAAK,EAAE,gBAAgB,SAAS,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CACtG,YAAY,eAAc;EAC3C,MAAM,MAAM,KAAK,IAAI,YAAW;EAChC,QAAQ,KAAK,QAAQ,MAAK,CAAE,EAAE,aAAa,CAAA,EAAA,CAAI,QAAO,MAAK,CAAC,OAAO,EAAE,gBAAgB,GAAG;CAC1F,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAEiB,eAAe,gBAAgB,KAAK,QAAQ,MAAK,CAAE,EAAE,aAAa,CAAA,EAAA,CAAI,KAAI,MAAK,EAAE,WAAW,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEhG,QAAQ,eAAe,IAAI,KAAK,KAAK,SAAS,MAAK,CAAE,EAAE,SAAS,CAAA,EAAA,CAAI,KAAI,MAAK,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAClG,WAAW,OAA0B;EAC3C,MAAM,QAAQ,UAAU,WAAW,WAAW;EAC9C,QAAQ,KAAK,SAAS,MAAK,CAAE,EAAE,SAAS,CAAA,EAAA,CAAI,QAAO,MAAK,EAAE,WAAW,KAAK,GAAE,CAAE,CAAC,CAAC,KAAI,SAAO;GACzF,MAAM,OAAO,KAAK,MAAK,CAAE,CAAC,IAAI,KAAK,MAAM;GACzC,OAAO;IACL;IAAM,MAAM,MAAM,eAAe,KAAK;IAAQ,MAAM,MAAM,QAAQ;IAClE,MAAM,KAAK,MAAM,GAAG,MAAM,SAAS,YAAY,aAAa,aAAa,KAAK,MAAM;GACrF;EACH,CAAC;CACH;CACmB,aAAa,eAAe,KAAK,WAAW,QAAQ,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CACtD,WAAW,eAAe,KAAK,WAAW,QAAQ,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGvE,aAAa,QAAc;EACzB,MAAM,OAAO,OAAO,MAAM,oDAAoD,CAAC,GAAG;EAClF,IAAI,CAAC,MAAM,OAAO;EAClB,MAAM,OAAO,CAAC,GAAG,KAAK,MAAK,CAAE,CAAC,OAAM,CAAE,CAAC,CAAC,MAAK,MAAK,EAAE,SAAS,cAAc,EAAE,gBAAgB,QAAQ,KAAK,SAAS,IAAI,EAAE,aAAa,EAAE;EACxI,OAAO,OAAO,EAAE,IAAI,KAAK,GAAE,IAAK;CAClC;CAEmB,aAAa,gBAAgB;EAC9C,WAAW,KAAK,GAAE;EAAI,WAAW,KAAK,GAAE;EAAI,QAAQ,KAAK,OAAM;EAAI,eAAe,KAAK,KAAI;EAC3F,YAAY,KAAK,SAAQ;EAAI,cAAc,KAAK,WAAU;CAC3D,IAAE,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CAEgB,eAAe,eAAc;EAC9C,MAAM,UAA4D,CAAA;EAClE,IAAI,KAAK,GAAE,GAAI,QAAQ,KAAK;GAAE,OAAO,cAAc,KAAK,GAAE;GAAM,OAAO,EAAE,IAAI,KAAI;EAAE,CAAE;EACrF,IAAI,KAAK,OAAM,GAAI,QAAQ,KAAK;GAAE,OAAO,WAAW,KAAK,OAAM;GAAM,OAAO,EAAE,QAAQ,KAAI;EAAE,CAAE;EAC9F,IAAI,KAAK,KAAI,GAAI,QAAQ,KAAK;GAAE,OAAO,eAAe,KAAK,MAAM,OAAO,KAAK,KAAI,CAAE,CAAC,EAAC;GAAO,OAAO,EAAE,MAAM,KAAI;EAAE,CAAE;EACnH,IAAI,KAAK,SAAQ,GAAI,QAAQ,KAAK;GAAE,OAAO;GAAgB,OAAO,EAAE,UAAU,KAAI;EAAE,CAAE;EACtF,IAAI,KAAK,WAAU,GAAI,QAAQ,KAAK;GAAE,OAAO;GAA0B,OAAO,EAAE,YAAY,KAAI;EAAE,CAAE;EACpG,OAAO;CACT,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEF,QAAQ,OAAa;EACnB,AAAK,KAAK,OAAO,SAAS,CAAA,GAAI;GAAE,aAAa,EAAE,KAAK,KAAK,OAAM;GAAI,qBAAqB;EAAO,CAAE;CACnG;CAzEW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAW,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAX,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAW,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,IAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,KAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,IAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,QAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,UAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UAjJZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+IN,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MAjJgB;IAAU,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAa;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,QAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,aAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAoB;GAAA;IAAA,MAAA;IAAA,MAAAC,GAAA;IAAA,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA,CAAA,cAAA;IAAA,UAAA,CAAA,gBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;GAAe;GAAA;IAAA,MAAA;IAAA,MAAA,GAAA;IAAA,UAAA;IAAA,QAAA,CAAA,WAAA;IAAA,UAAA,CAAA,aAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAW,UAAA;IAAA,QAAA,CAAA,SAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAe,UAAA;IAAA,QAAA,CAAA,YAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAC5H;IAAU,UAAA;IAAA,QAAA,CAAA,SAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAQ,UAAA;IAAA,QAAA,CAAA,SAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAc,UAAA;IAAA,QAAA,CAAA,SAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAkB,UAAA;IAAA,QAAA,CAAA,OAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAe,UAAA;IAAA,QAAA,CAAA,aAAA,cAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MADjE;IAAQ,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAC2D;IAAO,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAY,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAW,MAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,MAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AAkJhH,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAW,YAAA,CAAA;EArJvB,MAAA;EAAU,MAAA,CAAA;GACT,iBAAiB,wBAAwB;GACzC,SAAS;IAAC;IAAU;IAAY;IAAe;IAAsB;IAAiB;IAAY;IAAa;IAC7G;IAAY;IAAU;IAAgB;IAAoB;IAAiB;IAAS;IAAc;IAAa;GAAU;GAC3H,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgJX,CAAA"}
@@ -188,6 +188,12 @@ var ApiService = class ApiService {
188
188
  capacity(query) {
189
189
  return this.get("/telemetry/capacity", query);
190
190
  }
191
+ loadRuns(query) {
192
+ return this.get("/load-runs", query);
193
+ }
194
+ loadRunResults(id) {
195
+ return this.get(`/load-runs/${id}/results`);
196
+ }
191
197
  dependencyStats(query) {
192
198
  return this.get("/telemetry/dependencies", query);
193
199
  }
@@ -331,7 +337,7 @@ function page(path, title, load) {
331
337
  const OBSERVABILITY_ROUTES = [
332
338
  page("", "Overview", () => import("./juice-js-observability-overview.page-DXZl_fUh.mjs").then((m) => m.OverviewPage)),
333
339
  page("services", "Services", () => import("./juice-js-observability-services.page-B1huLe_a.mjs").then((m) => m.ServicesPage)),
334
- page("services/:id", "Service", () => import("./juice-js-observability-service.page-rGAvUK_k.mjs").then((m) => m.ServicePage)),
340
+ page("services/:id", "Service", () => import("./juice-js-observability-service.page-BrcID0Bs.mjs").then((m) => m.ServicePage)),
335
341
  page("traces", "Traces", () => import("./juice-js-observability-traces.page-DXSDdeTH.mjs").then((m) => m.TracesPage)),
336
342
  page("traces/:traceId", "Trace", () => import("./juice-js-observability-trace.page-ByxGFLpE.mjs").then((m) => m.TracePage)),
337
343
  page("topology", "Dependencies", () => import("./juice-js-observability-topology.page-CUayiRVT.mjs").then((m) => m.TopologyPage)),
@@ -341,7 +347,8 @@ const OBSERVABILITY_ROUTES = [
341
347
  page("resources/:id", "Resource", () => import("./juice-js-observability-resource.page-CauDE6kK.mjs").then((m) => m.ResourcePage)),
342
348
  page("alerts", "Alerts", () => import("./juice-js-observability-alerts.page-DUXXBanH.mjs").then((m) => m.AlertsPage)),
343
349
  page("alerts/:id", "Alert", () => import("./juice-js-observability-alert.page-nEy5Gc6j.mjs").then((m) => m.AlertPage)),
344
- page("rules", "Rules", () => import("./juice-js-observability-rules.page-Crb07uWm.mjs").then((m) => m.RulesPage))
350
+ page("rules", "Rules", () => import("./juice-js-observability-rules.page-Crb07uWm.mjs").then((m) => m.RulesPage)),
351
+ page("load-runs", "Load runs", () => import("./juice-js-observability-load-runs.page-C2rk2bz9.mjs").then((m) => m.LoadRunsPage))
345
352
  ];
346
353
  function observabilityMenus(basePath = "/", order = 1) {
347
354
  const base = normalizeBasePath(basePath);
@@ -400,6 +407,12 @@ function observabilityMenus(basePath = "/", order = 1) {
400
407
  label: "Rules",
401
408
  icon: "rule",
402
409
  routerLink: link("rules")
410
+ },
411
+ {
412
+ id: "load-runs",
413
+ label: "Load runs",
414
+ icon: "speed",
415
+ routerLink: link("load-runs")
403
416
  }
404
417
  ].map((menu, index) => ({
405
418
  ...menu,
@@ -1 +1 @@
1
- {"version":3,"file":"juice-js-observability.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/core/options.ts","../../../../projects/juice-js/observability/src/lib/core/api.service.ts","../../../../projects/juice-js/observability/src/lib/core/context.service.ts","../../../../projects/juice-js/observability/src/lib/observability.routes.ts"],"sourcesContent":["import { EnvironmentProviders, Injectable, InjectionToken, Pipe, PipeTransform, inject, makeEnvironmentProviders } from '@angular/core';\n\n/** How the host app mounts Juice Observability. */\nexport interface ObservabilityOptions {\n /** The Juice API origin; empty when the API serves the app (same origin). */\n apiBase: string;\n /** Where the host mounts {@link OBSERVABILITY_ROUTES}: `/` for the whole app, or a path such as `/observability`. */\n basePath: string;\n /**\n * A link to the audit records of a trace, shown on the trace page: for the Juice Audit pages mounted at `/audit`,\n * `id => ['/audit/traces', id]`. A string is an external URL. Without it, no link.\n */\n auditLink?: ((traceId: string) => unknown[] | string | null) | null;\n}\n\nexport const OBSERVABILITY_OPTIONS = new InjectionToken<ObservabilityOptions>('OBSERVABILITY_OPTIONS', {\n providedIn: 'root',\n factory: () => ({ apiBase: '', basePath: '/', auditLink: null }),\n});\n\n/** Configures the pages: where the API is, where the host mounted the routes, and where a trace's audit records open. */\nexport function provideJuiceObservability(options: Partial<ObservabilityOptions> = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: OBSERVABILITY_OPTIONS,\n useValue: {\n apiBase: options.apiBase ?? '',\n basePath: options.basePath ?? '/',\n auditLink: options.auditLink ?? null,\n } satisfies ObservabilityOptions,\n },\n ]);\n}\n\n/** `/observability/` → `/observability`; `` and `/` → `/`. */\nexport function normalizeBasePath(basePath: string): string {\n const trimmed = basePath.trim().replace(/^\\/+|\\/+$/g, '');\n return trimmed ? `/${trimmed}` : '/';\n}\n\n/** Router links to the pages, under the host's mount path. */\n@Injectable({ providedIn: 'root' })\nexport class ObservabilityLinks {\n private readonly options = inject(OBSERVABILITY_OPTIONS);\n readonly base = normalizeBasePath(this.options.basePath);\n\n /** `to('services', id)` → `['/observability', 'services', id]`. */\n to(...segments: unknown[]): unknown[] { return [this.base, ...segments]; }\n\n /** The host's link to the audit records of a trace, if it gave one. */\n audit(traceId: string | null | undefined): { route: unknown[] } | { href: string } | null {\n if (!traceId || !this.options.auditLink) return null;\n const link = this.options.auditLink(traceId);\n if (!link) return null;\n return typeof link === 'string' ? { href: link } : { route: link };\n }\n}\n\n/** `['services', id] | jcLink`: the same as {@link ObservabilityLinks.to} in a template. */\n@Pipe({ name: 'jcLink' })\nexport class LinkPipe implements PipeTransform {\n private readonly links = inject(ObservabilityLinks);\n transform(segments: unknown[]): unknown[] { return this.links.to(...segments); }\n}\n","import { HttpClient, HttpParams } from '@angular/common/http';\nimport { Injectable, inject } from '@angular/core';\nimport { EMPTY, Observable, expand, map, reduce, shareReplay, switchMap } from 'rxjs';\nimport { OBSERVABILITY_OPTIONS } from './options';\nimport {\n AlertDetail, Alert, Application, DependencyEvidence, DependencyStatsPage, DeploymentEnvironment, Health, LogRecord, MetricInfo, Operation,\n Page, ResourceDetail, ResourceSummary, Rule, SearchHit, Series, Capacity, ServiceStats, Tenant, TelemetryPage, Topology, Trace,\n TraceSummary,\n} from './models';\n\nexport type Query = Record<string, string | number | boolean | string[] | null | undefined>;\n\n/**\n * The Console's only backend: the Juice API (Console requirements §3). One deployment serves one tenant, the one the\n * signed-in user's token names.\n */\n@Injectable({ providedIn: 'root' })\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly base = `${inject(OBSERVABILITY_OPTIONS).apiBase}/api/v1`;\n\n /** The caller's tenant, fetched once. */\n readonly tenant$: Observable<Tenant> = this.http.get<Page<Tenant>>(`${this.base}/tenants`).pipe(\n map(page => {\n if (page.items.length === 0) throw new Error('No tenant is authorized for this user.');\n return page.items[0];\n }),\n shareReplay(1),\n );\n\n private get<T>(path: string, query: Query = {}): Observable<T> {\n let params = new HttpParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined || value === '') continue;\n for (const item of Array.isArray(value) ? value : [value]) params = params.append(key, String(item));\n }\n return this.tenant$.pipe(switchMap(t => this.http.get<T>(`${this.base}/tenants/${t.id}${path}`, { params })));\n }\n\n /** Every page of a cursor-paged list (bounded: at most `maxPages` requests). */\n private all<T>(path: string, query: Query = {}, maxPages = 25): Observable<Page<T>> {\n let pages = 0;\n return this.get<Page<T>>(path, { ...query, limit: 200 }).pipe(\n expand(page => page.nextCursor && ++pages < maxPages ? this.get<Page<T>>(path, { ...query, limit: 200, cursor: page.nextCursor }) : EMPTY),\n reduce((all, page) => ({ items: [...all.items, ...page.items], nextCursor: page.nextCursor }), { items: [] as T[], nextCursor: null as string | null }),\n );\n }\n\n environments() { return this.get<Page<DeploymentEnvironment>>('/environments'); }\n applications() { return this.get<Page<Application>>('/applications', { limit: 200 }); }\n resources(type?: string) { return this.all<ResourceSummary>('/resources', { type }); }\n resource(id: string) { return this.get<ResourceDetail>(`/resources/${id}`); }\n health(query: Query) { return this.all<Health>('/health', query); }\n topology(environment?: string | null) { return this.get<Topology>('/topology', { environment }); }\n\n rules() { return this.get<Page<Rule>>('/rules', { limit: 200 }); }\n /** One page of alerts, newest first; pass the previous page's `nextCursor` as `cursor` for the next. */\n alerts(query: Query) { return this.get<Page<Alert>>('/alerts', { limit: 100, ...query }); }\n /** Every alert matching the query (bounded, like the other complete lists). */\n allAlerts(query: Query) { return this.all<Alert>('/alerts', query); }\n alert(id: string) { return this.get<AlertDetail>(`/alerts/${id}`); }\n acknowledge(id: string, comment: string): Observable<Alert> {\n return this.tenant$.pipe(switchMap(t => this.http.post<Alert>(`${this.base}/tenants/${t.id}/alerts/${id}/acknowledgement`,\n { comment }, { headers: { 'Idempotency-Key': crypto.randomUUID() } })));\n }\n\n traces(query: Query) { return this.get<TelemetryPage<TraceSummary>>('/telemetry/traces', query); }\n trace(traceId: string) { return this.get<Trace>(`/telemetry/traces/${traceId}`); }\n logs(query: Query) { return this.get<TelemetryPage<LogRecord>>('/telemetry/logs', query); }\n operations(query: Query) { return this.get<TelemetryPage<Operation>>('/telemetry/operations', query); }\n metrics(query: Query) { return this.get<TelemetryPage<MetricInfo>>('/telemetry/metrics', query); }\n series(query: Query) { return this.get<Series>('/telemetry/metrics/series', query); }\n serviceStats(query: Query) { return this.get<ServiceStats>('/telemetry/service-stats', query); }\n capacity(query: Query) { return this.get<Capacity>('/telemetry/capacity', query); }\n dependencyStats(query: Query) { return this.get<DependencyStatsPage>('/telemetry/dependencies', query); }\n dependency(id: string, query: Query) { return this.get<DependencyEvidence>(`/telemetry/dependencies/${id}`, query); }\n search(q: string) { return this.get<TelemetryPage<SearchHit>>('/search', { q }); }\n}\n","import { Injectable, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';\nimport { filter, map, startWith } from 'rxjs';\n\nexport const RANGES: Record<string, number> = {\n '15m': 15 * 60_000, '1h': 3_600_000, '6h': 6 * 3_600_000, '24h': 24 * 3_600_000, '7d': 7 * 24 * 3_600_000,\n};\n\n/** Search ranges are at most 24 hours (the API's bound); series and operations take up to 7 days. */\nexport const MAX_SEARCH_MS = RANGES['24h'];\n\n/**\n * The investigation context that follows the user between pages (Console requirements §17): environment and time\n * range. It lives in the URL (`env`, and `range` or `from`/`to`), so every page is a deep link and the browser's back\n * button restores it.\n */\n@Injectable({ providedIn: 'root' })\nexport class ContextService {\n private readonly router = inject(Router);\n\n // Created on first use, after the first navigation may already have ended: start from the current URL.\n private readonly params = toSignal(this.router.events.pipe(\n filter(e => e instanceof NavigationEnd),\n startWith(null),\n map(() => this.leaf().snapshot.queryParams),\n ), { initialValue: {} as Params });\n\n readonly environment = computed(() => (this.params()['env'] as string | undefined) ?? '');\n readonly range = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? 'custom' : ((p['range'] as string | undefined) ?? '1h');\n });\n readonly absolute = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? { from: p['from'] as string, to: p['to'] as string } : null;\n });\n\n /** The window as ISO strings, evaluated now for a relative range. */\n window(maxMs?: number): { from: string; to: string } {\n const absolute = this.absolute();\n if (absolute) return absolute;\n const to = Date.now();\n const span = Math.min(RANGES[this.range()] ?? RANGES['1h'], maxMs ?? Number.MAX_SAFE_INTEGER);\n return { from: new Date(to - span).toISOString(), to: new Date(to).toISOString() };\n }\n\n /** Query parameters for a link: the context plus the target page's own. */\n keep(extra: Params = {}): Params {\n const p = this.params();\n const context: Params = {};\n for (const key of ['env', 'range', 'from', 'to']) if (p[key]) context[key] = p[key];\n return { ...context, ...extra };\n }\n\n /** Changes the context on the current page, keeping its other parameters. */\n set(change: { env?: string | null; range?: string | null; from?: string | null; to?: string | null }) {\n const queryParams: Params = { ...change };\n if (change.range) Object.assign(queryParams, { from: null, to: null });\n void this.router.navigate([], { relativeTo: this.leaf(), queryParams, queryParamsHandling: 'merge' });\n }\n\n /** A time window centred on a moment, for \"around this span\" links. */\n around(at: string, durationMs: number, paddingMs = 5 * 60_000): Params {\n const start = new Date(at).getTime();\n return this.keep({\n from: new Date(start - paddingMs).toISOString(),\n to: new Date(start + durationMs + paddingMs).toISOString(),\n range: null,\n });\n }\n\n private leaf(): ActivatedRoute {\n let route = this.router.routerState.root;\n while (route.firstChild) route = route.firstChild;\n return route;\n }\n}\n","import { Routes } from '@angular/router';\nimport { Menus } from '@juice-js/core';\nimport { normalizeBasePath } from './core/options';\n\nfunction page(path: string, title: string, load: Routes[number]['loadComponent']): Routes[number] {\n return { path, title: `${title} · Juice Observability`, loadComponent: load };\n}\n\n/**\n * The pages, investigation first (Console requirements §4): services, traces, topology, logs and metrics; alerts and\n * rules after. Detail pages are deep links (§17) and carry the environment and time range in their query parameters.\n * The host mounts them at its `basePath` (see {@link provideJuiceObservability}) and adds its own guards.\n */\nexport const OBSERVABILITY_ROUTES: Routes = [\n page('', 'Overview', () => import('./pages/overview.page').then(m => m.OverviewPage)),\n page('services', 'Services', () => import('./pages/services.page').then(m => m.ServicesPage)),\n page('services/:id', 'Service', () => import('./pages/service.page').then(m => m.ServicePage)),\n page('traces', 'Traces', () => import('./pages/traces.page').then(m => m.TracesPage)),\n page('traces/:traceId', 'Trace', () => import('./pages/trace.page').then(m => m.TracePage)),\n page('topology', 'Dependencies', () => import('./pages/topology.page').then(m => m.TopologyPage)),\n page('logs', 'Logs', () => import('./pages/logs.page').then(m => m.LogsPage)),\n page('metrics', 'Metrics', () => import('./pages/metrics.page').then(m => m.MetricsPage)),\n page('resources', 'Resources', () => import('./pages/resources.page').then(m => m.ResourcesPage)),\n page('resources/:id', 'Resource', () => import('./pages/resource.page').then(m => m.ResourcePage)),\n page('alerts', 'Alerts', () => import('./pages/alerts.page').then(m => m.AlertsPage)),\n page('alerts/:id', 'Alert', () => import('./pages/alert.page').then(m => m.AlertPage)),\n page('rules', 'Rules', () => import('./pages/rules.page').then(m => m.RulesPage)),\n];\n\n/** The side menu entries (juice-layout), investigation pages first, linking under `basePath`. */\nexport function observabilityMenus(basePath = '/', order = 1): Menus {\n const base = normalizeBasePath(basePath);\n const link = (path: string) => (base === '/' ? `/${path}` : path ? `${base}/${path}` : base);\n return [\n { id: 'overview', label: 'Overview', icon: 'dashboard', routerLink: link('') },\n { id: 'services', label: 'Services', icon: 'hub', routerLink: link('services') },\n { id: 'traces', label: 'Traces', icon: 'timeline', routerLink: link('traces') },\n { id: 'topology', label: 'Dependencies', icon: 'account_tree', routerLink: link('topology') },\n { id: 'logs', label: 'Logs', icon: 'notes', routerLink: link('logs') },\n { id: 'metrics', label: 'Metrics', icon: 'show_chart', routerLink: link('metrics') },\n { id: 'resources', label: 'Resources', icon: 'dns', routerLink: link('resources') },\n { id: 'alerts', label: 'Alerts', icon: 'notifications', routerLink: link('alerts') },\n { id: 'rules', label: 'Rules', icon: 'rule', routerLink: link('rules') },\n ].map((menu, index) => ({ ...menu, order: order + index }));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"juice-js-observability.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/core/options.ts","../../../../projects/juice-js/observability/src/lib/core/api.service.ts","../../../../projects/juice-js/observability/src/lib/core/context.service.ts","../../../../projects/juice-js/observability/src/lib/observability.routes.ts"],"sourcesContent":["import { EnvironmentProviders, Injectable, InjectionToken, Pipe, PipeTransform, inject, makeEnvironmentProviders } from '@angular/core';\n\n/** How the host app mounts Juice Observability. */\nexport interface ObservabilityOptions {\n /** The Juice API origin; empty when the API serves the app (same origin). */\n apiBase: string;\n /** Where the host mounts {@link OBSERVABILITY_ROUTES}: `/` for the whole app, or a path such as `/observability`. */\n basePath: string;\n /**\n * A link to the audit records of a trace, shown on the trace page: for the Juice Audit pages mounted at `/audit`,\n * `id => ['/audit/traces', id]`. A string is an external URL. Without it, no link.\n */\n auditLink?: ((traceId: string) => unknown[] | string | null) | null;\n}\n\nexport const OBSERVABILITY_OPTIONS = new InjectionToken<ObservabilityOptions>('OBSERVABILITY_OPTIONS', {\n providedIn: 'root',\n factory: () => ({ apiBase: '', basePath: '/', auditLink: null }),\n});\n\n/** Configures the pages: where the API is, where the host mounted the routes, and where a trace's audit records open. */\nexport function provideJuiceObservability(options: Partial<ObservabilityOptions> = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: OBSERVABILITY_OPTIONS,\n useValue: {\n apiBase: options.apiBase ?? '',\n basePath: options.basePath ?? '/',\n auditLink: options.auditLink ?? null,\n } satisfies ObservabilityOptions,\n },\n ]);\n}\n\n/** `/observability/` → `/observability`; `` and `/` → `/`. */\nexport function normalizeBasePath(basePath: string): string {\n const trimmed = basePath.trim().replace(/^\\/+|\\/+$/g, '');\n return trimmed ? `/${trimmed}` : '/';\n}\n\n/** Router links to the pages, under the host's mount path. */\n@Injectable({ providedIn: 'root' })\nexport class ObservabilityLinks {\n private readonly options = inject(OBSERVABILITY_OPTIONS);\n readonly base = normalizeBasePath(this.options.basePath);\n\n /** `to('services', id)` → `['/observability', 'services', id]`. */\n to(...segments: unknown[]): unknown[] { return [this.base, ...segments]; }\n\n /** The host's link to the audit records of a trace, if it gave one. */\n audit(traceId: string | null | undefined): { route: unknown[] } | { href: string } | null {\n if (!traceId || !this.options.auditLink) return null;\n const link = this.options.auditLink(traceId);\n if (!link) return null;\n return typeof link === 'string' ? { href: link } : { route: link };\n }\n}\n\n/** `['services', id] | jcLink`: the same as {@link ObservabilityLinks.to} in a template. */\n@Pipe({ name: 'jcLink' })\nexport class LinkPipe implements PipeTransform {\n private readonly links = inject(ObservabilityLinks);\n transform(segments: unknown[]): unknown[] { return this.links.to(...segments); }\n}\n","import { HttpClient, HttpParams } from '@angular/common/http';\nimport { Injectable, inject } from '@angular/core';\nimport { EMPTY, Observable, expand, map, reduce, shareReplay, switchMap } from 'rxjs';\nimport { OBSERVABILITY_OPTIONS } from './options';\nimport {\n AlertDetail, Alert, Application, DependencyEvidence, DependencyStatsPage, DeploymentEnvironment, LoadRun, LoadRunResults, Health, LogRecord, MetricInfo, Operation,\n Page, ResourceDetail, ResourceSummary, Rule, SearchHit, Series, Capacity, ServiceStats, Tenant, TelemetryPage, Topology, Trace,\n TraceSummary,\n} from './models';\n\nexport type Query = Record<string, string | number | boolean | string[] | null | undefined>;\n\n/**\n * The Console's only backend: the Juice API (Console requirements §3). One deployment serves one tenant, the one the\n * signed-in user's token names.\n */\n@Injectable({ providedIn: 'root' })\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly base = `${inject(OBSERVABILITY_OPTIONS).apiBase}/api/v1`;\n\n /** The caller's tenant, fetched once. */\n readonly tenant$: Observable<Tenant> = this.http.get<Page<Tenant>>(`${this.base}/tenants`).pipe(\n map(page => {\n if (page.items.length === 0) throw new Error('No tenant is authorized for this user.');\n return page.items[0];\n }),\n shareReplay(1),\n );\n\n private get<T>(path: string, query: Query = {}): Observable<T> {\n let params = new HttpParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined || value === '') continue;\n for (const item of Array.isArray(value) ? value : [value]) params = params.append(key, String(item));\n }\n return this.tenant$.pipe(switchMap(t => this.http.get<T>(`${this.base}/tenants/${t.id}${path}`, { params })));\n }\n\n /** Every page of a cursor-paged list (bounded: at most `maxPages` requests). */\n private all<T>(path: string, query: Query = {}, maxPages = 25): Observable<Page<T>> {\n let pages = 0;\n return this.get<Page<T>>(path, { ...query, limit: 200 }).pipe(\n expand(page => page.nextCursor && ++pages < maxPages ? this.get<Page<T>>(path, { ...query, limit: 200, cursor: page.nextCursor }) : EMPTY),\n reduce((all, page) => ({ items: [...all.items, ...page.items], nextCursor: page.nextCursor }), { items: [] as T[], nextCursor: null as string | null }),\n );\n }\n\n environments() { return this.get<Page<DeploymentEnvironment>>('/environments'); }\n applications() { return this.get<Page<Application>>('/applications', { limit: 200 }); }\n resources(type?: string) { return this.all<ResourceSummary>('/resources', { type }); }\n resource(id: string) { return this.get<ResourceDetail>(`/resources/${id}`); }\n health(query: Query) { return this.all<Health>('/health', query); }\n topology(environment?: string | null) { return this.get<Topology>('/topology', { environment }); }\n\n rules() { return this.get<Page<Rule>>('/rules', { limit: 200 }); }\n /** One page of alerts, newest first; pass the previous page's `nextCursor` as `cursor` for the next. */\n alerts(query: Query) { return this.get<Page<Alert>>('/alerts', { limit: 100, ...query }); }\n /** Every alert matching the query (bounded, like the other complete lists). */\n allAlerts(query: Query) { return this.all<Alert>('/alerts', query); }\n alert(id: string) { return this.get<AlertDetail>(`/alerts/${id}`); }\n acknowledge(id: string, comment: string): Observable<Alert> {\n return this.tenant$.pipe(switchMap(t => this.http.post<Alert>(`${this.base}/tenants/${t.id}/alerts/${id}/acknowledgement`,\n { comment }, { headers: { 'Idempotency-Key': crypto.randomUUID() } })));\n }\n\n traces(query: Query) { return this.get<TelemetryPage<TraceSummary>>('/telemetry/traces', query); }\n trace(traceId: string) { return this.get<Trace>(`/telemetry/traces/${traceId}`); }\n logs(query: Query) { return this.get<TelemetryPage<LogRecord>>('/telemetry/logs', query); }\n operations(query: Query) { return this.get<TelemetryPage<Operation>>('/telemetry/operations', query); }\n metrics(query: Query) { return this.get<TelemetryPage<MetricInfo>>('/telemetry/metrics', query); }\n series(query: Query) { return this.get<Series>('/telemetry/metrics/series', query); }\n serviceStats(query: Query) { return this.get<ServiceStats>('/telemetry/service-stats', query); }\n capacity(query: Query) { return this.get<Capacity>('/telemetry/capacity', query); }\n loadRuns(query: Query) { return this.get<Page<LoadRun>>('/load-runs', query); }\n loadRunResults(id: string) { return this.get<LoadRunResults>(`/load-runs/${id}/results`); }\n dependencyStats(query: Query) { return this.get<DependencyStatsPage>('/telemetry/dependencies', query); }\n dependency(id: string, query: Query) { return this.get<DependencyEvidence>(`/telemetry/dependencies/${id}`, query); }\n search(q: string) { return this.get<TelemetryPage<SearchHit>>('/search', { q }); }\n}\n","import { Injectable, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';\nimport { filter, map, startWith } from 'rxjs';\n\nexport const RANGES: Record<string, number> = {\n '15m': 15 * 60_000, '1h': 3_600_000, '6h': 6 * 3_600_000, '24h': 24 * 3_600_000, '7d': 7 * 24 * 3_600_000,\n};\n\n/** Search ranges are at most 24 hours (the API's bound); series and operations take up to 7 days. */\nexport const MAX_SEARCH_MS = RANGES['24h'];\n\n/**\n * The investigation context that follows the user between pages (Console requirements §17): environment and time\n * range. It lives in the URL (`env`, and `range` or `from`/`to`), so every page is a deep link and the browser's back\n * button restores it.\n */\n@Injectable({ providedIn: 'root' })\nexport class ContextService {\n private readonly router = inject(Router);\n\n // Created on first use, after the first navigation may already have ended: start from the current URL.\n private readonly params = toSignal(this.router.events.pipe(\n filter(e => e instanceof NavigationEnd),\n startWith(null),\n map(() => this.leaf().snapshot.queryParams),\n ), { initialValue: {} as Params });\n\n readonly environment = computed(() => (this.params()['env'] as string | undefined) ?? '');\n readonly range = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? 'custom' : ((p['range'] as string | undefined) ?? '1h');\n });\n readonly absolute = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? { from: p['from'] as string, to: p['to'] as string } : null;\n });\n\n /** The window as ISO strings, evaluated now for a relative range. */\n window(maxMs?: number): { from: string; to: string } {\n const absolute = this.absolute();\n if (absolute) return absolute;\n const to = Date.now();\n const span = Math.min(RANGES[this.range()] ?? RANGES['1h'], maxMs ?? Number.MAX_SAFE_INTEGER);\n return { from: new Date(to - span).toISOString(), to: new Date(to).toISOString() };\n }\n\n /** Query parameters for a link: the context plus the target page's own. */\n keep(extra: Params = {}): Params {\n const p = this.params();\n const context: Params = {};\n for (const key of ['env', 'range', 'from', 'to']) if (p[key]) context[key] = p[key];\n return { ...context, ...extra };\n }\n\n /** Changes the context on the current page, keeping its other parameters. */\n set(change: { env?: string | null; range?: string | null; from?: string | null; to?: string | null }) {\n const queryParams: Params = { ...change };\n if (change.range) Object.assign(queryParams, { from: null, to: null });\n void this.router.navigate([], { relativeTo: this.leaf(), queryParams, queryParamsHandling: 'merge' });\n }\n\n /** A time window centred on a moment, for \"around this span\" links. */\n around(at: string, durationMs: number, paddingMs = 5 * 60_000): Params {\n const start = new Date(at).getTime();\n return this.keep({\n from: new Date(start - paddingMs).toISOString(),\n to: new Date(start + durationMs + paddingMs).toISOString(),\n range: null,\n });\n }\n\n private leaf(): ActivatedRoute {\n let route = this.router.routerState.root;\n while (route.firstChild) route = route.firstChild;\n return route;\n }\n}\n","import { Routes } from '@angular/router';\nimport { Menus } from '@juice-js/core';\nimport { normalizeBasePath } from './core/options';\n\nfunction page(path: string, title: string, load: Routes[number]['loadComponent']): Routes[number] {\n return { path, title: `${title} · Juice Observability`, loadComponent: load };\n}\n\n/**\n * The pages, investigation first (Console requirements §4): services, traces, topology, logs and metrics; alerts and\n * rules after. Detail pages are deep links (§17) and carry the environment and time range in their query parameters.\n * The host mounts them at its `basePath` (see {@link provideJuiceObservability}) and adds its own guards.\n */\nexport const OBSERVABILITY_ROUTES: Routes = [\n page('', 'Overview', () => import('./pages/overview.page').then(m => m.OverviewPage)),\n page('services', 'Services', () => import('./pages/services.page').then(m => m.ServicesPage)),\n page('services/:id', 'Service', () => import('./pages/service.page').then(m => m.ServicePage)),\n page('traces', 'Traces', () => import('./pages/traces.page').then(m => m.TracesPage)),\n page('traces/:traceId', 'Trace', () => import('./pages/trace.page').then(m => m.TracePage)),\n page('topology', 'Dependencies', () => import('./pages/topology.page').then(m => m.TopologyPage)),\n page('logs', 'Logs', () => import('./pages/logs.page').then(m => m.LogsPage)),\n page('metrics', 'Metrics', () => import('./pages/metrics.page').then(m => m.MetricsPage)),\n page('resources', 'Resources', () => import('./pages/resources.page').then(m => m.ResourcesPage)),\n page('resources/:id', 'Resource', () => import('./pages/resource.page').then(m => m.ResourcePage)),\n page('alerts', 'Alerts', () => import('./pages/alerts.page').then(m => m.AlertsPage)),\n page('alerts/:id', 'Alert', () => import('./pages/alert.page').then(m => m.AlertPage)),\n page('rules', 'Rules', () => import('./pages/rules.page').then(m => m.RulesPage)),\n page('load-runs', 'Load runs', () => import('./pages/load-runs.page').then(m => m.LoadRunsPage)),\n];\n\n/** The side menu entries (juice-layout), investigation pages first, linking under `basePath`. */\nexport function observabilityMenus(basePath = '/', order = 1): Menus {\n const base = normalizeBasePath(basePath);\n const link = (path: string) => (base === '/' ? `/${path}` : path ? `${base}/${path}` : base);\n return [\n { id: 'overview', label: 'Overview', icon: 'dashboard', routerLink: link('') },\n { id: 'services', label: 'Services', icon: 'hub', routerLink: link('services') },\n { id: 'traces', label: 'Traces', icon: 'timeline', routerLink: link('traces') },\n { id: 'topology', label: 'Dependencies', icon: 'account_tree', routerLink: link('topology') },\n { id: 'logs', label: 'Logs', icon: 'notes', routerLink: link('logs') },\n { id: 'metrics', label: 'Metrics', icon: 'show_chart', routerLink: link('metrics') },\n { id: 'resources', label: 'Resources', icon: 'dns', routerLink: link('resources') },\n { id: 'alerts', label: 'Alerts', icon: 'notifications', routerLink: link('alerts') },\n { id: 'rules', label: 'Rules', icon: 'rule', routerLink: link('rules') },\n { id: 'load-runs', label: 'Load runs', icon: 'speed', routerLink: link('load-runs') },\n ].map((menu, index) => ({ ...menu, order: order + index }));\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juice-js/observability",
3
- "version": "1.1.0-alpha.20",
3
+ "version": "1.1.0-alpha.21",
4
4
  "description": "Juice Observability pages (services, traces, topology, logs, metrics, resources, alerts, rules) for an Angular app on juice-layout",
5
5
  "author": "jack@creatorflow.io",
6
6
  "license": "MIT",
@@ -166,6 +166,8 @@
166
166
  .jc-capacity-result strong { font-size: 24px; font-weight: 500; line-height: 1.2; }
167
167
  .jc-capacity-notes { margin: 12px 0 0; padding-left: 18px; font-size: 13px; }
168
168
  .jc-capacity-measured th { width: 240px; }
169
+ .jc-load-steps { margin-top: 12px; }
170
+ .jc-load-steps summary { cursor: pointer; font-size: 13px; }
169
171
 
170
172
  .jc-chart { height: 180px; width: 100%; background: var(--jc-surface); border: 1px solid var(--jc-border); border-radius: 6px; }
171
173
  .jc-chart-empty { display: flex; align-items: center; justify-content: center; color: var(--jc-muted); font-size: 13px; }
@@ -337,6 +337,49 @@ export interface OutboxBacklog {
337
337
  recentDelaySeconds: number | null;
338
338
  fallingBehind: boolean;
339
339
  }
340
+ export interface LoadRun {
341
+ id: string;
342
+ name: string;
343
+ environment: string | null;
344
+ startedAt: string;
345
+ endedAt: string | null;
346
+ startedBy: string;
347
+ }
348
+ export interface LoadRunPoint {
349
+ at: string;
350
+ perSecond: number;
351
+ errorRate: number;
352
+ p95Ms: number | null;
353
+ queue: number | null;
354
+ consumerBusy: number;
355
+ created: number;
356
+ delivered: number;
357
+ backlog: number;
358
+ }
359
+ /** Where a service started to struggle. */
360
+ export interface LoadRunLimit {
361
+ reason: string;
362
+ at: string;
363
+ perSecond: number;
364
+ value: number;
365
+ baseline: number | null;
366
+ }
367
+ export interface LoadRunService {
368
+ serviceId: string | null;
369
+ name: string;
370
+ points: LoadRunPoint[];
371
+ peakPerSecond: number;
372
+ p95AtPeakMs: number | null;
373
+ errorRateAtPeak: number;
374
+ baselineP95Ms: number | null;
375
+ limit: LoadRunLimit | null;
376
+ }
377
+ export interface LoadRunResults {
378
+ run: LoadRun;
379
+ stepSeconds: number;
380
+ services: LoadRunService[];
381
+ truncated: boolean;
382
+ }
340
383
  export interface DependencyStats {
341
384
  dependencyId: string;
342
385
  count: number;
@@ -397,6 +440,8 @@ export declare class ApiService {
397
440
  series(query: Query): Observable<Series>;
398
441
  serviceStats(query: Query): Observable<ServiceStats>;
399
442
  capacity(query: Query): Observable<Capacity>;
443
+ loadRuns(query: Query): Observable<Page<LoadRun>>;
444
+ loadRunResults(id: string): Observable<LoadRunResults>;
400
445
  dependencyStats(query: Query): Observable<DependencyStatsPage>;
401
446
  dependency(id: string, query: Query): Observable<DependencyEvidence>;
402
447
  search(q: string): Observable<TelemetryPage<SearchHit>>;
@@ -1 +0,0 @@
1
- {"version":3,"file":"juice-js-observability-service.page-rGAvUK_k.mjs","names":["i1","i3","i2"],"sources":["../../../../projects/juice-js/observability/src/lib/shared/capacity.ts","../../../../projects/juice-js/observability/src/lib/pages/service.page.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, input, linkedSignal, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { Capacity } from '../core/models';\nimport { apiResource } from './resource';\nimport { problemText } from './telemetry';\n\nexport interface CapacityInputs {\n users: number;\n /** Seconds between one user's actions. */\n thinkSeconds: number;\n /** Requests or messages one user action causes in this service. */\n perAction: number;\n /** The CPU use to stay under, from 0 to 1. */\n targetCpu: number;\n coresPerInstance: number;\n}\n\nexport interface CapacityPlan {\n perSecond: number;\n instances: number;\n cores: number;\n /** Per instance; null without memory data. */\n memoryBytes: number | null;\n /** The load each instance would carry. */\n perInstance: number;\n /** That load over the highest measured: above 1 is beyond what was measured. */\n beyondMeasured: number | null;\n /** What set the number of instances: CPU, or a consumer that was already busy all the time. */\n limitedBy: 'cpu' | 'messages';\n}\n\n/**\n * The calculator: `null` when the inputs or the measurements can't give an answer, such as a target that idle CPU alone\n * already exceeds. Each instance can carry (target × cores − idle) ÷ CPU per unit, unless a consumer was seen busy all the\n * time: more CPU doesn't help one that handles a message at a time, so an instance carries only the rate it reached.\n */\nexport function planCapacity(m: Capacity, i: CapacityInputs): CapacityPlan | null {\n if (!m.cpuSecondsPerUnit || m.cpuSecondsPerUnit <= 0) return null;\n if (!(i.users > 0 && i.perAction > 0 && i.targetCpu > 0 && i.targetCpu <= 1 && i.coresPerInstance > 0 && i.thinkSeconds >= 0)) return null;\n const response = (m.responseMs ?? 0) / 1000;\n if (i.thinkSeconds + response <= 0) return null;\n\n const byCpu = (i.targetCpu * i.coresPerInstance - (m.idleCores ?? 0)) / m.cpuSecondsPerUnit;\n if (byCpu <= 0) return null;\n const limitedBy = consumerBusy(m) && m.peakMessagesPerSecond < byCpu ? 'messages' : 'cpu';\n const carried = limitedBy === 'messages' ? m.peakMessagesPerSecond : byCpu;\n const perSecond = i.users * i.perAction / (i.thinkSeconds + response);\n const instances = Math.max(1, Math.ceil(perSecond / carried));\n const perInstance = perSecond / instances;\n const memoryBytes = m.memoryModel === 'fitted'\n ? m.idleMemoryBytes! + m.memoryBytesPerInFlight! * perInstance * response\n : m.peakMemoryBytes;\n // A consumer held back by message handling never used its cores: count what it used, at the target, not the machine's.\n const coresEach = limitedBy === 'messages' && m.peakCores !== null\n ? Math.min(i.coresPerInstance, Math.ceil(m.peakCores / i.targetCpu))\n : i.coresPerInstance;\n return {\n perSecond, instances, cores: instances * coresEach, memoryBytes, perInstance,\n beyondMeasured: m.peakPerSecond > 0 ? perInstance / m.peakPerSecond : null,\n limitedBy,\n };\n}\n\n/** A consumer counts as busy all the time from this much message processing per second of time. */\nconst BUSY_CONSUMER = 0.9;\nfunction consumerBusy(m: Capacity): boolean { return m.peakMessagesInFlight >= BUSY_CONSUMER && m.peakMessagesPerSecond > 0; }\n\n/** A thread pool queue of a few items comes and goes on a healthy host; from this many, work was really waiting. */\nconst NOTABLE_QUEUE = 10;\n\n/** Response time counts as rising with load from this ratio between the busiest and quietest samples. */\nconst RISING_RESPONSE = 1.5;\n\nfunction bytes(value: number | null): string {\n if (value === null) return '—';\n return value >= 1024 ** 3 ? `${(value / 1024 ** 3).toFixed(1)} GB` : `${Math.round(value / 1024 ** 2)} MB`;\n}\nfunction rate(value: number): string { return value >= 10 ? value.toFixed(0) : value.toFixed(2); }\n\n/**\n * A service's Capacity tab: what it was measured to need in the selected window, and a calculator from users to instances,\n * cores and memory. The estimate says where it comes from and where it stops holding: scaling isn't linear, and the\n * service's own databases and brokers are not in it.\n */\n@Component({\n selector: 'jc-service-capacity',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [FormsModule, MatFormFieldModule, MatInputModule, MatProgressBarModule],\n template: `\n @if (capacity.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (capacity.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (mixedEnvironments()) {\n <p class=\"jc-notice\">This service runs in more than one environment, and their hardware may differ. Choose an environment for an estimate that means something.</p>\n }\n @if (capacity.value(); as m) {\n @if (m.samples === 0) {\n <p class=\"jc-muted\">No telemetry from this service in this window.</p>\n } @else {\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-estimate\">\n <h3 id=\"jc-capacity-estimate\">Estimate</h3>\n <div class=\"jc-capacity-inputs\">\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Concurrent users</mat-label>\n <input matInput type=\"number\" min=\"1\" name=\"users\" [ngModel]=\"users()\" (ngModelChange)=\"users.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Seconds between actions</mat-label>\n <input matInput type=\"number\" min=\"0\" name=\"thinkSeconds\" [ngModel]=\"thinkSeconds()\" (ngModelChange)=\"thinkSeconds.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Requests per action</mat-label>\n <input matInput type=\"number\" min=\"0\" name=\"perAction\" [ngModel]=\"perAction()\" (ngModelChange)=\"perAction.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Target CPU use (%)</mat-label>\n <input matInput type=\"number\" min=\"1\" max=\"100\" name=\"targetCpu\" [ngModel]=\"targetCpu()\" (ngModelChange)=\"targetCpu.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Cores per instance</mat-label>\n <input matInput type=\"number\" min=\"1\" name=\"coresPerInstance\" [ngModel]=\"coresPerInstance()\" (ngModelChange)=\"coresPerInstance.set($event)\" /></mat-form-field>\n </div>\n @if (plan(); as p) {\n <div class=\"jc-capacity-result\" data-testid=\"capacity-plan\">\n <div><strong>{{ p.instances }}</strong><span class=\"jc-muted\">instance{{ p.instances === 1 ? '' : 's' }}</span></div>\n <div><strong>{{ p.cores }}</strong><span class=\"jc-muted\">cores in total</span></div>\n <div><strong>{{ memory(p.memoryBytes) }}</strong><span class=\"jc-muted\">memory per instance{{ m.memoryModel === 'peak' ? ' (highest seen)' : '' }}</span></div>\n <div><strong>{{ perSecond(p.perSecond) }}</strong><span class=\"jc-muted\">requests and messages/s</span></div>\n </div>\n @if (p.beyondMeasured !== null && p.beyondMeasured > 1) {\n <p class=\"jc-notice\" data-testid=\"capacity-beyond\">Each instance would carry {{ perSecond(p.perInstance) }}/s, {{ p.beyondMeasured.toFixed(1) }}× the highest measured ({{ perSecond(m.peakPerSecond) }}/s). This is an extrapolation, and only a load test can confirm it.</p>\n }\n } @else if (m.cpuModel === 'none') {\n <p class=\"jc-notice\">No CPU data, so no estimate. CPU needs two readings at least a minute apart: choose a longer window, or wait. If it stays empty, add <code>AddMeter(\"System.Runtime\")</code> to the host's metrics (.NET 9+).</p>\n } @else {\n <p class=\"jc-notice\">No estimate for these values. Check them: idle CPU alone may already exceed the target.</p>\n }\n <ul class=\"jc-capacity-notes\">\n @if (responseRising(); as r) {\n <li class=\"jc-bad\">Response time already rises with load: {{ r.low }} ms when quiet, {{ r.high }} ms when busy. Something may be near its limit, so the real capacity may be lower.</li>\n }\n @if (busyConsumer()) {\n <li class=\"jc-bad\" data-testid=\"capacity-busy-consumer\">Message processing was busy nearly all the time, at about {{ perSecond(m.peakMessagesPerSecond) }} messages/s per instance. If it handles one message at a time, that is its limit whatever CPU says, and its queue may be growing. The estimate uses that rate. See which step takes the time in a trace.</li>\n }\n @if (m.outbox?.fallingBehind) {\n <li class=\"jc-bad\" data-testid=\"capacity-outbox-behind\">Outbox delivery is falling behind: {{ count(m.outbox!.undelivered) }} messages created in this window weren't delivered. CPU and requests can look healthy while this grows, and the estimate doesn't include it. If another service delivers this outbox, this doesn't apply.</li>\n }\n @if (m.peakQueue !== null && m.peakQueue >= notableQueue) {\n <li class=\"jc-bad\">The thread pool queue reached {{ m.peakQueue }}: work was waiting for a thread.</li>\n }\n <li>Assumes cost grows in a straight line with load. A database, lock or connection limit can be reached long before CPU runs out.</li>\n <li>Covers this service only, not its databases, brokers or the services it calls.</li>\n </ul>\n </section>\n\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-measured\">\n <h3 id=\"jc-capacity-measured\">Measured in this window</h3>\n <table class=\"jc-table jc-capacity-measured\">\n <tbody>\n <tr><th>CPU per request or message</th>\n <td>\n @if (m.cpuSecondsPerUnit !== null) { {{ (m.cpuSecondsPerUnit * 1000).toFixed(1) }} ms }\n @else { <span class=\"jc-muted\">No CPU data yet</span> }\n @if (m.cpuModel === 'average') {\n <div class=\"jc-muted jc-small\">Measured in the busiest samples and includes idle CPU, so it can read high. Load didn't vary enough, or CPU didn't follow it in a straight line, to separate the two.</div>\n }\n </td></tr>\n <tr><th>Idle CPU</th><td>@if (m.idleCores !== null) { {{ m.idleCores.toFixed(2) }} cores } @else { <span class=\"jc-muted\">Not separated</span> }</td></tr>\n <tr><th>Idle memory</th><td>@if (m.idleMemoryBytes !== null) { {{ memory(m.idleMemoryBytes) }} } @else { <span class=\"jc-muted\">Not separated</span> }</td></tr>\n <tr><th>Memory growth under load</th><td>@if (m.memoryBytesPerInFlight !== null) { {{ memory(m.memoryBytesPerInFlight) }} per request in flight } @else { <span class=\"jc-muted\">Not separated</span> }</td></tr>\n <tr><th>Highest load, one instance</th><td>{{ perSecond(m.peakPerSecond) }} requests and messages/s</td></tr>\n <tr><th>Highest use, one instance</th><td>\n {{ m.peakCores !== null ? m.peakCores.toFixed(2) + ' cores' : 'CPU unknown' }} · {{ m.peakMemoryBytes !== null ? memory(m.peakMemoryBytes) : 'memory unknown' }}</td></tr>\n <tr><th>Average response</th><td>{{ m.responseMs !== null ? m.responseMs.toFixed(0) + ' ms' : '—' }}</td></tr>\n <tr><th>Measured on</th><td>\n {{ m.instances }} instance{{ m.instances === 1 ? '' : 's' }}@if (m.coresPerInstance) {, {{ m.coresPerInstance }} cores each}@if (m.hosts.length) {, on {{ m.hosts.join(', ') }}}.\n <div class=\"jc-muted jc-small\">Figures from other hardware carry over only roughly.</div></td></tr>\n <tr><th>Based on</th><td>\n {{ m.samples }} samples of {{ m.stepSeconds }} s.\n <div class=\"jc-muted jc-small\">{{ sourceNote() }}</div>\n @if (m.truncated) { <div class=\"jc-bad jc-small\">Too many samples in this window, so only the newest were used. Choose a shorter one.</div> }</td></tr>\n </tbody>\n </table>\n </section>\n\n @if (m.outbox; as o) {\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-outbox\" data-testid=\"capacity-outbox\">\n <h3 id=\"jc-capacity-outbox\">Outbox delivery</h3>\n <table class=\"jc-table jc-capacity-measured\">\n <tbody>\n <tr><th>Messages created</th><td>{{ perSecond(o.createdPerSecond) }}/s</td></tr>\n <tr><th>Messages delivered</th><td>{{ perSecond(o.deliveredPerSecond) }}/s</td></tr>\n <tr><th>Created but not delivered</th><td [class.jc-bad]=\"o.fallingBehind\">{{ count(o.undelivered) }} in this window\n <div class=\"jc-muted jc-small\">Includes messages of rolled-back transactions, which are never delivered.</div></td></tr>\n <tr><th>Time to delivery, p95</th><td [class.jc-bad]=\"o.fallingBehind\">@if (o.recentDelaySeconds !== null) { {{ o.recentDelaySeconds.toFixed(1) }} s in the last 5 minutes } @else { <span class=\"jc-muted\">Nothing delivered in the last 5 minutes</span> }</td></tr>\n </tbody>\n </table>\n </section>\n }\n }\n }`,\n})\nexport class ServiceCapacity {\n private readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n readonly serviceId = input.required<string>();\n /** The environments the service runs in: figures across several mix their hardware. */\n readonly environments = input<string[]>([]);\n\n protected readonly capacity = apiResource({\n params: () => ({ id: this.serviceId(), env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute() }),\n stream: ({ params }) => this.api.capacity({ serviceId: params.id, environment: params.env, ...this.ctx.window() }),\n });\n protected readonly users = signal(1000);\n protected readonly thinkSeconds = signal(10);\n protected readonly perAction = signal(1);\n protected readonly targetCpu = signal(60);\n protected readonly coresPerInstance = linkedSignal(() => this.capacity.value()?.coresPerInstance ?? 2);\n\n protected readonly error = computed(() => problemText(this.capacity.error()));\n protected readonly mixedEnvironments = computed(() => !this.ctx.environment() && new Set(this.environments()).size > 1);\n protected readonly plan = computed(() => {\n const m = this.capacity.value();\n return m ? planCapacity(m, {\n users: this.users(), thinkSeconds: this.thinkSeconds(), perAction: this.perAction(), targetCpu: this.targetCpu() / 100, coresPerInstance: this.coresPerInstance(),\n }) : null;\n });\n protected readonly busyConsumer = computed(() => { const m = this.capacity.value(); return !!m && consumerBusy(m); });\n protected readonly responseRising = computed(() => {\n const m = this.capacity.value();\n return m?.responseLowLoadMs && m.responseHighLoadMs && m.responseHighLoadMs >= RISING_RESPONSE * m.responseLowLoadMs\n ? { low: Math.round(m.responseLowLoadMs), high: Math.round(m.responseHighLoadMs) } : null;\n });\n protected readonly sourceNote = computed(() => this.capacity.value()?.requestSource === 'metrics'\n ? 'Requests are counted from the http.server.request.duration metric, messages from spans.'\n : 'Requests and messages are counted from spans, which undercount if traces are sampled. That makes the cost per request read high. Exporting http.server.request.duration fixes the request count.');\n\n protected readonly notableQueue = NOTABLE_QUEUE;\n protected readonly memory = bytes;\n protected readonly perSecond = rate;\n protected readonly count = (value: number) => Math.round(value).toLocaleString('en-US');\n}\n","import { LinkPipe, ObservabilityLinks } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { apiResource } from '../shared/resource';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { MatTabsModule } from '@angular/material/tabs';\nimport { MatButtonModule } from '@angular/material/button';\nimport { Router, RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { AgoPipe, DurationPipe, PercentPipe, ReasonPipe } from '../shared/format';\nimport { ConfidenceBadge, ContextBar, HealthBadge } from '../shared/ui';\nimport { LogTable, MetricExplorer, ServiceStatsCharts, TraceTable, problemText } from '../shared/telemetry';\nimport { ServiceCapacity } from '../shared/capacity';\n\nconst TABS = ['overview', 'operations', 'dependencies', 'traces', 'logs', 'metrics', 'capacity'] as const;\n\n/**\n * One service across its environments (Console requirements §8): health with reasons, instances, operations,\n * dependencies, traces, logs and metrics, without leaving the service. The tab and the selected operation are in the\n * URL, so the investigation can be shared and the back button returns to it.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, MatTabsModule, MatProgressBarModule, MatButtonModule, ContextBar, HealthBadge, ConfidenceBadge,\n TraceTable, LogTable, MetricExplorer, ServiceStatsCharts, ServiceCapacity, AgoPipe, DurationPipe, PercentPipe, ReasonPipe],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['services'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Services</a>\n <span class=\"jc-crumb-sep\">›</span>\n <h1>{{ service.value()?.key ?? '…' }}</h1>\n @for (l of service.value()?.links ?? []; track l.url) { <a mat-button [href]=\"l.url\" target=\"_blank\" rel=\"noopener\">{{ l.title }} ↗</a> }\n </jc-context-bar>\n @if (service.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (service.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n\n @if (service.value(); as s) {\n <div class=\"jc-health-strip\">\n @for (h of health(); track h.environment) {\n <div class=\"jc-card jc-health-card\" [class.jc-selected]=\"h.environment === ctx.environment()\">\n <div class=\"jc-row\"><strong>{{ h.environment ?? 'no environment' }}</strong><jc-health [state]=\"h.state\" [stale]=\"h.stale\" /></div>\n <ul class=\"jc-reasons\">\n @for (r of h.reasons; track r) {\n <li>\n @if (dependencyOf(r); as dep) {\n {{ r | reason }} — <a [routerLink]=\"['services', dep.id] | jcLink\" [queryParams]=\"ctx.keep()\">open</a>\n } @else { {{ r | reason }} }\n </li>\n }\n </ul>\n <div class=\"jc-muted jc-small\">since {{ h.since | ago }} · evaluated {{ h.evaluatedAt | ago }}</div>\n @if (h.environment && h.environment !== ctx.environment()) {\n <button mat-button (click)=\"ctx.set({ env: h.environment })\">Investigate {{ h.environment }}</button>\n }\n </div>\n } @empty { <p class=\"jc-muted\">No health evaluated yet: no data.</p> }\n </div>\n\n <mat-tab-group [selectedIndex]=\"tabIndex()\" (selectedIndexChange)=\"openTab($event)\" animationDuration=\"0ms\" mat-stretch-tabs=\"false\">\n <mat-tab label=\"Overview\">\n @if (tabIndex() === 0) {\n <jc-service-stats [query]=\"{ serviceId: id() }\" />\n <h3>Instances</h3>\n <table class=\"jc-table\">\n <thead><tr><th>Instance</th><th>Environment</th><th>Health</th><th>Version</th><th>State</th><th>Last seen</th><th></th></tr></thead>\n <tbody>\n @for (i of instances(); track i.id) {\n <tr>\n <td>{{ i.instanceKey }}@if (i.keySource !== 'Observed') { <span class=\"jc-muted jc-small\" title=\"No service.instance.id: derived from host and process\"> (derived)</span> }</td>\n <td>{{ i.environment }}</td>\n <td><jc-health [state]=\"i.health\" /></td>\n <td>{{ i.serviceVersion ?? '—' }}</td>\n <td>{{ i.state }}</td>\n <td>{{ i.lastSeen | ago }}</td>\n <td class=\"jc-nowrap\">\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', instance: i.id })\">traces</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'logs', instance: i.id })\">logs</a>\n @if (i.hostResourceId) { · <a [routerLink]=\"['resources', i.hostResourceId] | jcLink\" [queryParams]=\"ctx.keep()\">host</a> }\n </td>\n </tr>\n } @empty { <tr><td colspan=\"7\" class=\"jc-muted\">No instances.</td></tr> }\n </tbody>\n </table>\n }\n </mat-tab>\n\n <mat-tab label=\"Operations\">\n @if (tabIndex() === 1) {\n @if (operations.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n <table class=\"jc-table\">\n <thead><tr><th>Operation</th><th>Kind</th><th>Requests</th><th title=\"Average over the selected time range. The chart shows each moment, so a burst reads higher there.\">Avg/s</th><th>Errors</th><th>p50</th><th>p95</th><th>p99</th><th>Last seen</th><th></th></tr></thead>\n <tbody>\n @for (o of operations.value()?.items ?? []; track o.name + o.kind) {\n <tr [class.jc-selected]=\"o.name === operation()\">\n <td><a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'operations', op: o.name })\">{{ o.name }}</a></td>\n <td>{{ o.kind }}</td>\n <td>{{ o.count }}</td>\n <td>{{ o.requestsPerSecond.toFixed(2) }}</td>\n <td [class.jc-bad]=\"o.errorRate >= 0.05\">{{ o.errorRate | percent2 }}</td>\n <td>{{ o.p50Ms | duration }}</td><td>{{ o.p95Ms | duration }}</td><td>{{ o.p99Ms | duration }}</td>\n <td>{{ o.lastSeen | ago }}</td>\n <td class=\"jc-nowrap\">\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', op: o.name, status: 'error' })\">error traces</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', op: o.name, slow: o.p95Ms })\">slow traces</a>\n </td>\n </tr>\n } @empty { <tr><td colspan=\"10\" class=\"jc-muted\">No operations in this window.</td></tr> }\n </tbody>\n </table>\n @if (operation(); as op) {\n <h3>{{ op }}</h3>\n <jc-service-stats [query]=\"{ serviceId: id(), operation: op }\" />\n }\n }\n </mat-tab>\n\n <mat-tab label=\"Dependencies\">\n @if (tabIndex() === 2) {\n <h3>Calls and uses</h3>\n <table class=\"jc-table\">\n <thead><tr><th>Target</th><th>Relation</th><th>Evidence</th><th>Spans seen</th><th>Last seen</th><th></th></tr></thead>\n <tbody>\n @for (d of downstream(); track d.edge.id) {\n <tr>\n <td><a [routerLink]=\"d.link\" [queryParams]=\"ctx.keep()\">{{ d.name }}</a>&nbsp;<span class=\"jc-muted jc-small\">{{ d.type }}</span></td>\n <td>{{ d.edge.relation }}</td>\n <td><jc-confidence [confidence]=\"d.edge.confidence\" /></td>\n <td>{{ d.edge.spanCount }}</td>\n <td>{{ d.edge.lastSeen | ago }}</td>\n <td><a [routerLink]=\"['topology'] | jcLink\" [queryParams]=\"ctx.keep({ edge: d.edge.id })\">evidence</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', dependency: d.edge.id })\">traces</a></td>\n </tr>\n } @empty { <tr><td colspan=\"6\" class=\"jc-muted\">No downstream dependencies seen.</td></tr> }\n </tbody>\n </table>\n <h3>Called by</h3>\n <table class=\"jc-table\">\n <tbody>\n @for (d of upstream(); track d.edge.id) {\n <tr><td><a [routerLink]=\"d.link\" [queryParams]=\"ctx.keep()\">{{ d.name }}</a></td><td>{{ d.edge.relation }}</td>\n <td><jc-confidence [confidence]=\"d.edge.confidence\" /></td><td>{{ d.edge.lastSeen | ago }}</td>\n <td><a [routerLink]=\"['topology'] | jcLink\" [queryParams]=\"ctx.keep({ edge: d.edge.id })\">evidence</a></td></tr>\n } @empty { <tr><td class=\"jc-muted\">No callers seen.</td></tr> }\n </tbody>\n </table>\n }\n </mat-tab>\n\n <mat-tab label=\"Traces\">\n @if (tabIndex() === 3) {\n <div class=\"jc-filters\">\n @for (f of traceFilters(); track f.label) { <span class=\"jc-chip\">{{ f.label }} <a [routerLink]=\"[]\" [queryParams]=\"f.clear\" queryParamsHandling=\"merge\" [attr.aria-label]=\"'Remove ' + f.label\">×</a></span> }\n </div>\n <jc-trace-table [query]=\"traceQuery()\" />\n }\n </mat-tab>\n\n <mat-tab label=\"Logs\">\n @if (tabIndex() === 4) { <jc-log-table [query]=\"{ serviceId: id(), instanceId: instanceFilter() }\" /> }\n </mat-tab>\n\n <mat-tab label=\"Metrics\">\n @if (tabIndex() === 5) { <jc-metric-explorer [scope]=\"{ serviceId: id(), instanceId: instanceFilter() }\" /> }\n </mat-tab>\n\n <mat-tab label=\"Capacity\">\n @if (tabIndex() === 6) { <jc-service-capacity [serviceId]=\"id()\" [environments]=\"environments()\" /> }\n </mat-tab>\n </mat-tab-group>\n }`,\n})\nexport class ServicePage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n private readonly router = inject(Router);\n private readonly links = inject(ObservabilityLinks);\n\n readonly id = input.required<string>();\n readonly tab = input<string>();\n readonly op = input<string>();\n readonly status = input<string>();\n readonly slow = input<string>();\n readonly instance = input<string>();\n readonly dependency = input<string>();\n\n protected readonly tabIndex = computed(() => Math.max(0, TABS.indexOf((this.tab() ?? 'overview') as typeof TABS[number])));\n protected readonly operation = computed(() => this.op() ?? null);\n protected readonly instanceFilter = computed(() => this.instance() ?? null);\n\n protected readonly service = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.resource(params) });\n protected readonly topology = apiResource({ params: () => this.ctx.environment(), stream: ({ params }) => this.api.topology(params || null) });\n protected readonly operations = apiResource({\n params: () => ({ id: this.id(), env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute(), on: this.tabIndex() === 1 }),\n stream: ({ params }) => this.api.operations({ serviceId: params.id, environment: params.env, ...this.ctx.window(), limit: 200 }),\n });\n\n protected readonly error = computed(() => problemText(this.service.error()));\n protected readonly health = computed(() => (this.service.value()?.health ?? []).filter(h => h.subjectType === 'service'));\n protected readonly instances = computed(() => {\n const env = this.ctx.environment();\n return (this.service.value()?.instances ?? []).filter(i => !env || i.environment === env);\n });\n\n protected readonly environments = computed(() => (this.service.value()?.instances ?? []).map(i => i.environment));\n\n private readonly nodes = computed(() => new Map((this.topology.value()?.nodes ?? []).map(n => [n.id, n])));\n private edgesWhere(match: 'source' | 'target') {\n const other = match === 'source' ? 'target' : 'source';\n return (this.topology.value()?.edges ?? []).filter(e => e[match] === this.id()).map(edge => {\n const node = this.nodes().get(edge[other]);\n return {\n edge, name: node?.displayName ?? edge[other], type: node?.type ?? '',\n link: this.links.to(node?.type === 'service' ? 'services' : 'resources', edge[other]),\n };\n });\n }\n protected readonly downstream = computed(() => this.edgesWhere('source'));\n protected readonly upstream = computed(() => this.edgesWhere('target'));\n\n /** `dependency_failed:shop/payments` links to that service when it is in the topology. */\n dependencyOf(reason: string) {\n const name = reason.match(/^(?:critical_)?dependency_(?:failed|unknown):(.+)$/)?.[1];\n if (!name) return null;\n const node = [...this.nodes().values()].find(n => n.type === 'service' && (n.displayName === name || name.endsWith(`/${n.displayName}`)));\n return node ? { id: node.id } : null;\n }\n\n protected readonly traceQuery = computed(() => ({\n serviceId: this.id(), operation: this.op(), status: this.status(), minDurationMs: this.slow(),\n instanceId: this.instance(), dependencyId: this.dependency(),\n }));\n /** The trace filters in effect, each removable on its own. */\n protected readonly traceFilters = computed(() => {\n const filters: { label: string; clear: Record<string, null> }[] = [];\n if (this.op()) filters.push({ label: `operation: ${this.op()}`, clear: { op: null } });\n if (this.status()) filters.push({ label: `status: ${this.status()}`, clear: { status: null } });\n if (this.slow()) filters.push({ label: `slower than ${Math.round(Number(this.slow()))} ms`, clear: { slow: null } });\n if (this.instance()) filters.push({ label: 'one instance', clear: { instance: null } });\n if (this.dependency()) filters.push({ label: 'through one dependency', clear: { dependency: null } });\n return filters;\n });\n\n openTab(index: number) {\n void this.router.navigate([], { queryParams: { tab: TABS[index] }, queryParamsHandling: 'merge' });\n }\n}\n"],"mappings":"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"}