@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.
- package/README.md +60 -2
- package/fesm2022/juice-js-observability-alert.page-BHKAUvR-.mjs +404 -0
- package/fesm2022/juice-js-observability-alert.page-BHKAUvR-.mjs.map +1 -0
- package/fesm2022/juice-js-observability-alerts.page-C2tXzLQl.mjs +208 -0
- package/fesm2022/juice-js-observability-alerts.page-C2tXzLQl.mjs.map +1 -0
- package/fesm2022/juice-js-observability-logs.page--j6W1Ej-.mjs +413 -0
- package/fesm2022/juice-js-observability-logs.page--j6W1Ej-.mjs.map +1 -0
- package/fesm2022/juice-js-observability-metrics.page-DR6qZtfl.mjs +267 -0
- package/fesm2022/juice-js-observability-metrics.page-DR6qZtfl.mjs.map +1 -0
- package/fesm2022/juice-js-observability-overview.page-DU0GxSXQ.mjs +243 -0
- package/fesm2022/juice-js-observability-overview.page-DU0GxSXQ.mjs.map +1 -0
- package/fesm2022/juice-js-observability-resource.page-D6qpvo_f.mjs +274 -0
- package/fesm2022/juice-js-observability-resource.page-D6qpvo_f.mjs.map +1 -0
- package/fesm2022/juice-js-observability-resources.page-CR7-DFsh.mjs +209 -0
- package/fesm2022/juice-js-observability-resources.page-CR7-DFsh.mjs.map +1 -0
- package/fesm2022/juice-js-observability-rules.page-fq-wO5I_.mjs +175 -0
- package/fesm2022/juice-js-observability-rules.page-fq-wO5I_.mjs.map +1 -0
- package/fesm2022/juice-js-observability-service.page-Qc6FYChB.mjs +726 -0
- package/fesm2022/juice-js-observability-service.page-Qc6FYChB.mjs.map +1 -0
- package/fesm2022/juice-js-observability-services.page-CX6S7sUu.mjs +251 -0
- package/fesm2022/juice-js-observability-services.page-CX6S7sUu.mjs.map +1 -0
- package/fesm2022/juice-js-observability-telemetry-D9EYxkdr.mjs +1783 -0
- package/fesm2022/juice-js-observability-telemetry-D9EYxkdr.mjs.map +1 -0
- package/fesm2022/juice-js-observability-topology.page-CLz7J5pq.mjs +483 -0
- package/fesm2022/juice-js-observability-topology.page-CLz7J5pq.mjs.map +1 -0
- package/fesm2022/juice-js-observability-trace.page-DccvH1SV.mjs +518 -0
- package/fesm2022/juice-js-observability-trace.page-DccvH1SV.mjs.map +1 -0
- package/fesm2022/juice-js-observability-traces.page-BZxnac4Y.mjs +520 -0
- package/fesm2022/juice-js-observability-traces.page-BZxnac4Y.mjs.map +1 -0
- package/fesm2022/juice-js-observability.mjs +396 -0
- package/fesm2022/juice-js-observability.mjs.map +1 -0
- package/package.json +48 -4
- package/styles/_observability.scss +216 -0
- package/types/juice-js-observability.d.ts +387 -0
package/README.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @juice-js/observability
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Juice Console pages for an Angular app built on `@juice-js/layout`: services, traces, dependencies, logs, metrics,
|
|
4
|
+
resources, alerts and rules. The pages read only the Juice Observability API (the `Juice.Observability.Api` NuGet
|
|
5
|
+
package, or the platform image).
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// app module or app config
|
|
11
|
+
providers: [
|
|
12
|
+
provideHttpClient(withInterceptorsFromDi()),
|
|
13
|
+
provideJuiceObservability({ apiBase: '', basePath: '/observability' }),
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
// routes: mount the pages under basePath, behind your own sign-in guard
|
|
17
|
+
{ path: 'observability', children: OBSERVABILITY_ROUTES.map(r => ({ ...r, canActivate: [AuthGuard] })) },
|
|
18
|
+
|
|
19
|
+
// menu (juice-layout)
|
|
20
|
+
MenuModule.forRoot([...yourMenus, ...observabilityMenus('/observability', 100)]),
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```scss
|
|
24
|
+
// styles.scss (the app's global styles, once): the pages' layout, in the Juice Console's teal theme, light and dark (.dark).
|
|
25
|
+
@use '@juice-js/observability';
|
|
26
|
+
|
|
27
|
+
// Optional: colours from your own Angular Material (M2) themes instead.
|
|
28
|
+
:root { @include observability.colors($light-theme); }
|
|
29
|
+
.dark { @include observability.colors($dark-theme); } // under your dark-mode selector
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The default colours sit under zero-specificity `:where()` selectors, so your `colors()` replaces them wherever it
|
|
33
|
+
comes in the file. `colors($theme, $status: (), $series: null)` sets the `--jc-*` custom properties the pages, charts
|
|
34
|
+
and dependency graph read:
|
|
35
|
+
|
|
36
|
+
| From your theme | Used for |
|
|
37
|
+
|---|---|
|
|
38
|
+
| background, card, divider, text, secondary text | page, cards and tables, borders, text, labels |
|
|
39
|
+
| primary | links, selection, the selected graph node or edge |
|
|
40
|
+
| warn | unhealthy and critical, errors |
|
|
41
|
+
|
|
42
|
+
A Material theme has no colour for healthy, degraded or offline, nor a categorical palette, so they have defaults
|
|
43
|
+
(Material green, deep orange and greys; ten Material hues for services in a trace and lines in a chart). Set your own:
|
|
44
|
+
|
|
45
|
+
```scss
|
|
46
|
+
:root {
|
|
47
|
+
@include observability.colors($light-theme,
|
|
48
|
+
$status: (ok: #1b5e20, warn: #e65100, bad: #c62828, crit: #8e0000, off: #546e7a, none: #90a4ae),
|
|
49
|
+
$series: (#1565c0, #6a1b9a, #00838f, #ef6c00));
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The pages use the properties but don't style the page itself; give `body` your colours, for example
|
|
54
|
+
`background: var(--jc-bg); color: var(--jc-text);`. A theme switch at runtime (a class change on `<html>` or `<body>`,
|
|
55
|
+
like juice-layout's dark-mode toggle) repaints the charts and the graph.
|
|
56
|
+
|
|
57
|
+
- `apiBase`: the API origin; empty when the API serves the app.
|
|
58
|
+
- `basePath`: where the routes are mounted; every link between pages goes under it.
|
|
59
|
+
- The router needs `bindToComponentInputs: true`. The pages take their route and query parameters as inputs.
|
|
60
|
+
- The host's bearer-token interceptor (`@juice-js/auth`) must cover `apiBase`. The token carries the `platform_tenant`
|
|
61
|
+
claim and a platform role (`viewer`, `operator` or `admin`).
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { ApiService, ContextService, LinkPipe } from "./juice-js-observability.mjs";
|
|
2
|
+
import { _ as ReasonPipe, a as problemText, d as KeyValueTable, l as ContextBar, m as AgoPipe, 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, signal } from "@angular/core";
|
|
5
|
+
import { RouterLink } from "@angular/router";
|
|
6
|
+
import { DatePipe } from "@angular/common";
|
|
7
|
+
import * as i1 from "@angular/forms";
|
|
8
|
+
import { FormsModule } from "@angular/forms";
|
|
9
|
+
import * as i2 from "@angular/material/button";
|
|
10
|
+
import { MatButtonModule } from "@angular/material/button";
|
|
11
|
+
import { MatFormFieldModule } from "@angular/material/form-field";
|
|
12
|
+
import * as i4 from "@angular/material/input";
|
|
13
|
+
import { MatInputModule } from "@angular/material/input";
|
|
14
|
+
import * as i5 from "@angular/material/progress-bar";
|
|
15
|
+
import { MatProgressBarModule } from "@angular/material/progress-bar";
|
|
16
|
+
import * as i3 from "@angular/material/select";
|
|
17
|
+
var AlertPage = class AlertPage {
|
|
18
|
+
ctx = inject(ContextService);
|
|
19
|
+
api = inject(ApiService);
|
|
20
|
+
id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
|
|
21
|
+
comment = "";
|
|
22
|
+
sending = signal(false, ...ngDevMode ? [{ debugName: "sending" }] : /* istanbul ignore next */ []);
|
|
23
|
+
ackError = signal("", ...ngDevMode ? [{ debugName: "ackError" }] : /* istanbul ignore next */ []);
|
|
24
|
+
detail = apiResource({
|
|
25
|
+
params: () => this.id(),
|
|
26
|
+
stream: ({ params }) => this.api.alert(params)
|
|
27
|
+
});
|
|
28
|
+
error = computed(() => problemText(this.detail.error()), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
29
|
+
investigate(environment, extra = {}) {
|
|
30
|
+
const alert = this.detail.value()?.alert;
|
|
31
|
+
const start = alert?.breachStartedAt ? new Date(alert.breachStartedAt).getTime() - 9e5 : null;
|
|
32
|
+
const end = alert?.resolvedAt ? new Date(alert.resolvedAt).getTime() + 3e5 : Date.now();
|
|
33
|
+
const window = start && end - start <= 864e5 ? {
|
|
34
|
+
from: new Date(start).toISOString(),
|
|
35
|
+
to: new Date(end).toISOString(),
|
|
36
|
+
range: null
|
|
37
|
+
} : {};
|
|
38
|
+
return this.ctx.keep({
|
|
39
|
+
env: environment,
|
|
40
|
+
...window,
|
|
41
|
+
...extra
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
acknowledge() {
|
|
45
|
+
this.sending.set(true);
|
|
46
|
+
this.ackError.set("");
|
|
47
|
+
this.api.acknowledge(this.id(), this.comment).subscribe({
|
|
48
|
+
next: () => {
|
|
49
|
+
this.sending.set(false);
|
|
50
|
+
this.detail.reload();
|
|
51
|
+
},
|
|
52
|
+
error: (e) => {
|
|
53
|
+
this.sending.set(false);
|
|
54
|
+
this.ackError.set(e.status === 403 ? "Acknowledging needs the operator or admin role." : problemText(e));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
59
|
+
minVersion: "12.0.0",
|
|
60
|
+
version: "22.2.4",
|
|
61
|
+
ngImport: i0,
|
|
62
|
+
type: AlertPage,
|
|
63
|
+
deps: [],
|
|
64
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
65
|
+
});
|
|
66
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
67
|
+
minVersion: "17.0.0",
|
|
68
|
+
version: "22.2.4",
|
|
69
|
+
type: AlertPage,
|
|
70
|
+
isStandalone: true,
|
|
71
|
+
selector: "ng-component",
|
|
72
|
+
inputs: { id: {
|
|
73
|
+
classPropertyName: "id",
|
|
74
|
+
publicName: "id",
|
|
75
|
+
isSignal: true,
|
|
76
|
+
isRequired: true,
|
|
77
|
+
transformFunction: null
|
|
78
|
+
} },
|
|
79
|
+
ngImport: i0,
|
|
80
|
+
template: `
|
|
81
|
+
<jc-context-bar>
|
|
82
|
+
<a [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Alerts</a><span class="jc-crumb-sep">›</span>
|
|
83
|
+
<h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>
|
|
84
|
+
</jc-context-bar>
|
|
85
|
+
@if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
86
|
+
@if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
87
|
+
@if (detail.value(); as d) {
|
|
88
|
+
<div class="jc-summary">
|
|
89
|
+
<span class="jc-badge" [class.jc-health-unhealthy]="d.alert.severity === 'Critical'" [class.jc-health-degraded]="d.alert.severity === 'Warning'">{{ d.alert.severity }}</span>
|
|
90
|
+
<strong>{{ d.alert.state }}</strong>
|
|
91
|
+
<span>{{ d.alert.subjectKey }}</span>
|
|
92
|
+
@if (d.alert.noData) { <span class="jc-muted">no data at the last evaluation: state kept</span> }
|
|
93
|
+
@if (d.alert.subjectType === 'service' && d.alert.subjectId) {
|
|
94
|
+
<a mat-flat-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment)">Investigate</a>
|
|
95
|
+
<a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
|
|
96
|
+
}
|
|
97
|
+
@for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
|
|
98
|
+
</div>
|
|
99
|
+
<div class="jc-grid-2">
|
|
100
|
+
<section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
|
|
101
|
+
<section class="jc-card">
|
|
102
|
+
<h2>Subject health</h2>
|
|
103
|
+
@for (h of d.subjectHealth; track h.subjectId + h.environment) {
|
|
104
|
+
<p><jc-health [state]="h.state" [stale]="h.stale" /> {{ h.environment }}</p>
|
|
105
|
+
<ul class="jc-reasons">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>
|
|
106
|
+
} @empty { <p class="jc-muted">No health for this subject.</p> }
|
|
107
|
+
</section>
|
|
108
|
+
</div>
|
|
109
|
+
<section class="jc-card">
|
|
110
|
+
<h2>History</h2>
|
|
111
|
+
<table class="jc-table"><tbody>
|
|
112
|
+
@for (t of d.history; track t.at + t.to) {
|
|
113
|
+
<tr><td>{{ t.at | date: 'MMM d, HH:mm:ss' }}</td><td>{{ t.from ?? '—' }} → <strong>{{ t.to }}</strong></td><td>{{ t.reason | reason }}</td><td>{{ t.value ?? '—' }}</td></tr>
|
|
114
|
+
}
|
|
115
|
+
</tbody></table>
|
|
116
|
+
</section>
|
|
117
|
+
<section class="jc-card">
|
|
118
|
+
<h2>Acknowledgement</h2>
|
|
119
|
+
@if (d.alert.acknowledgedAt) {
|
|
120
|
+
<p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>
|
|
121
|
+
} @else if (d.alert.state !== 'Resolved') {
|
|
122
|
+
<form (ngSubmit)="acknowledge()" class="jc-filter-form">
|
|
123
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Comment</mat-label><input matInput name="comment" [(ngModel)]="comment" maxlength="1000"></mat-form-field>
|
|
124
|
+
<button mat-flat-button type="submit" [disabled]="sending()">Acknowledge</button>
|
|
125
|
+
</form>
|
|
126
|
+
@if (ackError()) { <p class="jc-error">{{ ackError() }}</p> }
|
|
127
|
+
} @else { <p class="jc-muted">Resolved without acknowledgement.</p> }
|
|
128
|
+
</section>
|
|
129
|
+
}`,
|
|
130
|
+
isInline: true,
|
|
131
|
+
dependencies: [
|
|
132
|
+
{
|
|
133
|
+
kind: "directive",
|
|
134
|
+
type: RouterLink,
|
|
135
|
+
selector: "[routerLink]",
|
|
136
|
+
inputs: [
|
|
137
|
+
"target",
|
|
138
|
+
"queryParams",
|
|
139
|
+
"fragment",
|
|
140
|
+
"queryParamsHandling",
|
|
141
|
+
"state",
|
|
142
|
+
"info",
|
|
143
|
+
"relativeTo",
|
|
144
|
+
"preserveFragment",
|
|
145
|
+
"skipLocationChange",
|
|
146
|
+
"replaceUrl",
|
|
147
|
+
"browserUrl",
|
|
148
|
+
"routerLink"
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
kind: "ngmodule",
|
|
153
|
+
type: FormsModule
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
kind: "directive",
|
|
157
|
+
type: i1.ɵNgNoValidate,
|
|
158
|
+
selector: "form:not([ngNoForm]):not([ngNativeValidate])"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
kind: "directive",
|
|
162
|
+
type: i1.DefaultValueAccessor,
|
|
163
|
+
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]"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
kind: "directive",
|
|
167
|
+
type: i1.NgControlStatus,
|
|
168
|
+
selector: "[formControlName],[ngModel],[formControl]"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
kind: "directive",
|
|
172
|
+
type: i1.NgControlStatusGroup,
|
|
173
|
+
selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
kind: "directive",
|
|
177
|
+
type: i1.MaxLengthValidator,
|
|
178
|
+
selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]",
|
|
179
|
+
inputs: ["maxlength"]
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
kind: "directive",
|
|
183
|
+
type: i1.NgModel,
|
|
184
|
+
selector: "[ngModel]:not([formControlName]):not([formControl])",
|
|
185
|
+
inputs: [
|
|
186
|
+
"name",
|
|
187
|
+
"disabled",
|
|
188
|
+
"ngModel",
|
|
189
|
+
"ngModelOptions"
|
|
190
|
+
],
|
|
191
|
+
outputs: ["ngModelChange"],
|
|
192
|
+
exportAs: ["ngModel"]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
kind: "directive",
|
|
196
|
+
type: i1.NgForm,
|
|
197
|
+
selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]",
|
|
198
|
+
inputs: ["ngFormOptions"],
|
|
199
|
+
outputs: ["ngSubmit"],
|
|
200
|
+
exportAs: ["ngForm"]
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
kind: "ngmodule",
|
|
204
|
+
type: MatButtonModule
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
kind: "component",
|
|
208
|
+
type: i2.MatButton,
|
|
209
|
+
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] ",
|
|
210
|
+
inputs: ["matButton"],
|
|
211
|
+
exportAs: ["matButton", "matAnchor"]
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
kind: "ngmodule",
|
|
215
|
+
type: MatFormFieldModule
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
kind: "component",
|
|
219
|
+
type: i3.MatFormField,
|
|
220
|
+
selector: "mat-form-field",
|
|
221
|
+
inputs: [
|
|
222
|
+
"hideRequiredMarker",
|
|
223
|
+
"color",
|
|
224
|
+
"floatLabel",
|
|
225
|
+
"appearance",
|
|
226
|
+
"subscriptSizing",
|
|
227
|
+
"hintLabel"
|
|
228
|
+
],
|
|
229
|
+
exportAs: ["matFormField"]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
kind: "directive",
|
|
233
|
+
type: i3.MatLabel,
|
|
234
|
+
selector: "mat-label"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
kind: "ngmodule",
|
|
238
|
+
type: MatInputModule
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
kind: "directive",
|
|
242
|
+
type: i4.MatInput,
|
|
243
|
+
selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]",
|
|
244
|
+
inputs: [
|
|
245
|
+
"disabled",
|
|
246
|
+
"id",
|
|
247
|
+
"placeholder",
|
|
248
|
+
"name",
|
|
249
|
+
"required",
|
|
250
|
+
"type",
|
|
251
|
+
"errorStateMatcher",
|
|
252
|
+
"aria-describedby",
|
|
253
|
+
"value",
|
|
254
|
+
"readonly",
|
|
255
|
+
"disabledInteractive"
|
|
256
|
+
],
|
|
257
|
+
exportAs: ["matInput"]
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
kind: "ngmodule",
|
|
261
|
+
type: MatProgressBarModule
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
kind: "component",
|
|
265
|
+
type: i5.MatProgressBar,
|
|
266
|
+
selector: "mat-progress-bar",
|
|
267
|
+
inputs: [
|
|
268
|
+
"color",
|
|
269
|
+
"value",
|
|
270
|
+
"bufferValue",
|
|
271
|
+
"mode"
|
|
272
|
+
],
|
|
273
|
+
outputs: ["animationEnd"],
|
|
274
|
+
exportAs: ["matProgressBar"]
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
kind: "component",
|
|
278
|
+
type: ContextBar,
|
|
279
|
+
selector: "jc-context-bar"
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
kind: "component",
|
|
283
|
+
type: HealthBadge,
|
|
284
|
+
selector: "jc-health",
|
|
285
|
+
inputs: ["state", "stale"]
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
kind: "component",
|
|
289
|
+
type: KeyValueTable,
|
|
290
|
+
selector: "jc-kv",
|
|
291
|
+
inputs: ["values", "empty"]
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
kind: "pipe",
|
|
295
|
+
type: LinkPipe,
|
|
296
|
+
name: "jcLink"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
kind: "pipe",
|
|
300
|
+
type: DatePipe,
|
|
301
|
+
name: "date"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
kind: "pipe",
|
|
305
|
+
type: AgoPipe,
|
|
306
|
+
name: "ago"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
kind: "pipe",
|
|
310
|
+
type: ReasonPipe,
|
|
311
|
+
name: "reason"
|
|
312
|
+
}
|
|
313
|
+
],
|
|
314
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
318
|
+
minVersion: "12.0.0",
|
|
319
|
+
version: "22.2.4",
|
|
320
|
+
ngImport: i0,
|
|
321
|
+
type: AlertPage,
|
|
322
|
+
decorators: [{
|
|
323
|
+
type: Component,
|
|
324
|
+
args: [{
|
|
325
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
326
|
+
imports: [
|
|
327
|
+
LinkPipe,
|
|
328
|
+
RouterLink,
|
|
329
|
+
DatePipe,
|
|
330
|
+
FormsModule,
|
|
331
|
+
MatButtonModule,
|
|
332
|
+
MatFormFieldModule,
|
|
333
|
+
MatInputModule,
|
|
334
|
+
MatProgressBarModule,
|
|
335
|
+
ContextBar,
|
|
336
|
+
HealthBadge,
|
|
337
|
+
KeyValueTable,
|
|
338
|
+
AgoPipe,
|
|
339
|
+
ReasonPipe
|
|
340
|
+
],
|
|
341
|
+
template: `
|
|
342
|
+
<jc-context-bar>
|
|
343
|
+
<a [routerLink]="['alerts'] | jcLink" [queryParams]="ctx.keep()" class="jc-crumb">Alerts</a><span class="jc-crumb-sep">›</span>
|
|
344
|
+
<h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>
|
|
345
|
+
</jc-context-bar>
|
|
346
|
+
@if (detail.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
|
|
347
|
+
@if (detail.error()) { <p class="jc-error">{{ error() }}</p> }
|
|
348
|
+
@if (detail.value(); as d) {
|
|
349
|
+
<div class="jc-summary">
|
|
350
|
+
<span class="jc-badge" [class.jc-health-unhealthy]="d.alert.severity === 'Critical'" [class.jc-health-degraded]="d.alert.severity === 'Warning'">{{ d.alert.severity }}</span>
|
|
351
|
+
<strong>{{ d.alert.state }}</strong>
|
|
352
|
+
<span>{{ d.alert.subjectKey }}</span>
|
|
353
|
+
@if (d.alert.noData) { <span class="jc-muted">no data at the last evaluation: state kept</span> }
|
|
354
|
+
@if (d.alert.subjectType === 'service' && d.alert.subjectId) {
|
|
355
|
+
<a mat-flat-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment)">Investigate</a>
|
|
356
|
+
<a mat-stroked-button [routerLink]="['services', d.alert.subjectId] | jcLink" [queryParams]="investigate(d.alert.environment, { tab: 'traces', status: 'error' })">Error traces</a>
|
|
357
|
+
}
|
|
358
|
+
@for (l of d.links; track l.url) { <a mat-button [href]="l.url" target="_blank" rel="noopener">{{ l.title }} ↗</a> }
|
|
359
|
+
</div>
|
|
360
|
+
<div class="jc-grid-2">
|
|
361
|
+
<section class="jc-card"><h2>Evidence</h2><jc-kv [values]="d.evidence" /></section>
|
|
362
|
+
<section class="jc-card">
|
|
363
|
+
<h2>Subject health</h2>
|
|
364
|
+
@for (h of d.subjectHealth; track h.subjectId + h.environment) {
|
|
365
|
+
<p><jc-health [state]="h.state" [stale]="h.stale" /> {{ h.environment }}</p>
|
|
366
|
+
<ul class="jc-reasons">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>
|
|
367
|
+
} @empty { <p class="jc-muted">No health for this subject.</p> }
|
|
368
|
+
</section>
|
|
369
|
+
</div>
|
|
370
|
+
<section class="jc-card">
|
|
371
|
+
<h2>History</h2>
|
|
372
|
+
<table class="jc-table"><tbody>
|
|
373
|
+
@for (t of d.history; track t.at + t.to) {
|
|
374
|
+
<tr><td>{{ t.at | date: 'MMM d, HH:mm:ss' }}</td><td>{{ t.from ?? '—' }} → <strong>{{ t.to }}</strong></td><td>{{ t.reason | reason }}</td><td>{{ t.value ?? '—' }}</td></tr>
|
|
375
|
+
}
|
|
376
|
+
</tbody></table>
|
|
377
|
+
</section>
|
|
378
|
+
<section class="jc-card">
|
|
379
|
+
<h2>Acknowledgement</h2>
|
|
380
|
+
@if (d.alert.acknowledgedAt) {
|
|
381
|
+
<p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>
|
|
382
|
+
} @else if (d.alert.state !== 'Resolved') {
|
|
383
|
+
<form (ngSubmit)="acknowledge()" class="jc-filter-form">
|
|
384
|
+
<mat-form-field subscriptSizing="dynamic" class="jc-field-wide"><mat-label>Comment</mat-label><input matInput name="comment" [(ngModel)]="comment" maxlength="1000"></mat-form-field>
|
|
385
|
+
<button mat-flat-button type="submit" [disabled]="sending()">Acknowledge</button>
|
|
386
|
+
</form>
|
|
387
|
+
@if (ackError()) { <p class="jc-error">{{ ackError() }}</p> }
|
|
388
|
+
} @else { <p class="jc-muted">Resolved without acknowledgement.</p> }
|
|
389
|
+
</section>
|
|
390
|
+
}`
|
|
391
|
+
}]
|
|
392
|
+
}],
|
|
393
|
+
propDecorators: { id: [{
|
|
394
|
+
type: i0.Input,
|
|
395
|
+
args: [{
|
|
396
|
+
isSignal: true,
|
|
397
|
+
alias: "id",
|
|
398
|
+
required: true
|
|
399
|
+
}]
|
|
400
|
+
}] }
|
|
401
|
+
});
|
|
402
|
+
export { AlertPage };
|
|
403
|
+
|
|
404
|
+
//# sourceMappingURL=juice-js-observability-alert.page-BHKAUvR-.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"juice-js-observability-alert.page-BHKAUvR-.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/alert.page.ts"],"sourcesContent":["import { LinkPipe } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { apiResource } from '../shared/resource';\nimport { DatePipe } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { AgoPipe, ReasonPipe } from '../shared/format';\nimport { ContextBar, HealthBadge, KeyValueTable } from '../shared/ui';\nimport { problemText } from '../shared/telemetry';\n\n/**\n * One alert (Console requirements §14): evidence, lifecycle history, the subject's health and reasons, the way into\n * the investigation, and acknowledgement for operators.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, DatePipe, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatProgressBarModule,\n ContextBar, HealthBadge, KeyValueTable, AgoPipe, ReasonPipe],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['alerts'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Alerts</a><span class=\"jc-crumb-sep\">›</span>\n <h1>{{ detail.value()?.alert?.rule ?? '…' }}</h1>\n </jc-context-bar>\n @if (detail.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (detail.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (detail.value(); as d) {\n <div class=\"jc-summary\">\n <span class=\"jc-badge\" [class.jc-health-unhealthy]=\"d.alert.severity === 'Critical'\" [class.jc-health-degraded]=\"d.alert.severity === 'Warning'\">{{ d.alert.severity }}</span>\n <strong>{{ d.alert.state }}</strong>\n <span>{{ d.alert.subjectKey }}</span>\n @if (d.alert.noData) { <span class=\"jc-muted\">no data at the last evaluation: state kept</span> }\n @if (d.alert.subjectType === 'service' && d.alert.subjectId) {\n <a mat-flat-button [routerLink]=\"['services', d.alert.subjectId] | jcLink\" [queryParams]=\"investigate(d.alert.environment)\">Investigate</a>\n <a mat-stroked-button [routerLink]=\"['services', d.alert.subjectId] | jcLink\" [queryParams]=\"investigate(d.alert.environment, { tab: 'traces', status: 'error' })\">Error traces</a>\n }\n @for (l of d.links; track l.url) { <a mat-button [href]=\"l.url\" target=\"_blank\" rel=\"noopener\">{{ l.title }} ↗</a> }\n </div>\n <div class=\"jc-grid-2\">\n <section class=\"jc-card\"><h2>Evidence</h2><jc-kv [values]=\"d.evidence\" /></section>\n <section class=\"jc-card\">\n <h2>Subject health</h2>\n @for (h of d.subjectHealth; track h.subjectId + h.environment) {\n <p><jc-health [state]=\"h.state\" [stale]=\"h.stale\" /> {{ h.environment }}</p>\n <ul class=\"jc-reasons\">@for (r of h.reasons; track r) { <li>{{ r | reason }}</li> }</ul>\n } @empty { <p class=\"jc-muted\">No health for this subject.</p> }\n </section>\n </div>\n <section class=\"jc-card\">\n <h2>History</h2>\n <table class=\"jc-table\"><tbody>\n @for (t of d.history; track t.at + t.to) {\n <tr><td>{{ t.at | date: 'MMM d, HH:mm:ss' }}</td><td>{{ t.from ?? '—' }} → <strong>{{ t.to }}</strong></td><td>{{ t.reason | reason }}</td><td>{{ t.value ?? '—' }}</td></tr>\n }\n </tbody></table>\n </section>\n <section class=\"jc-card\">\n <h2>Acknowledgement</h2>\n @if (d.alert.acknowledgedAt) {\n <p>By <strong>{{ d.alert.acknowledgedBy }}</strong> {{ d.alert.acknowledgedAt | ago }}@if (d.acknowledgeComment) {: “{{ d.acknowledgeComment }}”}</p>\n } @else if (d.alert.state !== 'Resolved') {\n <form (ngSubmit)=\"acknowledge()\" class=\"jc-filter-form\">\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-wide\"><mat-label>Comment</mat-label><input matInput name=\"comment\" [(ngModel)]=\"comment\" maxlength=\"1000\"></mat-form-field>\n <button mat-flat-button type=\"submit\" [disabled]=\"sending()\">Acknowledge</button>\n </form>\n @if (ackError()) { <p class=\"jc-error\">{{ ackError() }}</p> }\n } @else { <p class=\"jc-muted\">Resolved without acknowledgement.</p> }\n </section>\n }`,\n})\nexport class AlertPage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n readonly id = input.required<string>();\n protected comment = '';\n protected readonly sending = signal(false);\n protected readonly ackError = signal('');\n\n protected readonly detail = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.alert(params) });\n protected readonly error = computed(() => problemText(this.detail.error()));\n\n /** The service page over the alert's environment, from shortly before the breach. */\n investigate(environment: string | null, extra: Record<string, string> = {}) {\n const alert = this.detail.value()?.alert;\n const start = alert?.breachStartedAt ? new Date(alert.breachStartedAt).getTime() - 15 * 60_000 : null;\n const end = alert?.resolvedAt ? new Date(alert.resolvedAt).getTime() + 5 * 60_000 : Date.now();\n const window = start && end - start <= 24 * 3_600_000\n ? { from: new Date(start).toISOString(), to: new Date(end).toISOString(), range: null } : {};\n return this.ctx.keep({ env: environment, ...window, ...extra });\n }\n\n acknowledge() {\n this.sending.set(true);\n this.ackError.set('');\n this.api.acknowledge(this.id(), this.comment).subscribe({\n next: () => { this.sending.set(false); this.detail.reload(); },\n error: e => {\n this.sending.set(false);\n this.ackError.set((e as { status?: number }).status === 403 ? 'Acknowledging needs the operator or admin role.' : problemText(e));\n },\n });\n }\n}\n"],"mappings":"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"}
|