@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
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
import { ApiService, ContextService, LinkPipe, ObservabilityLinks } from "./juice-js-observability.mjs";
|
|
2
|
+
import { S as apiResource, _ as PercentPipe, a as TraceTable, d as HealthBadge, g as DurationPipe, h as AgoPipe, i as ServiceStatsCharts, l as ConfidenceBadge, n as LogTable, o as problemText, r as MetricExplorer, u as ContextBar, v as ReasonPipe } from "./juice-js-observability-telemetry-BftWSOaQ.mjs";
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
import { ChangeDetectionStrategy, Component, computed, inject, input, linkedSignal, signal } from "@angular/core";
|
|
5
|
+
import { Router, RouterLink } from "@angular/router";
|
|
6
|
+
import * as i1$1 from "@angular/forms";
|
|
7
|
+
import { FormsModule } from "@angular/forms";
|
|
8
|
+
import * as i3 from "@angular/material/button";
|
|
9
|
+
import { MatButtonModule } from "@angular/material/button";
|
|
10
|
+
import { MatFormFieldModule } from "@angular/material/form-field";
|
|
11
|
+
import * as i3$1 from "@angular/material/input";
|
|
12
|
+
import { MatInputModule } from "@angular/material/input";
|
|
13
|
+
import * as i4 from "@angular/material/progress-bar";
|
|
14
|
+
import { MatProgressBarModule } from "@angular/material/progress-bar";
|
|
15
|
+
import * as i2 from "@angular/material/select";
|
|
16
|
+
import * as i1 from "@angular/material/tabs";
|
|
17
|
+
import { MatTabsModule } from "@angular/material/tabs";
|
|
18
|
+
function planCapacity(m, i) {
|
|
19
|
+
if (!m.cpuSecondsPerUnit || m.cpuSecondsPerUnit <= 0) return null;
|
|
20
|
+
if (!(i.users > 0 && i.perAction > 0 && i.targetCpu > 0 && i.targetCpu <= 1 && i.coresPerInstance > 0 && i.thinkSeconds >= 0)) return null;
|
|
21
|
+
const response = (m.responseMs ?? 0) / 1e3;
|
|
22
|
+
if (i.thinkSeconds + response <= 0) return null;
|
|
23
|
+
const byCpu = (i.targetCpu * i.coresPerInstance - (m.idleCores ?? 0)) / m.cpuSecondsPerUnit;
|
|
24
|
+
if (byCpu <= 0) return null;
|
|
25
|
+
const limitedBy = consumerBusy(m) && m.peakMessagesPerSecond < byCpu ? "messages" : "cpu";
|
|
26
|
+
const carried = limitedBy === "messages" ? m.peakMessagesPerSecond : byCpu;
|
|
27
|
+
const perSecond = i.users * i.perAction / (i.thinkSeconds + response);
|
|
28
|
+
const instances = Math.max(1, Math.ceil(perSecond / carried));
|
|
29
|
+
const perInstance = perSecond / instances;
|
|
30
|
+
const memoryBytes = m.memoryModel === "fitted" ? m.idleMemoryBytes + m.memoryBytesPerInFlight * perInstance * response : m.peakMemoryBytes;
|
|
31
|
+
return {
|
|
32
|
+
perSecond,
|
|
33
|
+
instances,
|
|
34
|
+
cores: instances * (limitedBy === "messages" && m.peakCores !== null ? Math.min(i.coresPerInstance, Math.ceil(m.peakCores / i.targetCpu)) : i.coresPerInstance),
|
|
35
|
+
memoryBytes,
|
|
36
|
+
perInstance,
|
|
37
|
+
beyondMeasured: m.peakPerSecond > 0 ? perInstance / m.peakPerSecond : null,
|
|
38
|
+
limitedBy
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const BUSY_CONSUMER = .9;
|
|
42
|
+
function consumerBusy(m) {
|
|
43
|
+
return m.peakMessagesInFlight >= BUSY_CONSUMER && m.peakMessagesPerSecond > 0;
|
|
44
|
+
}
|
|
45
|
+
const NOTABLE_QUEUE = 10;
|
|
46
|
+
const RISING_RESPONSE = 1.5;
|
|
47
|
+
function bytes(value) {
|
|
48
|
+
if (value === null) return "—";
|
|
49
|
+
return value >= 1024 ** 3 ? `${(value / 1024 ** 3).toFixed(1)} GB` : `${Math.round(value / 1024 ** 2)} MB`;
|
|
50
|
+
}
|
|
51
|
+
function rate(value) {
|
|
52
|
+
return value >= 10 ? value.toFixed(0) : value.toFixed(2);
|
|
53
|
+
}
|
|
54
|
+
var ServiceCapacity = class ServiceCapacity {
|
|
55
|
+
ctx = inject(ContextService);
|
|
56
|
+
api = inject(ApiService);
|
|
57
|
+
serviceId = input.required(...ngDevMode ? [{ debugName: "serviceId" }] : /* istanbul ignore next */ []);
|
|
58
|
+
environments = input([], ...ngDevMode ? [{ debugName: "environments" }] : /* istanbul ignore next */ []);
|
|
59
|
+
capacity = apiResource({
|
|
60
|
+
params: () => ({
|
|
61
|
+
id: this.serviceId(),
|
|
62
|
+
env: this.ctx.environment(),
|
|
63
|
+
range: this.ctx.range(),
|
|
64
|
+
abs: this.ctx.absolute()
|
|
65
|
+
}),
|
|
66
|
+
stream: ({ params }) => this.api.capacity({
|
|
67
|
+
serviceId: params.id,
|
|
68
|
+
environment: params.env,
|
|
69
|
+
...this.ctx.window()
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
users = signal(1e3, ...ngDevMode ? [{ debugName: "users" }] : /* istanbul ignore next */ []);
|
|
73
|
+
thinkSeconds = signal(10, ...ngDevMode ? [{ debugName: "thinkSeconds" }] : /* istanbul ignore next */ []);
|
|
74
|
+
perAction = signal(1, ...ngDevMode ? [{ debugName: "perAction" }] : /* istanbul ignore next */ []);
|
|
75
|
+
targetCpu = signal(60, ...ngDevMode ? [{ debugName: "targetCpu" }] : /* istanbul ignore next */ []);
|
|
76
|
+
coresPerInstance = linkedSignal(() => this.capacity.value()?.coresPerInstance ?? 2, ...ngDevMode ? [{ debugName: "coresPerInstance" }] : /* istanbul ignore next */ []);
|
|
77
|
+
error = computed(() => problemText(this.capacity.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
78
|
+
mixedEnvironments = computed(() => !this.ctx.environment() && new Set(this.environments()).size > 1, ...ngDevMode ? [{ debugName: "mixedEnvironments" }] : /* istanbul ignore next */ []);
|
|
79
|
+
plan = computed(() => {
|
|
80
|
+
const m = this.capacity.value();
|
|
81
|
+
return m ? planCapacity(m, {
|
|
82
|
+
users: this.users(),
|
|
83
|
+
thinkSeconds: this.thinkSeconds(),
|
|
84
|
+
perAction: this.perAction(),
|
|
85
|
+
targetCpu: this.targetCpu() / 100,
|
|
86
|
+
coresPerInstance: this.coresPerInstance()
|
|
87
|
+
}) : null;
|
|
88
|
+
}, ...ngDevMode ? [{ debugName: "plan" }] : /* istanbul ignore next */ []);
|
|
89
|
+
busyConsumer = computed(() => {
|
|
90
|
+
const m = this.capacity.value();
|
|
91
|
+
return !!m && consumerBusy(m);
|
|
92
|
+
}, ...ngDevMode ? [{ debugName: "busyConsumer" }] : /* istanbul ignore next */ []);
|
|
93
|
+
responseRising = computed(() => {
|
|
94
|
+
const m = this.capacity.value();
|
|
95
|
+
return m?.responseLowLoadMs && m.responseHighLoadMs && m.responseHighLoadMs >= RISING_RESPONSE * m.responseLowLoadMs ? {
|
|
96
|
+
low: Math.round(m.responseLowLoadMs),
|
|
97
|
+
high: Math.round(m.responseHighLoadMs)
|
|
98
|
+
} : null;
|
|
99
|
+
}, ...ngDevMode ? [{ debugName: "responseRising" }] : /* istanbul ignore next */ []);
|
|
100
|
+
sourceNote = computed(() => this.capacity.value()?.requestSource === "metrics" ? "Requests are counted from the http.server.request.duration metric, messages from spans." : "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.", ...ngDevMode ? [{ debugName: "sourceNote" }] : /* istanbul ignore next */ []);
|
|
101
|
+
notableQueue = NOTABLE_QUEUE;
|
|
102
|
+
memory = bytes;
|
|
103
|
+
perSecond = rate;
|
|
104
|
+
count = (value) => Math.round(value).toLocaleString("en-US");
|
|
105
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
106
|
+
minVersion: "12.0.0",
|
|
107
|
+
version: "22.2.4",
|
|
108
|
+
ngImport: i0,
|
|
109
|
+
type: ServiceCapacity,
|
|
110
|
+
deps: [],
|
|
111
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
112
|
+
});
|
|
113
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
114
|
+
minVersion: "17.0.0",
|
|
115
|
+
version: "22.2.4",
|
|
116
|
+
type: ServiceCapacity,
|
|
117
|
+
isStandalone: true,
|
|
118
|
+
selector: "jc-service-capacity",
|
|
119
|
+
inputs: {
|
|
120
|
+
serviceId: {
|
|
121
|
+
classPropertyName: "serviceId",
|
|
122
|
+
publicName: "serviceId",
|
|
123
|
+
isSignal: true,
|
|
124
|
+
isRequired: true,
|
|
125
|
+
transformFunction: null
|
|
126
|
+
},
|
|
127
|
+
environments: {
|
|
128
|
+
classPropertyName: "environments",
|
|
129
|
+
publicName: "environments",
|
|
130
|
+
isSignal: true,
|
|
131
|
+
isRequired: false,
|
|
132
|
+
transformFunction: null
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
ngImport: i0,
|
|
136
|
+
template: `
|
|
137
|
+
@if (capacity.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
138
|
+
@if (capacity.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
139
|
+
@if (mixedEnvironments()) {
|
|
140
|
+
<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>
|
|
141
|
+
}
|
|
142
|
+
@if (capacity.value(); as m) {
|
|
143
|
+
@if (m.samples === 0) {
|
|
144
|
+
<p class="jc-muted">No telemetry from this service in this window.</p>
|
|
145
|
+
} @else {
|
|
146
|
+
<section class="jc-card" aria-labelledby="jc-capacity-estimate">
|
|
147
|
+
<h3 id="jc-capacity-estimate">Estimate</h3>
|
|
148
|
+
<div class="jc-capacity-inputs">
|
|
149
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Concurrent users</mat-label>
|
|
150
|
+
<input matInput type="number" min="1" name="users" [ngModel]="users()" (ngModelChange)="users.set($event)" /></mat-form-field>
|
|
151
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Seconds between actions</mat-label>
|
|
152
|
+
<input matInput type="number" min="0" name="thinkSeconds" [ngModel]="thinkSeconds()" (ngModelChange)="thinkSeconds.set($event)" /></mat-form-field>
|
|
153
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Requests per action</mat-label>
|
|
154
|
+
<input matInput type="number" min="0" name="perAction" [ngModel]="perAction()" (ngModelChange)="perAction.set($event)" /></mat-form-field>
|
|
155
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Target CPU use (%)</mat-label>
|
|
156
|
+
<input matInput type="number" min="1" max="100" name="targetCpu" [ngModel]="targetCpu()" (ngModelChange)="targetCpu.set($event)" /></mat-form-field>
|
|
157
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Cores per instance</mat-label>
|
|
158
|
+
<input matInput type="number" min="1" name="coresPerInstance" [ngModel]="coresPerInstance()" (ngModelChange)="coresPerInstance.set($event)" /></mat-form-field>
|
|
159
|
+
</div>
|
|
160
|
+
@if (plan(); as p) {
|
|
161
|
+
<div class="jc-capacity-result" data-testid="capacity-plan">
|
|
162
|
+
<div><strong>{{ p.instances }}</strong><span class="jc-muted">instance{{ p.instances === 1 ? '' : 's' }}</span></div>
|
|
163
|
+
<div><strong>{{ p.cores }}</strong><span class="jc-muted">cores in total</span></div>
|
|
164
|
+
<div><strong>{{ memory(p.memoryBytes) }}</strong><span class="jc-muted">memory per instance{{ m.memoryModel === 'peak' ? ' (highest seen)' : '' }}</span></div>
|
|
165
|
+
<div><strong>{{ perSecond(p.perSecond) }}</strong><span class="jc-muted">requests and messages/s</span></div>
|
|
166
|
+
</div>
|
|
167
|
+
@if (p.beyondMeasured !== null && p.beyondMeasured > 1) {
|
|
168
|
+
<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>
|
|
169
|
+
}
|
|
170
|
+
} @else if (m.cpuModel === 'none') {
|
|
171
|
+
<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>
|
|
172
|
+
} @else {
|
|
173
|
+
<p class="jc-notice">No estimate for these values. Check them: idle CPU alone may already exceed the target.</p>
|
|
174
|
+
}
|
|
175
|
+
<ul class="jc-capacity-notes">
|
|
176
|
+
@if (responseRising(); as r) {
|
|
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
|
+
}
|
|
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, but a saturated consumer's rate can fall, so it may not hold. See which step takes the time in a trace.</li>
|
|
181
|
+
}
|
|
182
|
+
@if (m.outbox?.fallingBehind) {
|
|
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>
|
|
184
|
+
}
|
|
185
|
+
@if (m.peakQueue !== null && m.peakQueue >= notableQueue) {
|
|
186
|
+
<li class="jc-bad">The thread pool queue reached {{ m.peakQueue }}: work was waiting for a thread.</li>
|
|
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
|
+
}
|
|
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>
|
|
192
|
+
<li>Covers this service only, not its databases, brokers or the services it calls.</li>
|
|
193
|
+
</ul>
|
|
194
|
+
</section>
|
|
195
|
+
|
|
196
|
+
<section class="jc-card" aria-labelledby="jc-capacity-measured">
|
|
197
|
+
<h3 id="jc-capacity-measured">Measured in this window</h3>
|
|
198
|
+
<table class="jc-table jc-capacity-measured">
|
|
199
|
+
<tbody>
|
|
200
|
+
<tr><th>CPU per request or message</th>
|
|
201
|
+
<td>
|
|
202
|
+
@if (m.cpuSecondsPerUnit !== null) { {{ (m.cpuSecondsPerUnit * 1000).toFixed(1) }} ms }
|
|
203
|
+
@else { <span class="jc-muted">No CPU data yet</span> }
|
|
204
|
+
@if (m.cpuModel === 'average') {
|
|
205
|
+
<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>
|
|
206
|
+
}
|
|
207
|
+
</td></tr>
|
|
208
|
+
<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>
|
|
209
|
+
<tr><th>Idle memory</th><td>@if (m.idleMemoryBytes !== null) { {{ memory(m.idleMemoryBytes) }} } @else { <span class="jc-muted">Not separated</span> }</td></tr>
|
|
210
|
+
<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>
|
|
211
|
+
<tr><th>Highest load, one instance</th><td>{{ perSecond(m.peakPerSecond) }} requests and messages/s</td></tr>
|
|
212
|
+
<tr><th>Highest use, one instance</th><td>
|
|
213
|
+
{{ m.peakCores !== null ? m.peakCores.toFixed(2) + ' cores' : 'CPU unknown' }} · {{ m.peakMemoryBytes !== null ? memory(m.peakMemoryBytes) : 'memory unknown' }}</td></tr>
|
|
214
|
+
<tr><th>Average response</th><td>{{ m.responseMs !== null ? m.responseMs.toFixed(0) + ' ms' : '—' }}</td></tr>
|
|
215
|
+
<tr><th>Measured on</th><td>
|
|
216
|
+
{{ m.instances }} instance{{ m.instances === 1 ? '' : 's' }}@if (m.coresPerInstance) {, {{ m.coresPerInstance }} cores each}@if (m.hosts.length) {, on {{ m.hosts.join(', ') }}}.
|
|
217
|
+
<div class="jc-muted jc-small">Figures from other hardware carry over only roughly.</div></td></tr>
|
|
218
|
+
<tr><th>Based on</th><td>
|
|
219
|
+
{{ m.samples }} samples of {{ m.stepSeconds }} s.
|
|
220
|
+
<div class="jc-muted jc-small">{{ sourceNote() }}</div>
|
|
221
|
+
@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>
|
|
222
|
+
</tbody>
|
|
223
|
+
</table>
|
|
224
|
+
</section>
|
|
225
|
+
|
|
226
|
+
@if (m.outbox; as o) {
|
|
227
|
+
<section class="jc-card" aria-labelledby="jc-capacity-outbox" data-testid="capacity-outbox">
|
|
228
|
+
<h3 id="jc-capacity-outbox">Outbox delivery</h3>
|
|
229
|
+
<table class="jc-table jc-capacity-measured">
|
|
230
|
+
<tbody>
|
|
231
|
+
<tr><th>Messages created</th><td>{{ perSecond(o.createdPerSecond) }}/s</td></tr>
|
|
232
|
+
<tr><th>Messages delivered</th><td>{{ perSecond(o.deliveredPerSecond) }}/s</td></tr>
|
|
233
|
+
<tr><th>Created but not delivered</th><td [class.jc-bad]="o.fallingBehind">{{ count(o.undelivered) }} in this window
|
|
234
|
+
<div class="jc-muted jc-small">Includes messages of rolled-back transactions, which are never delivered.</div></td></tr>
|
|
235
|
+
<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>
|
|
236
|
+
</tbody>
|
|
237
|
+
</table>
|
|
238
|
+
</section>
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}`,
|
|
242
|
+
isInline: true,
|
|
243
|
+
dependencies: [
|
|
244
|
+
{
|
|
245
|
+
kind: "ngmodule",
|
|
246
|
+
type: FormsModule
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
kind: "directive",
|
|
250
|
+
type: i1$1.DefaultValueAccessor,
|
|
251
|
+
selector: "input:not([type=checkbox]):not([ngNoCva])[formControlName],textarea:not([ngNoCva])[formControlName],input:not([type=checkbox]):not([ngNoCva])[formControl],textarea:not([ngNoCva])[formControl],input:not([type=checkbox]):not([ngNoCva])[ngModel],textarea:not([ngNoCva])[ngModel],[ngDefaultControl]"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
kind: "directive",
|
|
255
|
+
type: i1$1.NumberValueAccessor,
|
|
256
|
+
selector: "input[type=number]:not([ngNoCva])[formControlName],input[type=number]:not([ngNoCva])[formControl],input[type=number]:not([ngNoCva])[ngModel]"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
kind: "directive",
|
|
260
|
+
type: i1$1.NgControlStatus,
|
|
261
|
+
selector: "[formControlName],[ngModel],[formControl]"
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
kind: "directive",
|
|
265
|
+
type: i1$1.MinValidator,
|
|
266
|
+
selector: "input[type=number][min][formControlName],input[type=number][min][formControl],input[type=number][min][ngModel]",
|
|
267
|
+
inputs: ["min"]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
kind: "directive",
|
|
271
|
+
type: i1$1.MaxValidator,
|
|
272
|
+
selector: "input[type=number][max][formControlName],input[type=number][max][formControl],input[type=number][max][ngModel]",
|
|
273
|
+
inputs: ["max"]
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
kind: "directive",
|
|
277
|
+
type: i1$1.NgModel,
|
|
278
|
+
selector: "[ngModel]:not([formControlName]):not([formControl])",
|
|
279
|
+
inputs: [
|
|
280
|
+
"name",
|
|
281
|
+
"disabled",
|
|
282
|
+
"ngModel",
|
|
283
|
+
"ngModelOptions"
|
|
284
|
+
],
|
|
285
|
+
outputs: ["ngModelChange"],
|
|
286
|
+
exportAs: ["ngModel"]
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
kind: "ngmodule",
|
|
290
|
+
type: MatFormFieldModule
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
kind: "component",
|
|
294
|
+
type: i2.MatFormField,
|
|
295
|
+
selector: "mat-form-field",
|
|
296
|
+
inputs: [
|
|
297
|
+
"hideRequiredMarker",
|
|
298
|
+
"color",
|
|
299
|
+
"floatLabel",
|
|
300
|
+
"appearance",
|
|
301
|
+
"subscriptSizing",
|
|
302
|
+
"hintLabel"
|
|
303
|
+
],
|
|
304
|
+
exportAs: ["matFormField"]
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
kind: "directive",
|
|
308
|
+
type: i2.MatLabel,
|
|
309
|
+
selector: "mat-label"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
kind: "ngmodule",
|
|
313
|
+
type: MatInputModule
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
kind: "directive",
|
|
317
|
+
type: i3$1.MatInput,
|
|
318
|
+
selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",
|
|
319
|
+
inputs: [
|
|
320
|
+
"disabled",
|
|
321
|
+
"id",
|
|
322
|
+
"placeholder",
|
|
323
|
+
"name",
|
|
324
|
+
"required",
|
|
325
|
+
"type",
|
|
326
|
+
"errorStateMatcher",
|
|
327
|
+
"aria-describedby",
|
|
328
|
+
"value",
|
|
329
|
+
"readonly",
|
|
330
|
+
"disabledInteractive"
|
|
331
|
+
],
|
|
332
|
+
exportAs: ["matInput"]
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
kind: "ngmodule",
|
|
336
|
+
type: MatProgressBarModule
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
kind: "component",
|
|
340
|
+
type: i4.MatProgressBar,
|
|
341
|
+
selector: "mat-progress-bar",
|
|
342
|
+
inputs: [
|
|
343
|
+
"color",
|
|
344
|
+
"value",
|
|
345
|
+
"bufferValue",
|
|
346
|
+
"mode"
|
|
347
|
+
],
|
|
348
|
+
outputs: ["animationEnd"],
|
|
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"
|
|
374
|
+
}
|
|
375
|
+
],
|
|
376
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
380
|
+
minVersion: "12.0.0",
|
|
381
|
+
version: "22.2.4",
|
|
382
|
+
ngImport: i0,
|
|
383
|
+
type: ServiceCapacity,
|
|
384
|
+
decorators: [{
|
|
385
|
+
type: Component,
|
|
386
|
+
args: [{
|
|
387
|
+
selector: "jc-service-capacity",
|
|
388
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
389
|
+
imports: [
|
|
390
|
+
FormsModule,
|
|
391
|
+
MatFormFieldModule,
|
|
392
|
+
MatInputModule,
|
|
393
|
+
MatProgressBarModule,
|
|
394
|
+
RouterLink,
|
|
395
|
+
LinkPipe
|
|
396
|
+
],
|
|
397
|
+
template: `
|
|
398
|
+
@if (capacity.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
399
|
+
@if (capacity.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
400
|
+
@if (mixedEnvironments()) {
|
|
401
|
+
<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>
|
|
402
|
+
}
|
|
403
|
+
@if (capacity.value(); as m) {
|
|
404
|
+
@if (m.samples === 0) {
|
|
405
|
+
<p class="jc-muted">No telemetry from this service in this window.</p>
|
|
406
|
+
} @else {
|
|
407
|
+
<section class="jc-card" aria-labelledby="jc-capacity-estimate">
|
|
408
|
+
<h3 id="jc-capacity-estimate">Estimate</h3>
|
|
409
|
+
<div class="jc-capacity-inputs">
|
|
410
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Concurrent users</mat-label>
|
|
411
|
+
<input matInput type="number" min="1" name="users" [ngModel]="users()" (ngModelChange)="users.set($event)" /></mat-form-field>
|
|
412
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Seconds between actions</mat-label>
|
|
413
|
+
<input matInput type="number" min="0" name="thinkSeconds" [ngModel]="thinkSeconds()" (ngModelChange)="thinkSeconds.set($event)" /></mat-form-field>
|
|
414
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Requests per action</mat-label>
|
|
415
|
+
<input matInput type="number" min="0" name="perAction" [ngModel]="perAction()" (ngModelChange)="perAction.set($event)" /></mat-form-field>
|
|
416
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Target CPU use (%)</mat-label>
|
|
417
|
+
<input matInput type="number" min="1" max="100" name="targetCpu" [ngModel]="targetCpu()" (ngModelChange)="targetCpu.set($event)" /></mat-form-field>
|
|
418
|
+
<mat-form-field subscriptSizing="dynamic"><mat-label>Cores per instance</mat-label>
|
|
419
|
+
<input matInput type="number" min="1" name="coresPerInstance" [ngModel]="coresPerInstance()" (ngModelChange)="coresPerInstance.set($event)" /></mat-form-field>
|
|
420
|
+
</div>
|
|
421
|
+
@if (plan(); as p) {
|
|
422
|
+
<div class="jc-capacity-result" data-testid="capacity-plan">
|
|
423
|
+
<div><strong>{{ p.instances }}</strong><span class="jc-muted">instance{{ p.instances === 1 ? '' : 's' }}</span></div>
|
|
424
|
+
<div><strong>{{ p.cores }}</strong><span class="jc-muted">cores in total</span></div>
|
|
425
|
+
<div><strong>{{ memory(p.memoryBytes) }}</strong><span class="jc-muted">memory per instance{{ m.memoryModel === 'peak' ? ' (highest seen)' : '' }}</span></div>
|
|
426
|
+
<div><strong>{{ perSecond(p.perSecond) }}</strong><span class="jc-muted">requests and messages/s</span></div>
|
|
427
|
+
</div>
|
|
428
|
+
@if (p.beyondMeasured !== null && p.beyondMeasured > 1) {
|
|
429
|
+
<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>
|
|
430
|
+
}
|
|
431
|
+
} @else if (m.cpuModel === 'none') {
|
|
432
|
+
<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>
|
|
433
|
+
} @else {
|
|
434
|
+
<p class="jc-notice">No estimate for these values. Check them: idle CPU alone may already exceed the target.</p>
|
|
435
|
+
}
|
|
436
|
+
<ul class="jc-capacity-notes">
|
|
437
|
+
@if (responseRising(); as r) {
|
|
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>
|
|
439
|
+
}
|
|
440
|
+
@if (busyConsumer()) {
|
|
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>
|
|
442
|
+
}
|
|
443
|
+
@if (m.outbox?.fallingBehind) {
|
|
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>
|
|
445
|
+
}
|
|
446
|
+
@if (m.peakQueue !== null && m.peakQueue >= notableQueue) {
|
|
447
|
+
<li class="jc-bad">The thread pool queue reached {{ m.peakQueue }}: work was waiting for a thread.</li>
|
|
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
|
+
}
|
|
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>
|
|
453
|
+
<li>Covers this service only, not its databases, brokers or the services it calls.</li>
|
|
454
|
+
</ul>
|
|
455
|
+
</section>
|
|
456
|
+
|
|
457
|
+
<section class="jc-card" aria-labelledby="jc-capacity-measured">
|
|
458
|
+
<h3 id="jc-capacity-measured">Measured in this window</h3>
|
|
459
|
+
<table class="jc-table jc-capacity-measured">
|
|
460
|
+
<tbody>
|
|
461
|
+
<tr><th>CPU per request or message</th>
|
|
462
|
+
<td>
|
|
463
|
+
@if (m.cpuSecondsPerUnit !== null) { {{ (m.cpuSecondsPerUnit * 1000).toFixed(1) }} ms }
|
|
464
|
+
@else { <span class="jc-muted">No CPU data yet</span> }
|
|
465
|
+
@if (m.cpuModel === 'average') {
|
|
466
|
+
<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>
|
|
467
|
+
}
|
|
468
|
+
</td></tr>
|
|
469
|
+
<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>
|
|
470
|
+
<tr><th>Idle memory</th><td>@if (m.idleMemoryBytes !== null) { {{ memory(m.idleMemoryBytes) }} } @else { <span class="jc-muted">Not separated</span> }</td></tr>
|
|
471
|
+
<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>
|
|
472
|
+
<tr><th>Highest load, one instance</th><td>{{ perSecond(m.peakPerSecond) }} requests and messages/s</td></tr>
|
|
473
|
+
<tr><th>Highest use, one instance</th><td>
|
|
474
|
+
{{ m.peakCores !== null ? m.peakCores.toFixed(2) + ' cores' : 'CPU unknown' }} · {{ m.peakMemoryBytes !== null ? memory(m.peakMemoryBytes) : 'memory unknown' }}</td></tr>
|
|
475
|
+
<tr><th>Average response</th><td>{{ m.responseMs !== null ? m.responseMs.toFixed(0) + ' ms' : '—' }}</td></tr>
|
|
476
|
+
<tr><th>Measured on</th><td>
|
|
477
|
+
{{ m.instances }} instance{{ m.instances === 1 ? '' : 's' }}@if (m.coresPerInstance) {, {{ m.coresPerInstance }} cores each}@if (m.hosts.length) {, on {{ m.hosts.join(', ') }}}.
|
|
478
|
+
<div class="jc-muted jc-small">Figures from other hardware carry over only roughly.</div></td></tr>
|
|
479
|
+
<tr><th>Based on</th><td>
|
|
480
|
+
{{ m.samples }} samples of {{ m.stepSeconds }} s.
|
|
481
|
+
<div class="jc-muted jc-small">{{ sourceNote() }}</div>
|
|
482
|
+
@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>
|
|
483
|
+
</tbody>
|
|
484
|
+
</table>
|
|
485
|
+
</section>
|
|
486
|
+
|
|
487
|
+
@if (m.outbox; as o) {
|
|
488
|
+
<section class="jc-card" aria-labelledby="jc-capacity-outbox" data-testid="capacity-outbox">
|
|
489
|
+
<h3 id="jc-capacity-outbox">Outbox delivery</h3>
|
|
490
|
+
<table class="jc-table jc-capacity-measured">
|
|
491
|
+
<tbody>
|
|
492
|
+
<tr><th>Messages created</th><td>{{ perSecond(o.createdPerSecond) }}/s</td></tr>
|
|
493
|
+
<tr><th>Messages delivered</th><td>{{ perSecond(o.deliveredPerSecond) }}/s</td></tr>
|
|
494
|
+
<tr><th>Created but not delivered</th><td [class.jc-bad]="o.fallingBehind">{{ count(o.undelivered) }} in this window
|
|
495
|
+
<div class="jc-muted jc-small">Includes messages of rolled-back transactions, which are never delivered.</div></td></tr>
|
|
496
|
+
<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>
|
|
497
|
+
</tbody>
|
|
498
|
+
</table>
|
|
499
|
+
</section>
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}`
|
|
503
|
+
}]
|
|
504
|
+
}],
|
|
505
|
+
propDecorators: {
|
|
506
|
+
serviceId: [{
|
|
507
|
+
type: i0.Input,
|
|
508
|
+
args: [{
|
|
509
|
+
isSignal: true,
|
|
510
|
+
alias: "serviceId",
|
|
511
|
+
required: true
|
|
512
|
+
}]
|
|
513
|
+
}],
|
|
514
|
+
environments: [{
|
|
515
|
+
type: i0.Input,
|
|
516
|
+
args: [{
|
|
517
|
+
isSignal: true,
|
|
518
|
+
alias: "environments",
|
|
519
|
+
required: false
|
|
520
|
+
}]
|
|
521
|
+
}]
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
const TABS = [
|
|
525
|
+
"overview",
|
|
526
|
+
"operations",
|
|
527
|
+
"dependencies",
|
|
528
|
+
"traces",
|
|
529
|
+
"logs",
|
|
530
|
+
"metrics",
|
|
531
|
+
"capacity"
|
|
532
|
+
];
|
|
533
|
+
var ServicePage = class ServicePage {
|
|
534
|
+
ctx = inject(ContextService);
|
|
535
|
+
api = inject(ApiService);
|
|
536
|
+
router = inject(Router);
|
|
537
|
+
links = inject(ObservabilityLinks);
|
|
538
|
+
id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
|
|
539
|
+
tab = input(...ngDevMode ? [void 0, { debugName: "tab" }] : /* istanbul ignore next */ []);
|
|
540
|
+
op = input(...ngDevMode ? [void 0, { debugName: "op" }] : /* istanbul ignore next */ []);
|
|
541
|
+
status = input(...ngDevMode ? [void 0, { debugName: "status" }] : /* istanbul ignore next */ []);
|
|
542
|
+
slow = input(...ngDevMode ? [void 0, { debugName: "slow" }] : /* istanbul ignore next */ []);
|
|
543
|
+
instance = input(...ngDevMode ? [void 0, { debugName: "instance" }] : /* istanbul ignore next */ []);
|
|
544
|
+
dependency = input(...ngDevMode ? [void 0, { debugName: "dependency" }] : /* istanbul ignore next */ []);
|
|
545
|
+
tabIndex = computed(() => Math.max(0, TABS.indexOf(this.tab() ?? "overview")), ...ngDevMode ? [{ debugName: "tabIndex" }] : /* istanbul ignore next */ []);
|
|
546
|
+
operation = computed(() => this.op() ?? null, ...ngDevMode ? [{ debugName: "operation" }] : /* istanbul ignore next */ []);
|
|
547
|
+
instanceFilter = computed(() => this.instance() ?? null, ...ngDevMode ? [{ debugName: "instanceFilter" }] : /* istanbul ignore next */ []);
|
|
548
|
+
service = apiResource({
|
|
549
|
+
params: () => this.id(),
|
|
550
|
+
stream: ({ params }) => this.api.resource(params)
|
|
551
|
+
});
|
|
552
|
+
topology = apiResource({
|
|
553
|
+
params: () => this.ctx.environment(),
|
|
554
|
+
stream: ({ params }) => this.api.topology(params || null)
|
|
555
|
+
});
|
|
556
|
+
operations = apiResource({
|
|
557
|
+
params: () => ({
|
|
558
|
+
id: this.id(),
|
|
559
|
+
env: this.ctx.environment(),
|
|
560
|
+
range: this.ctx.range(),
|
|
561
|
+
abs: this.ctx.absolute(),
|
|
562
|
+
on: this.tabIndex() === 1
|
|
563
|
+
}),
|
|
564
|
+
stream: ({ params }) => this.api.operations({
|
|
565
|
+
serviceId: params.id,
|
|
566
|
+
environment: params.env,
|
|
567
|
+
...this.ctx.window(),
|
|
568
|
+
limit: 200
|
|
569
|
+
})
|
|
570
|
+
});
|
|
571
|
+
error = computed(() => problemText(this.service.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
572
|
+
health = computed(() => (this.service.value()?.health ?? []).filter((h) => h.subjectType === "service"), ...ngDevMode ? [{ debugName: "health" }] : /* istanbul ignore next */ []);
|
|
573
|
+
instances = computed(() => {
|
|
574
|
+
const env = this.ctx.environment();
|
|
575
|
+
return (this.service.value()?.instances ?? []).filter((i) => !env || i.environment === env);
|
|
576
|
+
}, ...ngDevMode ? [{ debugName: "instances" }] : /* istanbul ignore next */ []);
|
|
577
|
+
environments = computed(() => (this.service.value()?.instances ?? []).map((i) => i.environment), ...ngDevMode ? [{ debugName: "environments" }] : /* istanbul ignore next */ []);
|
|
578
|
+
nodes = computed(() => new Map((this.topology.value()?.nodes ?? []).map((n) => [n.id, n])), ...ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []);
|
|
579
|
+
edgesWhere(match) {
|
|
580
|
+
const other = match === "source" ? "target" : "source";
|
|
581
|
+
return (this.topology.value()?.edges ?? []).filter((e) => e[match] === this.id()).map((edge) => {
|
|
582
|
+
const node = this.nodes().get(edge[other]);
|
|
583
|
+
return {
|
|
584
|
+
edge,
|
|
585
|
+
name: node?.displayName ?? edge[other],
|
|
586
|
+
type: node?.type ?? "",
|
|
587
|
+
link: this.links.to(node?.type === "service" ? "services" : "resources", edge[other])
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
downstream = computed(() => this.edgesWhere("source"), ...ngDevMode ? [{ debugName: "downstream" }] : /* istanbul ignore next */ []);
|
|
592
|
+
upstream = computed(() => this.edgesWhere("target"), ...ngDevMode ? [{ debugName: "upstream" }] : /* istanbul ignore next */ []);
|
|
593
|
+
dependencyOf(reason) {
|
|
594
|
+
const name = reason.match(/^(?:critical_)?dependency_(?:failed|unknown):(.+)$/)?.[1];
|
|
595
|
+
if (!name) return null;
|
|
596
|
+
const node = [...this.nodes().values()].find((n) => n.type === "service" && (n.displayName === name || name.endsWith(`/${n.displayName}`)));
|
|
597
|
+
return node ? { id: node.id } : null;
|
|
598
|
+
}
|
|
599
|
+
traceQuery = computed(() => ({
|
|
600
|
+
serviceId: this.id(),
|
|
601
|
+
operation: this.op(),
|
|
602
|
+
status: this.status(),
|
|
603
|
+
minDurationMs: this.slow(),
|
|
604
|
+
instanceId: this.instance(),
|
|
605
|
+
dependencyId: this.dependency()
|
|
606
|
+
}), ...ngDevMode ? [{ debugName: "traceQuery" }] : /* istanbul ignore next */ []);
|
|
607
|
+
traceFilters = computed(() => {
|
|
608
|
+
const filters = [];
|
|
609
|
+
if (this.op()) filters.push({
|
|
610
|
+
label: `operation: ${this.op()}`,
|
|
611
|
+
clear: { op: null }
|
|
612
|
+
});
|
|
613
|
+
if (this.status()) filters.push({
|
|
614
|
+
label: `status: ${this.status()}`,
|
|
615
|
+
clear: { status: null }
|
|
616
|
+
});
|
|
617
|
+
if (this.slow()) filters.push({
|
|
618
|
+
label: `slower than ${Math.round(Number(this.slow()))} ms`,
|
|
619
|
+
clear: { slow: null }
|
|
620
|
+
});
|
|
621
|
+
if (this.instance()) filters.push({
|
|
622
|
+
label: "one instance",
|
|
623
|
+
clear: { instance: null }
|
|
624
|
+
});
|
|
625
|
+
if (this.dependency()) filters.push({
|
|
626
|
+
label: "through one dependency",
|
|
627
|
+
clear: { dependency: null }
|
|
628
|
+
});
|
|
629
|
+
return filters;
|
|
630
|
+
}, ...ngDevMode ? [{ debugName: "traceFilters" }] : /* istanbul ignore next */ []);
|
|
631
|
+
openTab(index) {
|
|
632
|
+
this.router.navigate([], {
|
|
633
|
+
queryParams: { tab: TABS[index] },
|
|
634
|
+
queryParamsHandling: "merge"
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
638
|
+
minVersion: "12.0.0",
|
|
639
|
+
version: "22.2.4",
|
|
640
|
+
ngImport: i0,
|
|
641
|
+
type: ServicePage,
|
|
642
|
+
deps: [],
|
|
643
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
644
|
+
});
|
|
645
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
646
|
+
minVersion: "17.0.0",
|
|
647
|
+
version: "22.2.4",
|
|
648
|
+
type: ServicePage,
|
|
649
|
+
isStandalone: true,
|
|
650
|
+
selector: "ng-component",
|
|
651
|
+
inputs: {
|
|
652
|
+
id: {
|
|
653
|
+
classPropertyName: "id",
|
|
654
|
+
publicName: "id",
|
|
655
|
+
isSignal: true,
|
|
656
|
+
isRequired: true,
|
|
657
|
+
transformFunction: null
|
|
658
|
+
},
|
|
659
|
+
tab: {
|
|
660
|
+
classPropertyName: "tab",
|
|
661
|
+
publicName: "tab",
|
|
662
|
+
isSignal: true,
|
|
663
|
+
isRequired: false,
|
|
664
|
+
transformFunction: null
|
|
665
|
+
},
|
|
666
|
+
op: {
|
|
667
|
+
classPropertyName: "op",
|
|
668
|
+
publicName: "op",
|
|
669
|
+
isSignal: true,
|
|
670
|
+
isRequired: false,
|
|
671
|
+
transformFunction: null
|
|
672
|
+
},
|
|
673
|
+
status: {
|
|
674
|
+
classPropertyName: "status",
|
|
675
|
+
publicName: "status",
|
|
676
|
+
isSignal: true,
|
|
677
|
+
isRequired: false,
|
|
678
|
+
transformFunction: null
|
|
679
|
+
},
|
|
680
|
+
slow: {
|
|
681
|
+
classPropertyName: "slow",
|
|
682
|
+
publicName: "slow",
|
|
683
|
+
isSignal: true,
|
|
684
|
+
isRequired: false,
|
|
685
|
+
transformFunction: null
|
|
686
|
+
},
|
|
687
|
+
instance: {
|
|
688
|
+
classPropertyName: "instance",
|
|
689
|
+
publicName: "instance",
|
|
690
|
+
isSignal: true,
|
|
691
|
+
isRequired: false,
|
|
692
|
+
transformFunction: null
|
|
693
|
+
},
|
|
694
|
+
dependency: {
|
|
695
|
+
classPropertyName: "dependency",
|
|
696
|
+
publicName: "dependency",
|
|
697
|
+
isSignal: true,
|
|
698
|
+
isRequired: false,
|
|
699
|
+
transformFunction: null
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
ngImport: i0,
|
|
703
|
+
template: `
|
|
704
|
+
<jc-context-bar>
|
|
705
|
+
<a [routerLink]="['services'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Services</a>
|
|
706
|
+
<span class="jc-crumb-sep">›</span>
|
|
707
|
+
<h1>{{ service.value()?.key ?? '…' }}</h1>
|
|
708
|
+
@for (l of service.value()?.links ?? []; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
|
|
709
|
+
</jc-context-bar>
|
|
710
|
+
@if (service.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
711
|
+
@if (service.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
712
|
+
|
|
713
|
+
@if (service.value(); as s) {
|
|
714
|
+
<div class="jc-health-strip">
|
|
715
|
+
@for (h of health(); track h.environment) {
|
|
716
|
+
<div class="jc-card jc-health-card" [class.jc-selected]="h.environment === ctx.environment()">
|
|
717
|
+
<div class="jc-row"><strong>{{ h.environment ?? 'no environment' }}</strong><jc-health [state]="h.state" [stale]="h.stale" /></div>
|
|
718
|
+
<ul class="jc-reasons">
|
|
719
|
+
@for (r of h.reasons; track r) {
|
|
720
|
+
<li>
|
|
721
|
+
@if (dependencyOf(r); as dep) {
|
|
722
|
+
{{ r | reason }} — <a [routerLink]="['services', dep.id] | jcLink" [queryParams]="ctx.keep()">open</a>
|
|
723
|
+
} @else { {{ r | reason }} }
|
|
724
|
+
</li>
|
|
725
|
+
}
|
|
726
|
+
</ul>
|
|
727
|
+
<div class="jc-muted jc-small">since {{ h.since | ago }} · evaluated {{ h.evaluatedAt | ago }}</div>
|
|
728
|
+
@if (h.environment && h.environment !== ctx.environment()) {
|
|
729
|
+
<button mat-button (click)="ctx.set({ env: h.environment })">Investigate {{ h.environment }}</button>
|
|
730
|
+
}
|
|
731
|
+
</div>
|
|
732
|
+
} @empty { <p class="jc-muted">No health evaluated yet: no data.</p> }
|
|
733
|
+
</div>
|
|
734
|
+
|
|
735
|
+
<mat-tab-group [selectedIndex]="tabIndex()" (selectedIndexChange)="openTab($event)" animationDuration="0ms" mat-stretch-tabs="false">
|
|
736
|
+
<mat-tab label="Overview">
|
|
737
|
+
@if (tabIndex() === 0) {
|
|
738
|
+
<jc-service-stats [query]="{ serviceId: id() }" />
|
|
739
|
+
<h3>Instances</h3>
|
|
740
|
+
<table class="jc-table">
|
|
741
|
+
<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>
|
|
742
|
+
<tbody>
|
|
743
|
+
@for (i of instances(); track i.id) {
|
|
744
|
+
<tr>
|
|
745
|
+
<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>
|
|
746
|
+
<td>{{ i.environment }}</td>
|
|
747
|
+
<td><jc-health [state]="i.health" /></td>
|
|
748
|
+
<td>{{ i.serviceVersion ?? '—' }}</td>
|
|
749
|
+
<td>{{ i.state }}</td>
|
|
750
|
+
<td>{{ i.lastSeen | ago }}</td>
|
|
751
|
+
<td class="jc-nowrap">
|
|
752
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', instance: i.id })">traces</a> ·
|
|
753
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'logs', instance: i.id })">logs</a>
|
|
754
|
+
@if (i.hostResourceId) { · <a [routerLink]="['resources', i.hostResourceId] | jcLink" [queryParams]="ctx.keep()">host</a> }
|
|
755
|
+
</td>
|
|
756
|
+
</tr>
|
|
757
|
+
} @empty { <tr><td colspan="7" class="jc-muted">No instances.</td></tr> }
|
|
758
|
+
</tbody>
|
|
759
|
+
</table>
|
|
760
|
+
}
|
|
761
|
+
</mat-tab>
|
|
762
|
+
|
|
763
|
+
<mat-tab label="Operations">
|
|
764
|
+
@if (tabIndex() === 1) {
|
|
765
|
+
@if (operations.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
766
|
+
<table class="jc-table">
|
|
767
|
+
<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>
|
|
768
|
+
<tbody>
|
|
769
|
+
@for (o of operations.value()?.items ?? []; track o.name + o.kind) {
|
|
770
|
+
<tr [class.jc-selected]="o.name === operation()">
|
|
771
|
+
<td><a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'operations', op: o.name })">{{ o.name }}</a></td>
|
|
772
|
+
<td>{{ o.kind }}</td>
|
|
773
|
+
<td>{{ o.count }}</td>
|
|
774
|
+
<td>{{ o.requestsPerSecond.toFixed(2) }}</td>
|
|
775
|
+
<td [class.jc-bad]="o.errorRate >= 0.05">{{ o.errorRate | percent2 }}</td>
|
|
776
|
+
<td>{{ o.p50Ms | duration }}</td><td>{{ o.p95Ms | duration }}</td><td>{{ o.p99Ms | duration }}</td>
|
|
777
|
+
<td>{{ o.lastSeen | ago }}</td>
|
|
778
|
+
<td class="jc-nowrap">
|
|
779
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, status: 'error' })">error traces</a> ·
|
|
780
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, slow: o.p95Ms })">slow traces</a>
|
|
781
|
+
</td>
|
|
782
|
+
</tr>
|
|
783
|
+
} @empty { <tr><td colspan="10" class="jc-muted">No operations in this window.</td></tr> }
|
|
784
|
+
</tbody>
|
|
785
|
+
</table>
|
|
786
|
+
@if (operation(); as op) {
|
|
787
|
+
<h3>{{ op }}</h3>
|
|
788
|
+
<jc-service-stats [query]="{ serviceId: id(), operation: op }" />
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
</mat-tab>
|
|
792
|
+
|
|
793
|
+
<mat-tab label="Dependencies">
|
|
794
|
+
@if (tabIndex() === 2) {
|
|
795
|
+
<h3>Calls and uses</h3>
|
|
796
|
+
<table class="jc-table">
|
|
797
|
+
<thead><tr><th>Target</th><th>Relation</th><th>Evidence</th><th>Spans seen</th><th>Last seen</th><th></th></tr></thead>
|
|
798
|
+
<tbody>
|
|
799
|
+
@for (d of downstream(); track d.edge.id) {
|
|
800
|
+
<tr>
|
|
801
|
+
<td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a> <span class="jc-muted jc-small">{{ d.type }}</span></td>
|
|
802
|
+
<td>{{ d.edge.relation }}</td>
|
|
803
|
+
<td><jc-confidence [confidence]="d.edge.confidence" /></td>
|
|
804
|
+
<td>{{ d.edge.spanCount }}</td>
|
|
805
|
+
<td>{{ d.edge.lastSeen | ago }}</td>
|
|
806
|
+
<td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a> ·
|
|
807
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', dependency: d.edge.id })">traces</a></td>
|
|
808
|
+
</tr>
|
|
809
|
+
} @empty { <tr><td colspan="6" class="jc-muted">No downstream dependencies seen.</td></tr> }
|
|
810
|
+
</tbody>
|
|
811
|
+
</table>
|
|
812
|
+
<h3>Called by</h3>
|
|
813
|
+
<table class="jc-table">
|
|
814
|
+
<tbody>
|
|
815
|
+
@for (d of upstream(); track d.edge.id) {
|
|
816
|
+
<tr><td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a></td><td>{{ d.edge.relation }}</td>
|
|
817
|
+
<td><jc-confidence [confidence]="d.edge.confidence" /></td><td>{{ d.edge.lastSeen | ago }}</td>
|
|
818
|
+
<td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a></td></tr>
|
|
819
|
+
} @empty { <tr><td class="jc-muted">No callers seen.</td></tr> }
|
|
820
|
+
</tbody>
|
|
821
|
+
</table>
|
|
822
|
+
}
|
|
823
|
+
</mat-tab>
|
|
824
|
+
|
|
825
|
+
<mat-tab label="Traces">
|
|
826
|
+
@if (tabIndex() === 3) {
|
|
827
|
+
<div class="jc-filters">
|
|
828
|
+
@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> }
|
|
829
|
+
</div>
|
|
830
|
+
<jc-trace-table [query]="traceQuery()" />
|
|
831
|
+
}
|
|
832
|
+
</mat-tab>
|
|
833
|
+
|
|
834
|
+
<mat-tab label="Logs">
|
|
835
|
+
@if (tabIndex() === 4) { <jc-log-table [query]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
|
|
836
|
+
</mat-tab>
|
|
837
|
+
|
|
838
|
+
<mat-tab label="Metrics">
|
|
839
|
+
@if (tabIndex() === 5) { <jc-metric-explorer [scope]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
|
|
840
|
+
</mat-tab>
|
|
841
|
+
|
|
842
|
+
<mat-tab label="Capacity">
|
|
843
|
+
@if (tabIndex() === 6) { <jc-service-capacity [serviceId]="id()" [environments]="environments()" /> }
|
|
844
|
+
</mat-tab>
|
|
845
|
+
</mat-tab-group>
|
|
846
|
+
}`,
|
|
847
|
+
isInline: true,
|
|
848
|
+
dependencies: [
|
|
849
|
+
{
|
|
850
|
+
kind: "directive",
|
|
851
|
+
type: RouterLink,
|
|
852
|
+
selector: "[routerLink]",
|
|
853
|
+
inputs: [
|
|
854
|
+
"target",
|
|
855
|
+
"queryParams",
|
|
856
|
+
"fragment",
|
|
857
|
+
"queryParamsHandling",
|
|
858
|
+
"state",
|
|
859
|
+
"info",
|
|
860
|
+
"relativeTo",
|
|
861
|
+
"preserveFragment",
|
|
862
|
+
"skipLocationChange",
|
|
863
|
+
"replaceUrl",
|
|
864
|
+
"browserUrl",
|
|
865
|
+
"routerLink"
|
|
866
|
+
]
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
kind: "ngmodule",
|
|
870
|
+
type: MatTabsModule
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
kind: "component",
|
|
874
|
+
type: i1.MatTab,
|
|
875
|
+
selector: "mat-tab",
|
|
876
|
+
inputs: [
|
|
877
|
+
"disabled",
|
|
878
|
+
"label",
|
|
879
|
+
"aria-label",
|
|
880
|
+
"aria-labelledby",
|
|
881
|
+
"labelClass",
|
|
882
|
+
"bodyClass",
|
|
883
|
+
"id"
|
|
884
|
+
],
|
|
885
|
+
exportAs: ["matTab"]
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
kind: "component",
|
|
889
|
+
type: i1.MatTabGroup,
|
|
890
|
+
selector: "mat-tab-group",
|
|
891
|
+
inputs: [
|
|
892
|
+
"color",
|
|
893
|
+
"fitInkBarToContent",
|
|
894
|
+
"mat-stretch-tabs",
|
|
895
|
+
"mat-align-tabs",
|
|
896
|
+
"dynamicHeight",
|
|
897
|
+
"selectedIndex",
|
|
898
|
+
"headerPosition",
|
|
899
|
+
"animationDuration",
|
|
900
|
+
"contentTabIndex",
|
|
901
|
+
"disablePagination",
|
|
902
|
+
"disableRipple",
|
|
903
|
+
"preserveContent",
|
|
904
|
+
"backgroundColor",
|
|
905
|
+
"aria-label",
|
|
906
|
+
"aria-labelledby"
|
|
907
|
+
],
|
|
908
|
+
outputs: [
|
|
909
|
+
"selectedIndexChange",
|
|
910
|
+
"focusChange",
|
|
911
|
+
"animationDone",
|
|
912
|
+
"selectedTabChange"
|
|
913
|
+
],
|
|
914
|
+
exportAs: ["matTabGroup"]
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
kind: "ngmodule",
|
|
918
|
+
type: MatProgressBarModule
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
kind: "component",
|
|
922
|
+
type: i4.MatProgressBar,
|
|
923
|
+
selector: "mat-progress-bar",
|
|
924
|
+
inputs: [
|
|
925
|
+
"color",
|
|
926
|
+
"value",
|
|
927
|
+
"bufferValue",
|
|
928
|
+
"mode"
|
|
929
|
+
],
|
|
930
|
+
outputs: ["animationEnd"],
|
|
931
|
+
exportAs: ["matProgressBar"]
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
kind: "ngmodule",
|
|
935
|
+
type: MatButtonModule
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
kind: "component",
|
|
939
|
+
type: i3.MatButton,
|
|
940
|
+
selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ",
|
|
941
|
+
inputs: ["matButton"],
|
|
942
|
+
exportAs: ["matButton", "matAnchor"]
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
kind: "component",
|
|
946
|
+
type: ContextBar,
|
|
947
|
+
selector: "jc-context-bar"
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
kind: "component",
|
|
951
|
+
type: HealthBadge,
|
|
952
|
+
selector: "jc-health",
|
|
953
|
+
inputs: ["state", "stale"]
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
kind: "component",
|
|
957
|
+
type: ConfidenceBadge,
|
|
958
|
+
selector: "jc-confidence",
|
|
959
|
+
inputs: ["confidence"]
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
kind: "component",
|
|
963
|
+
type: TraceTable,
|
|
964
|
+
selector: "jc-trace-table",
|
|
965
|
+
inputs: ["query", "limit"]
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
kind: "component",
|
|
969
|
+
type: LogTable,
|
|
970
|
+
selector: "jc-log-table",
|
|
971
|
+
inputs: ["query", "compact"]
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
kind: "component",
|
|
975
|
+
type: MetricExplorer,
|
|
976
|
+
selector: "jc-metric-explorer",
|
|
977
|
+
inputs: ["scope", "initial"]
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
kind: "component",
|
|
981
|
+
type: ServiceStatsCharts,
|
|
982
|
+
selector: "jc-service-stats",
|
|
983
|
+
inputs: ["query"]
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
kind: "component",
|
|
987
|
+
type: ServiceCapacity,
|
|
988
|
+
selector: "jc-service-capacity",
|
|
989
|
+
inputs: ["serviceId", "environments"]
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
kind: "pipe",
|
|
993
|
+
type: LinkPipe,
|
|
994
|
+
name: "jcLink"
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
kind: "pipe",
|
|
998
|
+
type: AgoPipe,
|
|
999
|
+
name: "ago"
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
kind: "pipe",
|
|
1003
|
+
type: DurationPipe,
|
|
1004
|
+
name: "duration"
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
kind: "pipe",
|
|
1008
|
+
type: PercentPipe,
|
|
1009
|
+
name: "percent2"
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
kind: "pipe",
|
|
1013
|
+
type: ReasonPipe,
|
|
1014
|
+
name: "reason"
|
|
1015
|
+
}
|
|
1016
|
+
],
|
|
1017
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
1018
|
+
});
|
|
1019
|
+
};
|
|
1020
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1021
|
+
minVersion: "12.0.0",
|
|
1022
|
+
version: "22.2.4",
|
|
1023
|
+
ngImport: i0,
|
|
1024
|
+
type: ServicePage,
|
|
1025
|
+
decorators: [{
|
|
1026
|
+
type: Component,
|
|
1027
|
+
args: [{
|
|
1028
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1029
|
+
imports: [
|
|
1030
|
+
LinkPipe,
|
|
1031
|
+
RouterLink,
|
|
1032
|
+
MatTabsModule,
|
|
1033
|
+
MatProgressBarModule,
|
|
1034
|
+
MatButtonModule,
|
|
1035
|
+
ContextBar,
|
|
1036
|
+
HealthBadge,
|
|
1037
|
+
ConfidenceBadge,
|
|
1038
|
+
TraceTable,
|
|
1039
|
+
LogTable,
|
|
1040
|
+
MetricExplorer,
|
|
1041
|
+
ServiceStatsCharts,
|
|
1042
|
+
ServiceCapacity,
|
|
1043
|
+
AgoPipe,
|
|
1044
|
+
DurationPipe,
|
|
1045
|
+
PercentPipe,
|
|
1046
|
+
ReasonPipe
|
|
1047
|
+
],
|
|
1048
|
+
template: `
|
|
1049
|
+
<jc-context-bar>
|
|
1050
|
+
<a [routerLink]="['services'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Services</a>
|
|
1051
|
+
<span class="jc-crumb-sep">›</span>
|
|
1052
|
+
<h1>{{ service.value()?.key ?? '…' }}</h1>
|
|
1053
|
+
@for (l of service.value()?.links ?? []; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
|
|
1054
|
+
</jc-context-bar>
|
|
1055
|
+
@if (service.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
1056
|
+
@if (service.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
1057
|
+
|
|
1058
|
+
@if (service.value(); as s) {
|
|
1059
|
+
<div class="jc-health-strip">
|
|
1060
|
+
@for (h of health(); track h.environment) {
|
|
1061
|
+
<div class="jc-card jc-health-card" [class.jc-selected]="h.environment === ctx.environment()">
|
|
1062
|
+
<div class="jc-row"><strong>{{ h.environment ?? 'no environment' }}</strong><jc-health [state]="h.state" [stale]="h.stale" /></div>
|
|
1063
|
+
<ul class="jc-reasons">
|
|
1064
|
+
@for (r of h.reasons; track r) {
|
|
1065
|
+
<li>
|
|
1066
|
+
@if (dependencyOf(r); as dep) {
|
|
1067
|
+
{{ r | reason }} — <a [routerLink]="['services', dep.id] | jcLink" [queryParams]="ctx.keep()">open</a>
|
|
1068
|
+
} @else { {{ r | reason }} }
|
|
1069
|
+
</li>
|
|
1070
|
+
}
|
|
1071
|
+
</ul>
|
|
1072
|
+
<div class="jc-muted jc-small">since {{ h.since | ago }} · evaluated {{ h.evaluatedAt | ago }}</div>
|
|
1073
|
+
@if (h.environment && h.environment !== ctx.environment()) {
|
|
1074
|
+
<button mat-button (click)="ctx.set({ env: h.environment })">Investigate {{ h.environment }}</button>
|
|
1075
|
+
}
|
|
1076
|
+
</div>
|
|
1077
|
+
} @empty { <p class="jc-muted">No health evaluated yet: no data.</p> }
|
|
1078
|
+
</div>
|
|
1079
|
+
|
|
1080
|
+
<mat-tab-group [selectedIndex]="tabIndex()" (selectedIndexChange)="openTab($event)" animationDuration="0ms" mat-stretch-tabs="false">
|
|
1081
|
+
<mat-tab label="Overview">
|
|
1082
|
+
@if (tabIndex() === 0) {
|
|
1083
|
+
<jc-service-stats [query]="{ serviceId: id() }" />
|
|
1084
|
+
<h3>Instances</h3>
|
|
1085
|
+
<table class="jc-table">
|
|
1086
|
+
<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>
|
|
1087
|
+
<tbody>
|
|
1088
|
+
@for (i of instances(); track i.id) {
|
|
1089
|
+
<tr>
|
|
1090
|
+
<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>
|
|
1091
|
+
<td>{{ i.environment }}</td>
|
|
1092
|
+
<td><jc-health [state]="i.health" /></td>
|
|
1093
|
+
<td>{{ i.serviceVersion ?? '—' }}</td>
|
|
1094
|
+
<td>{{ i.state }}</td>
|
|
1095
|
+
<td>{{ i.lastSeen | ago }}</td>
|
|
1096
|
+
<td class="jc-nowrap">
|
|
1097
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', instance: i.id })">traces</a> ·
|
|
1098
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'logs', instance: i.id })">logs</a>
|
|
1099
|
+
@if (i.hostResourceId) { · <a [routerLink]="['resources', i.hostResourceId] | jcLink" [queryParams]="ctx.keep()">host</a> }
|
|
1100
|
+
</td>
|
|
1101
|
+
</tr>
|
|
1102
|
+
} @empty { <tr><td colspan="7" class="jc-muted">No instances.</td></tr> }
|
|
1103
|
+
</tbody>
|
|
1104
|
+
</table>
|
|
1105
|
+
}
|
|
1106
|
+
</mat-tab>
|
|
1107
|
+
|
|
1108
|
+
<mat-tab label="Operations">
|
|
1109
|
+
@if (tabIndex() === 1) {
|
|
1110
|
+
@if (operations.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
1111
|
+
<table class="jc-table">
|
|
1112
|
+
<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>
|
|
1113
|
+
<tbody>
|
|
1114
|
+
@for (o of operations.value()?.items ?? []; track o.name + o.kind) {
|
|
1115
|
+
<tr [class.jc-selected]="o.name === operation()">
|
|
1116
|
+
<td><a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'operations', op: o.name })">{{ o.name }}</a></td>
|
|
1117
|
+
<td>{{ o.kind }}</td>
|
|
1118
|
+
<td>{{ o.count }}</td>
|
|
1119
|
+
<td>{{ o.requestsPerSecond.toFixed(2) }}</td>
|
|
1120
|
+
<td [class.jc-bad]="o.errorRate >= 0.05">{{ o.errorRate | percent2 }}</td>
|
|
1121
|
+
<td>{{ o.p50Ms | duration }}</td><td>{{ o.p95Ms | duration }}</td><td>{{ o.p99Ms | duration }}</td>
|
|
1122
|
+
<td>{{ o.lastSeen | ago }}</td>
|
|
1123
|
+
<td class="jc-nowrap">
|
|
1124
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, status: 'error' })">error traces</a> ·
|
|
1125
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, slow: o.p95Ms })">slow traces</a>
|
|
1126
|
+
</td>
|
|
1127
|
+
</tr>
|
|
1128
|
+
} @empty { <tr><td colspan="10" class="jc-muted">No operations in this window.</td></tr> }
|
|
1129
|
+
</tbody>
|
|
1130
|
+
</table>
|
|
1131
|
+
@if (operation(); as op) {
|
|
1132
|
+
<h3>{{ op }}</h3>
|
|
1133
|
+
<jc-service-stats [query]="{ serviceId: id(), operation: op }" />
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
</mat-tab>
|
|
1137
|
+
|
|
1138
|
+
<mat-tab label="Dependencies">
|
|
1139
|
+
@if (tabIndex() === 2) {
|
|
1140
|
+
<h3>Calls and uses</h3>
|
|
1141
|
+
<table class="jc-table">
|
|
1142
|
+
<thead><tr><th>Target</th><th>Relation</th><th>Evidence</th><th>Spans seen</th><th>Last seen</th><th></th></tr></thead>
|
|
1143
|
+
<tbody>
|
|
1144
|
+
@for (d of downstream(); track d.edge.id) {
|
|
1145
|
+
<tr>
|
|
1146
|
+
<td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a> <span class="jc-muted jc-small">{{ d.type }}</span></td>
|
|
1147
|
+
<td>{{ d.edge.relation }}</td>
|
|
1148
|
+
<td><jc-confidence [confidence]="d.edge.confidence" /></td>
|
|
1149
|
+
<td>{{ d.edge.spanCount }}</td>
|
|
1150
|
+
<td>{{ d.edge.lastSeen | ago }}</td>
|
|
1151
|
+
<td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a> ·
|
|
1152
|
+
<a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', dependency: d.edge.id })">traces</a></td>
|
|
1153
|
+
</tr>
|
|
1154
|
+
} @empty { <tr><td colspan="6" class="jc-muted">No downstream dependencies seen.</td></tr> }
|
|
1155
|
+
</tbody>
|
|
1156
|
+
</table>
|
|
1157
|
+
<h3>Called by</h3>
|
|
1158
|
+
<table class="jc-table">
|
|
1159
|
+
<tbody>
|
|
1160
|
+
@for (d of upstream(); track d.edge.id) {
|
|
1161
|
+
<tr><td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a></td><td>{{ d.edge.relation }}</td>
|
|
1162
|
+
<td><jc-confidence [confidence]="d.edge.confidence" /></td><td>{{ d.edge.lastSeen | ago }}</td>
|
|
1163
|
+
<td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a></td></tr>
|
|
1164
|
+
} @empty { <tr><td class="jc-muted">No callers seen.</td></tr> }
|
|
1165
|
+
</tbody>
|
|
1166
|
+
</table>
|
|
1167
|
+
}
|
|
1168
|
+
</mat-tab>
|
|
1169
|
+
|
|
1170
|
+
<mat-tab label="Traces">
|
|
1171
|
+
@if (tabIndex() === 3) {
|
|
1172
|
+
<div class="jc-filters">
|
|
1173
|
+
@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> }
|
|
1174
|
+
</div>
|
|
1175
|
+
<jc-trace-table [query]="traceQuery()" />
|
|
1176
|
+
}
|
|
1177
|
+
</mat-tab>
|
|
1178
|
+
|
|
1179
|
+
<mat-tab label="Logs">
|
|
1180
|
+
@if (tabIndex() === 4) { <jc-log-table [query]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
|
|
1181
|
+
</mat-tab>
|
|
1182
|
+
|
|
1183
|
+
<mat-tab label="Metrics">
|
|
1184
|
+
@if (tabIndex() === 5) { <jc-metric-explorer [scope]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
|
|
1185
|
+
</mat-tab>
|
|
1186
|
+
|
|
1187
|
+
<mat-tab label="Capacity">
|
|
1188
|
+
@if (tabIndex() === 6) { <jc-service-capacity [serviceId]="id()" [environments]="environments()" /> }
|
|
1189
|
+
</mat-tab>
|
|
1190
|
+
</mat-tab-group>
|
|
1191
|
+
}`
|
|
1192
|
+
}]
|
|
1193
|
+
}],
|
|
1194
|
+
propDecorators: {
|
|
1195
|
+
id: [{
|
|
1196
|
+
type: i0.Input,
|
|
1197
|
+
args: [{
|
|
1198
|
+
isSignal: true,
|
|
1199
|
+
alias: "id",
|
|
1200
|
+
required: true
|
|
1201
|
+
}]
|
|
1202
|
+
}],
|
|
1203
|
+
tab: [{
|
|
1204
|
+
type: i0.Input,
|
|
1205
|
+
args: [{
|
|
1206
|
+
isSignal: true,
|
|
1207
|
+
alias: "tab",
|
|
1208
|
+
required: false
|
|
1209
|
+
}]
|
|
1210
|
+
}],
|
|
1211
|
+
op: [{
|
|
1212
|
+
type: i0.Input,
|
|
1213
|
+
args: [{
|
|
1214
|
+
isSignal: true,
|
|
1215
|
+
alias: "op",
|
|
1216
|
+
required: false
|
|
1217
|
+
}]
|
|
1218
|
+
}],
|
|
1219
|
+
status: [{
|
|
1220
|
+
type: i0.Input,
|
|
1221
|
+
args: [{
|
|
1222
|
+
isSignal: true,
|
|
1223
|
+
alias: "status",
|
|
1224
|
+
required: false
|
|
1225
|
+
}]
|
|
1226
|
+
}],
|
|
1227
|
+
slow: [{
|
|
1228
|
+
type: i0.Input,
|
|
1229
|
+
args: [{
|
|
1230
|
+
isSignal: true,
|
|
1231
|
+
alias: "slow",
|
|
1232
|
+
required: false
|
|
1233
|
+
}]
|
|
1234
|
+
}],
|
|
1235
|
+
instance: [{
|
|
1236
|
+
type: i0.Input,
|
|
1237
|
+
args: [{
|
|
1238
|
+
isSignal: true,
|
|
1239
|
+
alias: "instance",
|
|
1240
|
+
required: false
|
|
1241
|
+
}]
|
|
1242
|
+
}],
|
|
1243
|
+
dependency: [{
|
|
1244
|
+
type: i0.Input,
|
|
1245
|
+
args: [{
|
|
1246
|
+
isSignal: true,
|
|
1247
|
+
alias: "dependency",
|
|
1248
|
+
required: false
|
|
1249
|
+
}]
|
|
1250
|
+
}]
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
export { ServicePage };
|
|
1254
|
+
|
|
1255
|
+
//# sourceMappingURL=juice-js-observability-service.page-BrcID0Bs.mjs.map
|