@juice-js/observability 0.0.0-stage → 1.0.0-alpha.5

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.
Files changed (34) hide show
  1. package/README.md +60 -2
  2. package/fesm2022/juice-js-observability-alert.page-BHKAUvR-.mjs +404 -0
  3. package/fesm2022/juice-js-observability-alert.page-BHKAUvR-.mjs.map +1 -0
  4. package/fesm2022/juice-js-observability-alerts.page-C2tXzLQl.mjs +208 -0
  5. package/fesm2022/juice-js-observability-alerts.page-C2tXzLQl.mjs.map +1 -0
  6. package/fesm2022/juice-js-observability-logs.page--j6W1Ej-.mjs +413 -0
  7. package/fesm2022/juice-js-observability-logs.page--j6W1Ej-.mjs.map +1 -0
  8. package/fesm2022/juice-js-observability-metrics.page-DR6qZtfl.mjs +267 -0
  9. package/fesm2022/juice-js-observability-metrics.page-DR6qZtfl.mjs.map +1 -0
  10. package/fesm2022/juice-js-observability-overview.page-DU0GxSXQ.mjs +243 -0
  11. package/fesm2022/juice-js-observability-overview.page-DU0GxSXQ.mjs.map +1 -0
  12. package/fesm2022/juice-js-observability-resource.page-D6qpvo_f.mjs +274 -0
  13. package/fesm2022/juice-js-observability-resource.page-D6qpvo_f.mjs.map +1 -0
  14. package/fesm2022/juice-js-observability-resources.page-CR7-DFsh.mjs +209 -0
  15. package/fesm2022/juice-js-observability-resources.page-CR7-DFsh.mjs.map +1 -0
  16. package/fesm2022/juice-js-observability-rules.page-fq-wO5I_.mjs +175 -0
  17. package/fesm2022/juice-js-observability-rules.page-fq-wO5I_.mjs.map +1 -0
  18. package/fesm2022/juice-js-observability-service.page-Qc6FYChB.mjs +726 -0
  19. package/fesm2022/juice-js-observability-service.page-Qc6FYChB.mjs.map +1 -0
  20. package/fesm2022/juice-js-observability-services.page-CX6S7sUu.mjs +251 -0
  21. package/fesm2022/juice-js-observability-services.page-CX6S7sUu.mjs.map +1 -0
  22. package/fesm2022/juice-js-observability-telemetry-D9EYxkdr.mjs +1783 -0
  23. package/fesm2022/juice-js-observability-telemetry-D9EYxkdr.mjs.map +1 -0
  24. package/fesm2022/juice-js-observability-topology.page-CLz7J5pq.mjs +483 -0
  25. package/fesm2022/juice-js-observability-topology.page-CLz7J5pq.mjs.map +1 -0
  26. package/fesm2022/juice-js-observability-trace.page-DccvH1SV.mjs +518 -0
  27. package/fesm2022/juice-js-observability-trace.page-DccvH1SV.mjs.map +1 -0
  28. package/fesm2022/juice-js-observability-traces.page-BZxnac4Y.mjs +520 -0
  29. package/fesm2022/juice-js-observability-traces.page-BZxnac4Y.mjs.map +1 -0
  30. package/fesm2022/juice-js-observability.mjs +396 -0
  31. package/fesm2022/juice-js-observability.mjs.map +1 -0
  32. package/package.json +48 -4
  33. package/styles/_observability.scss +216 -0
  34. package/types/juice-js-observability.d.ts +387 -0
@@ -0,0 +1,726 @@
1
+ import { ApiService, ContextService, LinkPipe, ObservabilityLinks } from "./juice-js-observability.mjs";
2
+ import { _ as ReasonPipe, a as problemText, c as ConfidenceBadge, g as PercentPipe, h as DurationPipe, i as TraceTable, l as ContextBar, m as AgoPipe, n as MetricExplorer, r as ServiceStatsCharts, t as LogTable, u as HealthBadge, x as apiResource } from "./juice-js-observability-telemetry-D9EYxkdr.mjs";
3
+ import * as i0 from "@angular/core";
4
+ import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
5
+ import { Router, RouterLink } from "@angular/router";
6
+ import * as i3 from "@angular/material/button";
7
+ import { MatButtonModule } from "@angular/material/button";
8
+ import * as i2 from "@angular/material/progress-bar";
9
+ import { MatProgressBarModule } from "@angular/material/progress-bar";
10
+ import * as i1 from "@angular/material/tabs";
11
+ import { MatTabsModule } from "@angular/material/tabs";
12
+ const TABS = [
13
+ "overview",
14
+ "operations",
15
+ "dependencies",
16
+ "traces",
17
+ "logs",
18
+ "metrics"
19
+ ];
20
+ var ServicePage = class ServicePage {
21
+ ctx = inject(ContextService);
22
+ api = inject(ApiService);
23
+ router = inject(Router);
24
+ links = inject(ObservabilityLinks);
25
+ id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
26
+ tab = input(...ngDevMode ? [void 0, { debugName: "tab" }] : /* istanbul ignore next */ []);
27
+ op = input(...ngDevMode ? [void 0, { debugName: "op" }] : /* istanbul ignore next */ []);
28
+ status = input(...ngDevMode ? [void 0, { debugName: "status" }] : /* istanbul ignore next */ []);
29
+ slow = input(...ngDevMode ? [void 0, { debugName: "slow" }] : /* istanbul ignore next */ []);
30
+ instance = input(...ngDevMode ? [void 0, { debugName: "instance" }] : /* istanbul ignore next */ []);
31
+ dependency = input(...ngDevMode ? [void 0, { debugName: "dependency" }] : /* istanbul ignore next */ []);
32
+ tabIndex = computed(() => Math.max(0, TABS.indexOf(this.tab() ?? "overview")), ...ngDevMode ? [{ debugName: "tabIndex" }] : /* istanbul ignore next */ []);
33
+ operation = computed(() => this.op() ?? null, ...ngDevMode ? [{ debugName: "operation" }] : /* istanbul ignore next */ []);
34
+ instanceFilter = computed(() => this.instance() ?? null, ...ngDevMode ? [{ debugName: "instanceFilter" }] : /* istanbul ignore next */ []);
35
+ service = apiResource({
36
+ params: () => this.id(),
37
+ stream: ({ params }) => this.api.resource(params)
38
+ });
39
+ topology = apiResource({
40
+ params: () => this.ctx.environment(),
41
+ stream: ({ params }) => this.api.topology(params || null)
42
+ });
43
+ operations = apiResource({
44
+ params: () => ({
45
+ id: this.id(),
46
+ env: this.ctx.environment(),
47
+ range: this.ctx.range(),
48
+ abs: this.ctx.absolute(),
49
+ on: this.tabIndex() === 1
50
+ }),
51
+ stream: ({ params }) => this.api.operations({
52
+ serviceId: params.id,
53
+ environment: params.env,
54
+ ...this.ctx.window(),
55
+ limit: 200
56
+ })
57
+ });
58
+ error = computed(() => problemText(this.service.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
59
+ health = computed(() => (this.service.value()?.health ?? []).filter((h) => h.subjectType === "service"), ...ngDevMode ? [{ debugName: "health" }] : /* istanbul ignore next */ []);
60
+ instances = computed(() => {
61
+ const env = this.ctx.environment();
62
+ return (this.service.value()?.instances ?? []).filter((i) => !env || i.environment === env);
63
+ }, ...ngDevMode ? [{ debugName: "instances" }] : /* istanbul ignore next */ []);
64
+ nodes = computed(() => new Map((this.topology.value()?.nodes ?? []).map((n) => [n.id, n])), ...ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []);
65
+ edgesWhere(match) {
66
+ const other = match === "source" ? "target" : "source";
67
+ return (this.topology.value()?.edges ?? []).filter((e) => e[match] === this.id()).map((edge) => {
68
+ const node = this.nodes().get(edge[other]);
69
+ return {
70
+ edge,
71
+ name: node?.displayName ?? edge[other],
72
+ type: node?.type ?? "",
73
+ link: this.links.to(node?.type === "service" ? "services" : "resources", edge[other])
74
+ };
75
+ });
76
+ }
77
+ downstream = computed(() => this.edgesWhere("source"), ...ngDevMode ? [{ debugName: "downstream" }] : /* istanbul ignore next */ []);
78
+ upstream = computed(() => this.edgesWhere("target"), ...ngDevMode ? [{ debugName: "upstream" }] : /* istanbul ignore next */ []);
79
+ dependencyOf(reason) {
80
+ const name = reason.match(/^(?:critical_)?dependency_(?:failed|unknown):(.+)$/)?.[1];
81
+ if (!name) return null;
82
+ const node = [...this.nodes().values()].find((n) => n.type === "service" && (n.displayName === name || name.endsWith(`/${n.displayName}`)));
83
+ return node ? { id: node.id } : null;
84
+ }
85
+ traceQuery = computed(() => ({
86
+ serviceId: this.id(),
87
+ operation: this.op(),
88
+ status: this.status(),
89
+ minDurationMs: this.slow(),
90
+ instanceId: this.instance(),
91
+ dependencyId: this.dependency()
92
+ }), ...ngDevMode ? [{ debugName: "traceQuery" }] : /* istanbul ignore next */ []);
93
+ traceFilters = computed(() => {
94
+ const filters = [];
95
+ if (this.op()) filters.push({
96
+ label: `operation: ${this.op()}`,
97
+ clear: { op: null }
98
+ });
99
+ if (this.status()) filters.push({
100
+ label: `status: ${this.status()}`,
101
+ clear: { status: null }
102
+ });
103
+ if (this.slow()) filters.push({
104
+ label: `slower than ${Math.round(Number(this.slow()))} ms`,
105
+ clear: { slow: null }
106
+ });
107
+ if (this.instance()) filters.push({
108
+ label: "one instance",
109
+ clear: { instance: null }
110
+ });
111
+ if (this.dependency()) filters.push({
112
+ label: "through one dependency",
113
+ clear: { dependency: null }
114
+ });
115
+ return filters;
116
+ }, ...ngDevMode ? [{ debugName: "traceFilters" }] : /* istanbul ignore next */ []);
117
+ openTab(index) {
118
+ this.router.navigate([], {
119
+ queryParams: { tab: TABS[index] },
120
+ queryParamsHandling: "merge"
121
+ });
122
+ }
123
+ static ɵfac = i0.ɵɵngDeclareFactory({
124
+ minVersion: "12.0.0",
125
+ version: "22.2.4",
126
+ ngImport: i0,
127
+ type: ServicePage,
128
+ deps: [],
129
+ target: i0.ɵɵFactoryTarget.Component
130
+ });
131
+ static ɵcmp = i0.ɵɵngDeclareComponent({
132
+ minVersion: "17.0.0",
133
+ version: "22.2.4",
134
+ type: ServicePage,
135
+ isStandalone: true,
136
+ selector: "ng-component",
137
+ inputs: {
138
+ id: {
139
+ classPropertyName: "id",
140
+ publicName: "id",
141
+ isSignal: true,
142
+ isRequired: true,
143
+ transformFunction: null
144
+ },
145
+ tab: {
146
+ classPropertyName: "tab",
147
+ publicName: "tab",
148
+ isSignal: true,
149
+ isRequired: false,
150
+ transformFunction: null
151
+ },
152
+ op: {
153
+ classPropertyName: "op",
154
+ publicName: "op",
155
+ isSignal: true,
156
+ isRequired: false,
157
+ transformFunction: null
158
+ },
159
+ status: {
160
+ classPropertyName: "status",
161
+ publicName: "status",
162
+ isSignal: true,
163
+ isRequired: false,
164
+ transformFunction: null
165
+ },
166
+ slow: {
167
+ classPropertyName: "slow",
168
+ publicName: "slow",
169
+ isSignal: true,
170
+ isRequired: false,
171
+ transformFunction: null
172
+ },
173
+ instance: {
174
+ classPropertyName: "instance",
175
+ publicName: "instance",
176
+ isSignal: true,
177
+ isRequired: false,
178
+ transformFunction: null
179
+ },
180
+ dependency: {
181
+ classPropertyName: "dependency",
182
+ publicName: "dependency",
183
+ isSignal: true,
184
+ isRequired: false,
185
+ transformFunction: null
186
+ }
187
+ },
188
+ ngImport: i0,
189
+ template: `
190
+ <jc-context-bar>
191
+ <a [routerLink]="['services'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Services</a>
192
+ <span class="jc-crumb-sep">›</span>
193
+ <h1>{{ service.value()?.key ?? '…' }}</h1>
194
+ @for (l of service.value()?.links ?? []; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
195
+ </jc-context-bar>
196
+ @if (service.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
197
+ @if (service.error()) { <p class="jc-error">{{ error() }}</p> }
198
+
199
+ @if (service.value(); as s) {
200
+ <div class="jc-health-strip">
201
+ @for (h of health(); track h.environment) {
202
+ <div class="jc-card jc-health-card" [class.jc-selected]="h.environment === ctx.environment()">
203
+ <div class="jc-row"><strong>{{ h.environment ?? 'no environment' }}</strong><jc-health [state]="h.state" [stale]="h.stale" /></div>
204
+ <ul class="jc-reasons">
205
+ @for (r of h.reasons; track r) {
206
+ <li>
207
+ @if (dependencyOf(r); as dep) {
208
+ {{ r | reason }} — <a [routerLink]="['services', dep.id] | jcLink" [queryParams]="ctx.keep()">open</a>
209
+ } @else { {{ r | reason }} }
210
+ </li>
211
+ }
212
+ </ul>
213
+ <div class="jc-muted jc-small">since {{ h.since | ago }} · evaluated {{ h.evaluatedAt | ago }}</div>
214
+ @if (h.environment && h.environment !== ctx.environment()) {
215
+ <button mat-button (click)="ctx.set({ env: h.environment })">Investigate {{ h.environment }}</button>
216
+ }
217
+ </div>
218
+ } @empty { <p class="jc-muted">No health evaluated yet: no data.</p> }
219
+ </div>
220
+
221
+ <mat-tab-group [selectedIndex]="tabIndex()" (selectedIndexChange)="openTab($event)" animationDuration="0ms" mat-stretch-tabs="false">
222
+ <mat-tab label="Overview">
223
+ @if (tabIndex() === 0) {
224
+ <jc-service-stats [query]="{ serviceId: id() }" />
225
+ <h3>Instances</h3>
226
+ <table class="jc-table">
227
+ <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>
228
+ <tbody>
229
+ @for (i of instances(); track i.id) {
230
+ <tr>
231
+ <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>
232
+ <td>{{ i.environment }}</td>
233
+ <td><jc-health [state]="i.health" /></td>
234
+ <td>{{ i.serviceVersion ?? '—' }}</td>
235
+ <td>{{ i.state }}</td>
236
+ <td>{{ i.lastSeen | ago }}</td>
237
+ <td class="jc-nowrap">
238
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', instance: i.id })">traces</a> ·
239
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'logs', instance: i.id })">logs</a>
240
+ @if (i.hostResourceId) { · <a [routerLink]="['resources', i.hostResourceId] | jcLink" [queryParams]="ctx.keep()">host</a> }
241
+ </td>
242
+ </tr>
243
+ } @empty { <tr><td colspan="7" class="jc-muted">No instances.</td></tr> }
244
+ </tbody>
245
+ </table>
246
+ }
247
+ </mat-tab>
248
+
249
+ <mat-tab label="Operations">
250
+ @if (tabIndex() === 1) {
251
+ @if (operations.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
252
+ <table class="jc-table">
253
+ <thead><tr><th>Operation</th><th>Kind</th><th>Requests</th><th>Per second</th><th>Errors</th><th>p50</th><th>p95</th><th>p99</th><th>Last seen</th><th></th></tr></thead>
254
+ <tbody>
255
+ @for (o of operations.value()?.items ?? []; track o.name + o.kind) {
256
+ <tr [class.jc-selected]="o.name === operation()">
257
+ <td><a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'operations', op: o.name })">{{ o.name }}</a></td>
258
+ <td>{{ o.kind }}</td>
259
+ <td>{{ o.count }}</td>
260
+ <td>{{ o.requestsPerSecond.toFixed(2) }}</td>
261
+ <td [class.jc-bad]="o.errorRate >= 0.05">{{ o.errorRate | percent2 }}</td>
262
+ <td>{{ o.p50Ms | duration }}</td><td>{{ o.p95Ms | duration }}</td><td>{{ o.p99Ms | duration }}</td>
263
+ <td>{{ o.lastSeen | ago }}</td>
264
+ <td class="jc-nowrap">
265
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, status: 'error' })">error traces</a> ·
266
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, slow: o.p95Ms })">slow traces</a>
267
+ </td>
268
+ </tr>
269
+ } @empty { <tr><td colspan="10" class="jc-muted">No operations in this window.</td></tr> }
270
+ </tbody>
271
+ </table>
272
+ @if (operation(); as op) {
273
+ <h3>{{ op }}</h3>
274
+ <jc-service-stats [query]="{ serviceId: id(), operation: op }" />
275
+ }
276
+ }
277
+ </mat-tab>
278
+
279
+ <mat-tab label="Dependencies">
280
+ @if (tabIndex() === 2) {
281
+ <h3>Calls and uses</h3>
282
+ <table class="jc-table">
283
+ <thead><tr><th>Target</th><th>Relation</th><th>Evidence</th><th>Spans seen</th><th>Last seen</th><th></th></tr></thead>
284
+ <tbody>
285
+ @for (d of downstream(); track d.edge.id) {
286
+ <tr>
287
+ <td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a>&nbsp;<span class="jc-muted jc-small">{{ d.type }}</span></td>
288
+ <td>{{ d.edge.relation }}</td>
289
+ <td><jc-confidence [confidence]="d.edge.confidence" /></td>
290
+ <td>{{ d.edge.spanCount }}</td>
291
+ <td>{{ d.edge.lastSeen | ago }}</td>
292
+ <td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a> ·
293
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', dependency: d.edge.id })">traces</a></td>
294
+ </tr>
295
+ } @empty { <tr><td colspan="6" class="jc-muted">No downstream dependencies seen.</td></tr> }
296
+ </tbody>
297
+ </table>
298
+ <h3>Called by</h3>
299
+ <table class="jc-table">
300
+ <tbody>
301
+ @for (d of upstream(); track d.edge.id) {
302
+ <tr><td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a></td><td>{{ d.edge.relation }}</td>
303
+ <td><jc-confidence [confidence]="d.edge.confidence" /></td><td>{{ d.edge.lastSeen | ago }}</td>
304
+ <td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a></td></tr>
305
+ } @empty { <tr><td class="jc-muted">No callers seen.</td></tr> }
306
+ </tbody>
307
+ </table>
308
+ }
309
+ </mat-tab>
310
+
311
+ <mat-tab label="Traces">
312
+ @if (tabIndex() === 3) {
313
+ <div class="jc-filters">
314
+ @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> }
315
+ </div>
316
+ <jc-trace-table [query]="traceQuery()" />
317
+ }
318
+ </mat-tab>
319
+
320
+ <mat-tab label="Logs">
321
+ @if (tabIndex() === 4) { <jc-log-table [query]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
322
+ </mat-tab>
323
+
324
+ <mat-tab label="Metrics">
325
+ @if (tabIndex() === 5) { <jc-metric-explorer [scope]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
326
+ </mat-tab>
327
+ </mat-tab-group>
328
+ }`,
329
+ isInline: true,
330
+ dependencies: [
331
+ {
332
+ kind: "directive",
333
+ type: RouterLink,
334
+ selector: "[routerLink]",
335
+ inputs: [
336
+ "target",
337
+ "queryParams",
338
+ "fragment",
339
+ "queryParamsHandling",
340
+ "state",
341
+ "info",
342
+ "relativeTo",
343
+ "preserveFragment",
344
+ "skipLocationChange",
345
+ "replaceUrl",
346
+ "browserUrl",
347
+ "routerLink"
348
+ ]
349
+ },
350
+ {
351
+ kind: "ngmodule",
352
+ type: MatTabsModule
353
+ },
354
+ {
355
+ kind: "component",
356
+ type: i1.MatTab,
357
+ selector: "mat-tab",
358
+ inputs: [
359
+ "disabled",
360
+ "label",
361
+ "aria-label",
362
+ "aria-labelledby",
363
+ "labelClass",
364
+ "bodyClass",
365
+ "id"
366
+ ],
367
+ exportAs: ["matTab"]
368
+ },
369
+ {
370
+ kind: "component",
371
+ type: i1.MatTabGroup,
372
+ selector: "mat-tab-group",
373
+ inputs: [
374
+ "color",
375
+ "fitInkBarToContent",
376
+ "mat-stretch-tabs",
377
+ "mat-align-tabs",
378
+ "dynamicHeight",
379
+ "selectedIndex",
380
+ "headerPosition",
381
+ "animationDuration",
382
+ "contentTabIndex",
383
+ "disablePagination",
384
+ "disableRipple",
385
+ "preserveContent",
386
+ "backgroundColor",
387
+ "aria-label",
388
+ "aria-labelledby"
389
+ ],
390
+ outputs: [
391
+ "selectedIndexChange",
392
+ "focusChange",
393
+ "animationDone",
394
+ "selectedTabChange"
395
+ ],
396
+ exportAs: ["matTabGroup"]
397
+ },
398
+ {
399
+ kind: "ngmodule",
400
+ type: MatProgressBarModule
401
+ },
402
+ {
403
+ kind: "component",
404
+ type: i2.MatProgressBar,
405
+ selector: "mat-progress-bar",
406
+ inputs: [
407
+ "color",
408
+ "value",
409
+ "bufferValue",
410
+ "mode"
411
+ ],
412
+ outputs: ["animationEnd"],
413
+ exportAs: ["matProgressBar"]
414
+ },
415
+ {
416
+ kind: "ngmodule",
417
+ type: MatButtonModule
418
+ },
419
+ {
420
+ kind: "component",
421
+ type: i3.MatButton,
422
+ 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] ",
423
+ inputs: ["matButton"],
424
+ exportAs: ["matButton", "matAnchor"]
425
+ },
426
+ {
427
+ kind: "component",
428
+ type: ContextBar,
429
+ selector: "jc-context-bar"
430
+ },
431
+ {
432
+ kind: "component",
433
+ type: HealthBadge,
434
+ selector: "jc-health",
435
+ inputs: ["state", "stale"]
436
+ },
437
+ {
438
+ kind: "component",
439
+ type: ConfidenceBadge,
440
+ selector: "jc-confidence",
441
+ inputs: ["confidence"]
442
+ },
443
+ {
444
+ kind: "component",
445
+ type: TraceTable,
446
+ selector: "jc-trace-table",
447
+ inputs: ["query", "limit"]
448
+ },
449
+ {
450
+ kind: "component",
451
+ type: LogTable,
452
+ selector: "jc-log-table",
453
+ inputs: ["query", "compact"]
454
+ },
455
+ {
456
+ kind: "component",
457
+ type: MetricExplorer,
458
+ selector: "jc-metric-explorer",
459
+ inputs: ["scope", "initial"]
460
+ },
461
+ {
462
+ kind: "component",
463
+ type: ServiceStatsCharts,
464
+ selector: "jc-service-stats",
465
+ inputs: ["query"]
466
+ },
467
+ {
468
+ kind: "pipe",
469
+ type: LinkPipe,
470
+ name: "jcLink"
471
+ },
472
+ {
473
+ kind: "pipe",
474
+ type: AgoPipe,
475
+ name: "ago"
476
+ },
477
+ {
478
+ kind: "pipe",
479
+ type: DurationPipe,
480
+ name: "duration"
481
+ },
482
+ {
483
+ kind: "pipe",
484
+ type: PercentPipe,
485
+ name: "percent2"
486
+ },
487
+ {
488
+ kind: "pipe",
489
+ type: ReasonPipe,
490
+ name: "reason"
491
+ }
492
+ ],
493
+ changeDetection: i0.ChangeDetectionStrategy.OnPush
494
+ });
495
+ };
496
+ i0.ɵɵngDeclareClassMetadata({
497
+ minVersion: "12.0.0",
498
+ version: "22.2.4",
499
+ ngImport: i0,
500
+ type: ServicePage,
501
+ decorators: [{
502
+ type: Component,
503
+ args: [{
504
+ changeDetection: ChangeDetectionStrategy.OnPush,
505
+ imports: [
506
+ LinkPipe,
507
+ RouterLink,
508
+ MatTabsModule,
509
+ MatProgressBarModule,
510
+ MatButtonModule,
511
+ ContextBar,
512
+ HealthBadge,
513
+ ConfidenceBadge,
514
+ TraceTable,
515
+ LogTable,
516
+ MetricExplorer,
517
+ ServiceStatsCharts,
518
+ AgoPipe,
519
+ DurationPipe,
520
+ PercentPipe,
521
+ ReasonPipe
522
+ ],
523
+ template: `
524
+ <jc-context-bar>
525
+ <a [routerLink]="['services'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Services</a>
526
+ <span class="jc-crumb-sep">›</span>
527
+ <h1>{{ service.value()?.key ?? '…' }}</h1>
528
+ @for (l of service.value()?.links ?? []; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
529
+ </jc-context-bar>
530
+ @if (service.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
531
+ @if (service.error()) { <p class="jc-error">{{ error() }}</p> }
532
+
533
+ @if (service.value(); as s) {
534
+ <div class="jc-health-strip">
535
+ @for (h of health(); track h.environment) {
536
+ <div class="jc-card jc-health-card" [class.jc-selected]="h.environment === ctx.environment()">
537
+ <div class="jc-row"><strong>{{ h.environment ?? 'no environment' }}</strong><jc-health [state]="h.state" [stale]="h.stale" /></div>
538
+ <ul class="jc-reasons">
539
+ @for (r of h.reasons; track r) {
540
+ <li>
541
+ @if (dependencyOf(r); as dep) {
542
+ {{ r | reason }} — <a [routerLink]="['services', dep.id] | jcLink" [queryParams]="ctx.keep()">open</a>
543
+ } @else { {{ r | reason }} }
544
+ </li>
545
+ }
546
+ </ul>
547
+ <div class="jc-muted jc-small">since {{ h.since | ago }} · evaluated {{ h.evaluatedAt | ago }}</div>
548
+ @if (h.environment && h.environment !== ctx.environment()) {
549
+ <button mat-button (click)="ctx.set({ env: h.environment })">Investigate {{ h.environment }}</button>
550
+ }
551
+ </div>
552
+ } @empty { <p class="jc-muted">No health evaluated yet: no data.</p> }
553
+ </div>
554
+
555
+ <mat-tab-group [selectedIndex]="tabIndex()" (selectedIndexChange)="openTab($event)" animationDuration="0ms" mat-stretch-tabs="false">
556
+ <mat-tab label="Overview">
557
+ @if (tabIndex() === 0) {
558
+ <jc-service-stats [query]="{ serviceId: id() }" />
559
+ <h3>Instances</h3>
560
+ <table class="jc-table">
561
+ <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>
562
+ <tbody>
563
+ @for (i of instances(); track i.id) {
564
+ <tr>
565
+ <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>
566
+ <td>{{ i.environment }}</td>
567
+ <td><jc-health [state]="i.health" /></td>
568
+ <td>{{ i.serviceVersion ?? '—' }}</td>
569
+ <td>{{ i.state }}</td>
570
+ <td>{{ i.lastSeen | ago }}</td>
571
+ <td class="jc-nowrap">
572
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', instance: i.id })">traces</a> ·
573
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'logs', instance: i.id })">logs</a>
574
+ @if (i.hostResourceId) { · <a [routerLink]="['resources', i.hostResourceId] | jcLink" [queryParams]="ctx.keep()">host</a> }
575
+ </td>
576
+ </tr>
577
+ } @empty { <tr><td colspan="7" class="jc-muted">No instances.</td></tr> }
578
+ </tbody>
579
+ </table>
580
+ }
581
+ </mat-tab>
582
+
583
+ <mat-tab label="Operations">
584
+ @if (tabIndex() === 1) {
585
+ @if (operations.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
586
+ <table class="jc-table">
587
+ <thead><tr><th>Operation</th><th>Kind</th><th>Requests</th><th>Per second</th><th>Errors</th><th>p50</th><th>p95</th><th>p99</th><th>Last seen</th><th></th></tr></thead>
588
+ <tbody>
589
+ @for (o of operations.value()?.items ?? []; track o.name + o.kind) {
590
+ <tr [class.jc-selected]="o.name === operation()">
591
+ <td><a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'operations', op: o.name })">{{ o.name }}</a></td>
592
+ <td>{{ o.kind }}</td>
593
+ <td>{{ o.count }}</td>
594
+ <td>{{ o.requestsPerSecond.toFixed(2) }}</td>
595
+ <td [class.jc-bad]="o.errorRate >= 0.05">{{ o.errorRate | percent2 }}</td>
596
+ <td>{{ o.p50Ms | duration }}</td><td>{{ o.p95Ms | duration }}</td><td>{{ o.p99Ms | duration }}</td>
597
+ <td>{{ o.lastSeen | ago }}</td>
598
+ <td class="jc-nowrap">
599
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, status: 'error' })">error traces</a> ·
600
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', op: o.name, slow: o.p95Ms })">slow traces</a>
601
+ </td>
602
+ </tr>
603
+ } @empty { <tr><td colspan="10" class="jc-muted">No operations in this window.</td></tr> }
604
+ </tbody>
605
+ </table>
606
+ @if (operation(); as op) {
607
+ <h3>{{ op }}</h3>
608
+ <jc-service-stats [query]="{ serviceId: id(), operation: op }" />
609
+ }
610
+ }
611
+ </mat-tab>
612
+
613
+ <mat-tab label="Dependencies">
614
+ @if (tabIndex() === 2) {
615
+ <h3>Calls and uses</h3>
616
+ <table class="jc-table">
617
+ <thead><tr><th>Target</th><th>Relation</th><th>Evidence</th><th>Spans seen</th><th>Last seen</th><th></th></tr></thead>
618
+ <tbody>
619
+ @for (d of downstream(); track d.edge.id) {
620
+ <tr>
621
+ <td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a>&nbsp;<span class="jc-muted jc-small">{{ d.type }}</span></td>
622
+ <td>{{ d.edge.relation }}</td>
623
+ <td><jc-confidence [confidence]="d.edge.confidence" /></td>
624
+ <td>{{ d.edge.spanCount }}</td>
625
+ <td>{{ d.edge.lastSeen | ago }}</td>
626
+ <td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a> ·
627
+ <a [routerLink]="[]" [queryParams]="ctx.keep({ tab: 'traces', dependency: d.edge.id })">traces</a></td>
628
+ </tr>
629
+ } @empty { <tr><td colspan="6" class="jc-muted">No downstream dependencies seen.</td></tr> }
630
+ </tbody>
631
+ </table>
632
+ <h3>Called by</h3>
633
+ <table class="jc-table">
634
+ <tbody>
635
+ @for (d of upstream(); track d.edge.id) {
636
+ <tr><td><a [routerLink]="d.link" [queryParams]="ctx.keep()">{{ d.name }}</a></td><td>{{ d.edge.relation }}</td>
637
+ <td><jc-confidence [confidence]="d.edge.confidence" /></td><td>{{ d.edge.lastSeen | ago }}</td>
638
+ <td><a [routerLink]="['topology'] | jcLink" [queryParams]="ctx.keep({ edge: d.edge.id })">evidence</a></td></tr>
639
+ } @empty { <tr><td class="jc-muted">No callers seen.</td></tr> }
640
+ </tbody>
641
+ </table>
642
+ }
643
+ </mat-tab>
644
+
645
+ <mat-tab label="Traces">
646
+ @if (tabIndex() === 3) {
647
+ <div class="jc-filters">
648
+ @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> }
649
+ </div>
650
+ <jc-trace-table [query]="traceQuery()" />
651
+ }
652
+ </mat-tab>
653
+
654
+ <mat-tab label="Logs">
655
+ @if (tabIndex() === 4) { <jc-log-table [query]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
656
+ </mat-tab>
657
+
658
+ <mat-tab label="Metrics">
659
+ @if (tabIndex() === 5) { <jc-metric-explorer [scope]="{ serviceId: id(), instanceId: instanceFilter() }" /> }
660
+ </mat-tab>
661
+ </mat-tab-group>
662
+ }`
663
+ }]
664
+ }],
665
+ propDecorators: {
666
+ id: [{
667
+ type: i0.Input,
668
+ args: [{
669
+ isSignal: true,
670
+ alias: "id",
671
+ required: true
672
+ }]
673
+ }],
674
+ tab: [{
675
+ type: i0.Input,
676
+ args: [{
677
+ isSignal: true,
678
+ alias: "tab",
679
+ required: false
680
+ }]
681
+ }],
682
+ op: [{
683
+ type: i0.Input,
684
+ args: [{
685
+ isSignal: true,
686
+ alias: "op",
687
+ required: false
688
+ }]
689
+ }],
690
+ status: [{
691
+ type: i0.Input,
692
+ args: [{
693
+ isSignal: true,
694
+ alias: "status",
695
+ required: false
696
+ }]
697
+ }],
698
+ slow: [{
699
+ type: i0.Input,
700
+ args: [{
701
+ isSignal: true,
702
+ alias: "slow",
703
+ required: false
704
+ }]
705
+ }],
706
+ instance: [{
707
+ type: i0.Input,
708
+ args: [{
709
+ isSignal: true,
710
+ alias: "instance",
711
+ required: false
712
+ }]
713
+ }],
714
+ dependency: [{
715
+ type: i0.Input,
716
+ args: [{
717
+ isSignal: true,
718
+ alias: "dependency",
719
+ required: false
720
+ }]
721
+ }]
722
+ }
723
+ });
724
+ export { ServicePage };
725
+
726
+ //# sourceMappingURL=juice-js-observability-service.page-Qc6FYChB.mjs.map