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

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