@juice-js/observability 1.1.0-alpha.18 → 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 +4 -2
- package/fesm2022/juice-js-observability-load-runs.page-C2rk2bz9.mjs +349 -0
- package/fesm2022/juice-js-observability-load-runs.page-C2rk2bz9.mjs.map +1 -0
- package/fesm2022/juice-js-observability-service.page-BrcID0Bs.mjs +1255 -0
- package/fesm2022/juice-js-observability-service.page-BrcID0Bs.mjs.map +1 -0
- package/fesm2022/{juice-js-observability-topology.page-aMWEXyMB.mjs → juice-js-observability-topology.page-CUayiRVT.mjs} +55 -3
- package/fesm2022/juice-js-observability-topology.page-CUayiRVT.mjs.map +1 -0
- package/fesm2022/{juice-js-observability-trace.page-DUCWnZL5.mjs → juice-js-observability-trace.page-ByxGFLpE.mjs} +9 -7
- package/fesm2022/juice-js-observability-trace.page-ByxGFLpE.mjs.map +1 -0
- package/fesm2022/juice-js-observability.mjs +23 -4
- package/fesm2022/juice-js-observability.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_observability.scss +22 -3
- package/types/juice-js-observability.d.ts +94 -0
- package/fesm2022/juice-js-observability-service.page-Dt8bN2Ry.mjs +0 -726
- package/fesm2022/juice-js-observability-service.page-Dt8bN2Ry.mjs.map +0 -1
- package/fesm2022/juice-js-observability-topology.page-aMWEXyMB.mjs.map +0 -1
- package/fesm2022/juice-js-observability-trace.page-DUCWnZL5.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -59,8 +59,10 @@ like juice-layout's dark-mode toggle) repaints the charts and the graph.
|
|
|
59
59
|
- `auditLink` (optional): a link from a trace to its audit records, shown on the trace page as "Audit records ↗".
|
|
60
60
|
With `@juice-js/audit` mounted at `/audit`: `auditLink: id => ['/audit/traces', id]`. A string is an external URL.
|
|
61
61
|
Without it, the trace page shows no link.
|
|
62
|
-
- The trace page's resource usage
|
|
63
|
-
host's `WithMetrics`.
|
|
62
|
+
- The trace page's resource usage and a service's Capacity tab read .NET's `System.Runtime` meter (.NET 9+): add
|
|
63
|
+
`AddMeter("System.Runtime")` to the host's `WithMetrics`. If traces are sampled, also add `AddAspNetCoreInstrumentation()`
|
|
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.
|
|
64
66
|
- The router needs `bindToComponentInputs: true`. The pages take their route and query parameters as inputs.
|
|
65
67
|
- The host's bearer-token interceptor (`@juice-js/auth`) must cover `apiBase`. The token carries the `platform_tenant`
|
|
66
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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"}
|