@juice-js/observability 1.1.0-alpha.21 → 1.1.0-alpha.22
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/fesm2022/{juice-js-observability-alert.page-nEy5Gc6j.mjs → juice-js-observability-alert.page-DOAX9WWa.mjs} +154 -2
- package/fesm2022/juice-js-observability-alert.page-DOAX9WWa.mjs.map +1 -0
- package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs +400 -0
- package/fesm2022/juice-js-observability-incident.page-CCHEWXou.mjs.map +1 -0
- package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs +438 -0
- package/fesm2022/juice-js-observability-incidents.page-CpNqYp2W.mjs.map +1 -0
- package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs +725 -0
- package/fesm2022/juice-js-observability-notifications.page-BDqb10C6.mjs.map +1 -0
- package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs +580 -0
- package/fesm2022/juice-js-observability-rules.page-Df2uDVwz.mjs.map +1 -0
- package/fesm2022/{juice-js-observability-service.page-BrcID0Bs.mjs → juice-js-observability-service.page-BFXUe4mX.mjs} +3 -1
- package/fesm2022/juice-js-observability-service.page-BFXUe4mX.mjs.map +1 -0
- package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs +523 -0
- package/fesm2022/juice-js-observability-silences.page-Cg53dJAy.mjs.map +1 -0
- package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs +213 -0
- package/fesm2022/juice-js-observability-timeline.page-CtU2EoDN.mjs.map +1 -0
- package/fesm2022/juice-js-observability.mjs +128 -3
- package/fesm2022/juice-js-observability.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_observability.scss +4 -0
- package/types/juice-js-observability.d.ts +197 -0
- package/fesm2022/juice-js-observability-alert.page-nEy5Gc6j.mjs.map +0 -1
- package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs +0 -175
- package/fesm2022/juice-js-observability-rules.page-Crb07uWm.mjs.map +0 -1
- package/fesm2022/juice-js-observability-service.page-BrcID0Bs.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"juice-js-observability-rules.page-Df2uDVwz.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/rules.page.ts"],"sourcesContent":["import { LinkPipe } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatCheckboxModule } from '@angular/material/checkbox';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { MatSelectModule } from '@angular/material/select';\nimport { apiResource } from '../shared/resource';\nimport { RouterLink } from '@angular/router';\nimport { Observable, forkJoin, map } from 'rxjs';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { Rule } from '../core/models';\nimport { AgoPipe } from '../shared/format';\nimport { ContextBar } from '../shared/ui';\nimport { problemText } from '../shared/telemetry';\n\nconst TEMPLATES: Record<string, string> = {\n ErrorRate: 'Ratio of failed requests, 0 to 1. Fires above the threshold.',\n LatencyP95: 'Milliseconds at the 95th percentile. Fires above the threshold.',\n ThroughputAbsent: 'Requests in the window, for a service that had traffic. Fires at or below the threshold.',\n DeliveryFailureRate: 'Ratio of failed deliveries, 0 to 1. Fires above the threshold.',\n HostMemory: 'Ratio of host memory used, 0 to 1. Fires above the threshold.',\n MediatorErrors: 'Mediator errors in the window. Fires above the threshold.',\n ConsumerFailures: 'Failed consumer messages in the window. Fires above the threshold.',\n};\n\n/** A TimeSpan as the API writes it (`1.02:03:04`) in seconds. */\nfunction seconds(value: unknown): number {\n const match = /^(?:(\\d+)\\.)?(\\d+):(\\d+):(\\d+)/.exec(String(value ?? ''));\n return match ? (+(match[1] ?? 0)) * 86400 + +match[2] * 3600 + +match[3] * 60 + +match[4] : 0;\n}\n\nfunction span(total: number): string {\n const s = Math.max(0, Math.round(total || 0));\n const pad = (n: number) => String(n).padStart(2, '0');\n const days = Math.floor(s / 86400);\n return `${days ? `${days}.` : ''}${pad(Math.floor(s / 3600) % 24)}:${pad(Math.floor(s / 60) % 60)}:${pad(s % 60)}`;\n}\n\ninterface RuleForm {\n key: string; name: string; template: string; severity: string; service: string; environment: string; host: string; threshold: number | null; recoveryThreshold: number | null;\n minimumSamples: number; scheduleSeconds: number; windowSeconds: number; forSeconds: number; recoverySeconds: number; cooldownSeconds: number; enabled: boolean;\n}\n\nconst NEW_RULE: RuleForm = {\n key: '', name: '', template: 'ErrorRate', severity: 'Warning', service: '', environment: '', host: '', threshold: null, recoveryThreshold: null,\n minimumSamples: 10, scheduleSeconds: 60, windowSeconds: 300, forSeconds: 120, recoverySeconds: 120, cooldownSeconds: 600, enabled: true,\n};\n\n/**\n * Rules and their current state (Console requirements §14), and changing them for admins. They stay typed templates (D5): this page never takes a query.\n * A rule in the deployment's file is shown but changed there. Disabling one, or changing what it watches, closes its open alerts, which the form says first.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, FormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, MatProgressBarModule, MatSelectModule, ContextBar, AgoPipe],\n template: `\n <jc-context-bar>\n <h1>Rules</h1>\n <span class=\"jc-spacer\"></span>\n <button mat-flat-button type=\"button\" data-testid=\"rule-new\" (click)=\"startNew()\">New rule</button>\n </jc-context-bar>\n @if (data.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (data.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (form(); as f) {\n <section class=\"jc-card\" data-testid=\"rule-form\">\n <h2>{{ editing() ? 'Change ' + editing()!.key : 'New rule' }}</h2>\n <form class=\"jc-form\" (ngSubmit)=\"save(f)\">\n @if (!editing()) { <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Key</mat-label><input matInput name=\"key\" [(ngModel)]=\"f.key\" required></mat-form-field> }\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-wide\"><mat-label>Name</mat-label><input matInput name=\"name\" [(ngModel)]=\"f.name\" required></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Template</mat-label>\n <mat-select name=\"template\" [(ngModel)]=\"f.template\">@for (t of templates; track t) { <mat-option [value]=\"t\">{{ t }}</mat-option> }</mat-select>\n </mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Severity</mat-label>\n <mat-select name=\"severity\" [(ngModel)]=\"f.severity\"><mat-option value=\"Info\">Info</mat-option><mat-option value=\"Warning\">Warning</mat-option><mat-option value=\"Critical\">Critical</mat-option></mat-select>\n </mat-form-field>\n @if (f.template === 'HostMemory') {\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Host (all if empty)</mat-label><input matInput name=\"host\" [(ngModel)]=\"f.host\"></mat-form-field>\n } @else {\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Service (all if empty)</mat-label><input matInput name=\"service\" [(ngModel)]=\"f.service\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Environment (all if empty)</mat-label><input matInput name=\"environment\" [(ngModel)]=\"f.environment\"></mat-form-field>\n }\n <p class=\"jc-muted jc-small jc-form-actions\">{{ hint(f.template) }}</p>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Threshold</mat-label><input matInput type=\"number\" step=\"any\" name=\"threshold\" [(ngModel)]=\"f.threshold\" required></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Recovery threshold</mat-label><input matInput type=\"number\" step=\"any\" name=\"recoveryThreshold\" [(ngModel)]=\"f.recoveryThreshold\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Minimum samples</mat-label><input matInput type=\"number\" name=\"minimumSamples\" [(ngModel)]=\"f.minimumSamples\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Every (s)</mat-label><input matInput type=\"number\" name=\"schedule\" [(ngModel)]=\"f.scheduleSeconds\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Window (s)</mat-label><input matInput type=\"number\" name=\"window\" [(ngModel)]=\"f.windowSeconds\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Fire after (s)</mat-label><input matInput type=\"number\" name=\"for\" [(ngModel)]=\"f.forSeconds\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Recover after (s)</mat-label><input matInput type=\"number\" name=\"recovery\" [(ngModel)]=\"f.recoverySeconds\"></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\" class=\"jc-field-sm\"><mat-label>Cooldown (s)</mat-label><input matInput type=\"number\" name=\"cooldown\" [(ngModel)]=\"f.cooldownSeconds\"></mat-form-field>\n <mat-checkbox name=\"enabled\" [(ngModel)]=\"f.enabled\">Enabled</mat-checkbox>\n @if (editing()) { <p class=\"jc-muted jc-small jc-form-actions\">Disabling this rule, or changing its template or scope, resolves its open alerts: nothing would be watching them any more.</p> }\n <div class=\"jc-form-actions\">\n <button mat-flat-button type=\"submit\" data-testid=\"rule-save\" [disabled]=\"saving()\">Save</button>\n <button mat-button type=\"button\" (click)=\"form.set(null)\">Cancel</button>\n @if (saveError()) { <span class=\"jc-error\" data-testid=\"rule-error\">{{ saveError() }}</span> }\n </div>\n </form>\n </section>\n }\n <table class=\"jc-table\" data-testid=\"rules\">\n <thead><tr><th>Rule</th><th>Template</th><th>Severity</th><th>Threshold</th><th>Recovery</th><th>Wait</th><th>Open alerts</th><th>Last evaluation</th><th></th></tr></thead>\n <tbody>\n @for (r of rows(); track r.key) {\n <tr [class.jc-muted]=\"!r.enabled\">\n <td><strong>{{ r.name }}</strong><div class=\"jc-muted jc-small\">{{ r.key }}@if (!r.enabled) { · disabled }@if (r.managedBy === 'config') { · from the rules file }</div></td>\n <td>{{ r.template }}</td>\n <td>{{ r.severity }}</td>\n <td>{{ r.definition['threshold'] ?? '—' }}</td>\n <td>{{ r.definition['recoveryThreshold'] ?? '—' }}</td>\n <td>{{ r.definition['forDuration'] ?? '—' }}</td>\n <td>\n @for (a of r.open; track a.id) {\n <div><a [routerLink]=\"['alerts', a.id] | jcLink\" [queryParams]=\"ctx.keep()\">{{ a.state }}</a> {{ a.subjectKey }} = {{ a.lastValue ?? 'no data' }}</div>\n } @empty { <span class=\"jc-muted\">none</span> }\n </td>\n <td>{{ r.lastEvaluatedAt | ago }}</td>\n <td class=\"jc-row-actions\">@if (r.managedBy !== 'config') { <button mat-button type=\"button\" [attr.data-testid]=\"'rule-edit-' + r.key\" (click)=\"edit(r)\">Change</button> }</td>\n </tr>\n }\n </tbody>\n </table>`,\n})\nexport class RulesPage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n protected readonly templates = Object.keys(TEMPLATES);\n protected readonly form = signal<RuleForm | null>(null);\n protected readonly editing = signal<Rule | null>(null);\n protected readonly saving = signal(false);\n protected readonly saveError = signal('');\n protected readonly data = apiResource({\n stream: () => forkJoin({\n rules: this.api.rules().pipe(map(p => p.items)),\n firing: this.api.allAlerts({ state: 'firing' }).pipe(map(p => p.items)),\n pending: this.api.allAlerts({ state: 'pending' }).pipe(map(p => p.items)),\n }),\n });\n protected readonly error = computed(() => problemText(this.data.error()));\n protected readonly rows = computed(() => {\n const d = this.data.value();\n if (!d) return [];\n const open = [...d.firing, ...d.pending];\n return d.rules.map(r => {\n const mine = open.filter(a => a.rule === r.key);\n const last = mine.map(a => a.lastEvaluatedAt).sort().at(-1) ?? null;\n return { ...r, open: mine, lastEvaluatedAt: last };\n });\n });\n\n protected hint(template: string): string { return TEMPLATES[template] ?? ''; }\n\n protected startNew() { this.editing.set(null); this.saveError.set(''); this.form.set({ ...NEW_RULE }); }\n\n protected edit(rule: Rule) {\n const d = rule.definition;\n this.editing.set(rule);\n this.saveError.set('');\n this.form.set({\n key: rule.key, name: rule.name, template: rule.template, severity: rule.severity, service: String(d['service'] ?? ''), environment: String(d['environment'] ?? ''), host: String(d['host'] ?? ''),\n threshold: d['threshold'] as number, recoveryThreshold: (d['recoveryThreshold'] as number | null) ?? null, minimumSamples: d['minimumSamples'] as number, scheduleSeconds: seconds(d['schedule']), windowSeconds: seconds(d['window']),\n forSeconds: seconds(d['forDuration']), recoverySeconds: seconds(d['recoveryDuration']), cooldownSeconds: seconds(d['cooldown']), enabled: rule.enabled,\n });\n }\n\n protected save(f: RuleForm) {\n const host = f.template === 'HostMemory';\n const body = {\n key: f.key, name: f.name, template: f.template, severity: f.severity, service: host ? '' : f.service.trim(), environment: host ? '' : f.environment.trim().toLowerCase(), host: host ? f.host.trim() : '',\n threshold: f.threshold, recoveryThreshold: f.recoveryThreshold, minimumSamples: f.minimumSamples, schedule: span(f.scheduleSeconds), window: span(f.windowSeconds), forDuration: span(f.forSeconds),\n recoveryDuration: span(f.recoverySeconds), cooldown: span(f.cooldownSeconds), enabled: f.enabled,\n };\n const editing = this.editing();\n const request: Observable<Rule> = editing ? this.api.updateRule(editing.key, body, editing.etag) : this.api.createRule(body);\n this.saving.set(true);\n this.saveError.set('');\n request.subscribe({\n next: () => { this.saving.set(false); this.form.set(null); this.data.reload(); },\n error: e => { this.saving.set(false); this.saveError.set(problemText(e)); },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -732,6 +732,7 @@ var ServicePage = class ServicePage {
|
|
|
732
732
|
} @empty { <p class="jc-muted">No health evaluated yet: no data.</p> }
|
|
733
733
|
</div>
|
|
734
734
|
|
|
735
|
+
<p><a mat-stroked-button [routerLink]="['timeline'] | jcLink" [queryParams]="ctx.keep({ service: id() })" data-testid="service-timeline">Timeline</a></p>
|
|
735
736
|
<mat-tab-group [selectedIndex]="tabIndex()" (selectedIndexChange)="openTab($event)" animationDuration="0ms" mat-stretch-tabs="false">
|
|
736
737
|
<mat-tab label="Overview">
|
|
737
738
|
@if (tabIndex() === 0) {
|
|
@@ -1077,6 +1078,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1077
1078
|
} @empty { <p class="jc-muted">No health evaluated yet: no data.</p> }
|
|
1078
1079
|
</div>
|
|
1079
1080
|
|
|
1081
|
+
<p><a mat-stroked-button [routerLink]="['timeline'] | jcLink" [queryParams]="ctx.keep({ service: id() })" data-testid="service-timeline">Timeline</a></p>
|
|
1080
1082
|
<mat-tab-group [selectedIndex]="tabIndex()" (selectedIndexChange)="openTab($event)" animationDuration="0ms" mat-stretch-tabs="false">
|
|
1081
1083
|
<mat-tab label="Overview">
|
|
1082
1084
|
@if (tabIndex() === 0) {
|
|
@@ -1252,4 +1254,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1252
1254
|
});
|
|
1253
1255
|
export { ServicePage };
|
|
1254
1256
|
|
|
1255
|
-
//# sourceMappingURL=juice-js-observability-service.page-
|
|
1257
|
+
//# sourceMappingURL=juice-js-observability-service.page-BFXUe4mX.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"juice-js-observability-service.page-BFXUe4mX.mjs","names":["i1","i3","i2"],"sources":["../../../../projects/juice-js/observability/src/lib/shared/capacity.ts","../../../../projects/juice-js/observability/src/lib/pages/service.page.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, input, linkedSignal, signal } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\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 { LinkPipe } from '../core/options';\nimport { Capacity } from '../core/models';\nimport { apiResource } from './resource';\nimport { problemText } from './telemetry';\n\nexport interface CapacityInputs {\n users: number;\n /** Seconds between one user's actions. */\n thinkSeconds: number;\n /** Requests or messages one user action causes in this service. */\n perAction: number;\n /** The CPU use to stay under, from 0 to 1. */\n targetCpu: number;\n coresPerInstance: number;\n}\n\nexport interface CapacityPlan {\n perSecond: number;\n instances: number;\n cores: number;\n /** Per instance; null without memory data. */\n memoryBytes: number | null;\n /** The load each instance would carry. */\n perInstance: number;\n /** That load over the highest measured: above 1 is beyond what was measured. */\n beyondMeasured: number | null;\n /** What set the number of instances: CPU, or a consumer that was already busy all the time. */\n limitedBy: 'cpu' | 'messages';\n}\n\n/**\n * The calculator: `null` when the inputs or the measurements can't give an answer, such as a target that idle CPU alone\n * already exceeds. Each instance can carry (target × cores − idle) ÷ CPU per unit, unless a consumer was seen busy all the\n * time: more CPU doesn't help one that handles a message at a time, so an instance carries only the rate it reached.\n */\nexport function planCapacity(m: Capacity, i: CapacityInputs): CapacityPlan | null {\n if (!m.cpuSecondsPerUnit || m.cpuSecondsPerUnit <= 0) return null;\n if (!(i.users > 0 && i.perAction > 0 && i.targetCpu > 0 && i.targetCpu <= 1 && i.coresPerInstance > 0 && i.thinkSeconds >= 0)) return null;\n const response = (m.responseMs ?? 0) / 1000;\n if (i.thinkSeconds + response <= 0) return null;\n\n const byCpu = (i.targetCpu * i.coresPerInstance - (m.idleCores ?? 0)) / m.cpuSecondsPerUnit;\n if (byCpu <= 0) return null;\n const limitedBy = consumerBusy(m) && m.peakMessagesPerSecond < byCpu ? 'messages' : 'cpu';\n const carried = limitedBy === 'messages' ? m.peakMessagesPerSecond : byCpu;\n const perSecond = i.users * i.perAction / (i.thinkSeconds + response);\n const instances = Math.max(1, Math.ceil(perSecond / carried));\n const perInstance = perSecond / instances;\n const memoryBytes = m.memoryModel === 'fitted'\n ? m.idleMemoryBytes! + m.memoryBytesPerInFlight! * perInstance * response\n : m.peakMemoryBytes;\n // A consumer held back by message handling never used its cores: count what it used, at the target, not the machine's.\n const coresEach = limitedBy === 'messages' && m.peakCores !== null\n ? Math.min(i.coresPerInstance, Math.ceil(m.peakCores / i.targetCpu))\n : i.coresPerInstance;\n return {\n perSecond, instances, cores: instances * coresEach, memoryBytes, perInstance,\n beyondMeasured: m.peakPerSecond > 0 ? perInstance / m.peakPerSecond : null,\n limitedBy,\n };\n}\n\n/** A consumer counts as busy all the time from this much message processing per second of time. */\nconst BUSY_CONSUMER = 0.9;\nfunction consumerBusy(m: Capacity): boolean { return m.peakMessagesInFlight >= BUSY_CONSUMER && m.peakMessagesPerSecond > 0; }\n\n/** A thread pool queue of a few items comes and goes on a healthy host; from this many, work was really waiting. */\nconst NOTABLE_QUEUE = 10;\n\n/** Response time counts as rising with load from this ratio between the busiest and quietest samples. */\nconst RISING_RESPONSE = 1.5;\n\nfunction bytes(value: number | null): string {\n if (value === null) return '—';\n return value >= 1024 ** 3 ? `${(value / 1024 ** 3).toFixed(1)} GB` : `${Math.round(value / 1024 ** 2)} MB`;\n}\nfunction rate(value: number): string { return value >= 10 ? value.toFixed(0) : value.toFixed(2); }\n\n/**\n * A service's Capacity tab: what it was measured to need in the selected window, and a calculator from users to instances,\n * cores and memory. The estimate says where it comes from and where it stops holding: scaling isn't linear, and the\n * service's own databases and brokers are not in it.\n */\n@Component({\n selector: 'jc-service-capacity',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [FormsModule, MatFormFieldModule, MatInputModule, MatProgressBarModule, RouterLink, LinkPipe],\n template: `\n @if (capacity.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (capacity.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n @if (mixedEnvironments()) {\n <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>\n }\n @if (capacity.value(); as m) {\n @if (m.samples === 0) {\n <p class=\"jc-muted\">No telemetry from this service in this window.</p>\n } @else {\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-estimate\">\n <h3 id=\"jc-capacity-estimate\">Estimate</h3>\n <div class=\"jc-capacity-inputs\">\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Concurrent users</mat-label>\n <input matInput type=\"number\" min=\"1\" name=\"users\" [ngModel]=\"users()\" (ngModelChange)=\"users.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Seconds between actions</mat-label>\n <input matInput type=\"number\" min=\"0\" name=\"thinkSeconds\" [ngModel]=\"thinkSeconds()\" (ngModelChange)=\"thinkSeconds.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Requests per action</mat-label>\n <input matInput type=\"number\" min=\"0\" name=\"perAction\" [ngModel]=\"perAction()\" (ngModelChange)=\"perAction.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Target CPU use (%)</mat-label>\n <input matInput type=\"number\" min=\"1\" max=\"100\" name=\"targetCpu\" [ngModel]=\"targetCpu()\" (ngModelChange)=\"targetCpu.set($event)\" /></mat-form-field>\n <mat-form-field subscriptSizing=\"dynamic\"><mat-label>Cores per instance</mat-label>\n <input matInput type=\"number\" min=\"1\" name=\"coresPerInstance\" [ngModel]=\"coresPerInstance()\" (ngModelChange)=\"coresPerInstance.set($event)\" /></mat-form-field>\n </div>\n @if (plan(); as p) {\n <div class=\"jc-capacity-result\" data-testid=\"capacity-plan\">\n <div><strong>{{ p.instances }}</strong><span class=\"jc-muted\">instance{{ p.instances === 1 ? '' : 's' }}</span></div>\n <div><strong>{{ p.cores }}</strong><span class=\"jc-muted\">cores in total</span></div>\n <div><strong>{{ memory(p.memoryBytes) }}</strong><span class=\"jc-muted\">memory per instance{{ m.memoryModel === 'peak' ? ' (highest seen)' : '' }}</span></div>\n <div><strong>{{ perSecond(p.perSecond) }}</strong><span class=\"jc-muted\">requests and messages/s</span></div>\n </div>\n @if (p.beyondMeasured !== null && p.beyondMeasured > 1) {\n <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>\n }\n } @else if (m.cpuModel === 'none') {\n <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>\n } @else {\n <p class=\"jc-notice\">No estimate for these values. Check them: idle CPU alone may already exceed the target.</p>\n }\n <ul class=\"jc-capacity-notes\">\n @if (responseRising(); as r) {\n <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>\n }\n @if (busyConsumer()) {\n <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>\n }\n @if (m.outbox?.fallingBehind) {\n <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>\n }\n @if (m.peakQueue !== null && m.peakQueue >= notableQueue) {\n <li class=\"jc-bad\">The thread pool queue reached {{ m.peakQueue }}: work was waiting for a thread.</li>\n }\n @if (m.cpuModel === 'average') {\n <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>\n }\n <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>\n <li>Covers this service only, not its databases, brokers or the services it calls.</li>\n </ul>\n </section>\n\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-measured\">\n <h3 id=\"jc-capacity-measured\">Measured in this window</h3>\n <table class=\"jc-table jc-capacity-measured\">\n <tbody>\n <tr><th>CPU per request or message</th>\n <td>\n @if (m.cpuSecondsPerUnit !== null) { {{ (m.cpuSecondsPerUnit * 1000).toFixed(1) }} ms }\n @else { <span class=\"jc-muted\">No CPU data yet</span> }\n @if (m.cpuModel === 'average') {\n <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>\n }\n </td></tr>\n <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>\n <tr><th>Idle memory</th><td>@if (m.idleMemoryBytes !== null) { {{ memory(m.idleMemoryBytes) }} } @else { <span class=\"jc-muted\">Not separated</span> }</td></tr>\n <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>\n <tr><th>Highest load, one instance</th><td>{{ perSecond(m.peakPerSecond) }} requests and messages/s</td></tr>\n <tr><th>Highest use, one instance</th><td>\n {{ m.peakCores !== null ? m.peakCores.toFixed(2) + ' cores' : 'CPU unknown' }} · {{ m.peakMemoryBytes !== null ? memory(m.peakMemoryBytes) : 'memory unknown' }}</td></tr>\n <tr><th>Average response</th><td>{{ m.responseMs !== null ? m.responseMs.toFixed(0) + ' ms' : '—' }}</td></tr>\n <tr><th>Measured on</th><td>\n {{ m.instances }} instance{{ m.instances === 1 ? '' : 's' }}@if (m.coresPerInstance) {, {{ m.coresPerInstance }} cores each}@if (m.hosts.length) {, on {{ m.hosts.join(', ') }}}.\n <div class=\"jc-muted jc-small\">Figures from other hardware carry over only roughly.</div></td></tr>\n <tr><th>Based on</th><td>\n {{ m.samples }} samples of {{ m.stepSeconds }} s.\n <div class=\"jc-muted jc-small\">{{ sourceNote() }}</div>\n @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>\n </tbody>\n </table>\n </section>\n\n @if (m.outbox; as o) {\n <section class=\"jc-card\" aria-labelledby=\"jc-capacity-outbox\" data-testid=\"capacity-outbox\">\n <h3 id=\"jc-capacity-outbox\">Outbox delivery</h3>\n <table class=\"jc-table jc-capacity-measured\">\n <tbody>\n <tr><th>Messages created</th><td>{{ perSecond(o.createdPerSecond) }}/s</td></tr>\n <tr><th>Messages delivered</th><td>{{ perSecond(o.deliveredPerSecond) }}/s</td></tr>\n <tr><th>Created but not delivered</th><td [class.jc-bad]=\"o.fallingBehind\">{{ count(o.undelivered) }} in this window\n <div class=\"jc-muted jc-small\">Includes messages of rolled-back transactions, which are never delivered.</div></td></tr>\n <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>\n </tbody>\n </table>\n </section>\n }\n }\n }`,\n})\nexport class ServiceCapacity {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n readonly serviceId = input.required<string>();\n /** The environments the service runs in: figures across several mix their hardware. */\n readonly environments = input<string[]>([]);\n\n protected readonly capacity = apiResource({\n params: () => ({ id: this.serviceId(), env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute() }),\n stream: ({ params }) => this.api.capacity({ serviceId: params.id, environment: params.env, ...this.ctx.window() }),\n });\n protected readonly users = signal(1000);\n protected readonly thinkSeconds = signal(10);\n protected readonly perAction = signal(1);\n protected readonly targetCpu = signal(60);\n protected readonly coresPerInstance = linkedSignal(() => this.capacity.value()?.coresPerInstance ?? 2);\n\n protected readonly error = computed(() => problemText(this.capacity.error()));\n protected readonly mixedEnvironments = computed(() => !this.ctx.environment() && new Set(this.environments()).size > 1);\n protected readonly plan = computed(() => {\n const m = this.capacity.value();\n return m ? planCapacity(m, {\n users: this.users(), thinkSeconds: this.thinkSeconds(), perAction: this.perAction(), targetCpu: this.targetCpu() / 100, coresPerInstance: this.coresPerInstance(),\n }) : null;\n });\n protected readonly busyConsumer = computed(() => { const m = this.capacity.value(); return !!m && consumerBusy(m); });\n protected readonly responseRising = computed(() => {\n const m = this.capacity.value();\n return m?.responseLowLoadMs && m.responseHighLoadMs && m.responseHighLoadMs >= RISING_RESPONSE * m.responseLowLoadMs\n ? { low: Math.round(m.responseLowLoadMs), high: Math.round(m.responseHighLoadMs) } : null;\n });\n protected readonly sourceNote = computed(() => this.capacity.value()?.requestSource === 'metrics'\n ? 'Requests are counted from the http.server.request.duration metric, messages from spans.'\n : '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.');\n\n protected readonly notableQueue = NOTABLE_QUEUE;\n protected readonly memory = bytes;\n protected readonly perSecond = rate;\n protected readonly count = (value: number) => Math.round(value).toLocaleString('en-US');\n}\n","import { LinkPipe, ObservabilityLinks } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';\nimport { apiResource } from '../shared/resource';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { MatTabsModule } from '@angular/material/tabs';\nimport { MatButtonModule } from '@angular/material/button';\nimport { Router, RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { AgoPipe, DurationPipe, PercentPipe, ReasonPipe } from '../shared/format';\nimport { ConfidenceBadge, ContextBar, HealthBadge } from '../shared/ui';\nimport { LogTable, MetricExplorer, ServiceStatsCharts, TraceTable, problemText } from '../shared/telemetry';\nimport { ServiceCapacity } from '../shared/capacity';\n\nconst TABS = ['overview', 'operations', 'dependencies', 'traces', 'logs', 'metrics', 'capacity'] as const;\n\n/**\n * One service across its environments (Console requirements §8): health with reasons, instances, operations,\n * dependencies, traces, logs and metrics, without leaving the service. The tab and the selected operation are in the\n * URL, so the investigation can be shared and the back button returns to it.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, MatTabsModule, MatProgressBarModule, MatButtonModule, ContextBar, HealthBadge, ConfidenceBadge,\n TraceTable, LogTable, MetricExplorer, ServiceStatsCharts, ServiceCapacity, AgoPipe, DurationPipe, PercentPipe, ReasonPipe],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['services'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Services</a>\n <span class=\"jc-crumb-sep\">›</span>\n <h1>{{ service.value()?.key ?? '…' }}</h1>\n @for (l of service.value()?.links ?? []; track l.url) { <a mat-button [href]=\"l.url\" target=\"_blank\" rel=\"noopener\">{{ l.title }} ↗</a> }\n </jc-context-bar>\n @if (service.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (service.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n\n @if (service.value(); as s) {\n <div class=\"jc-health-strip\">\n @for (h of health(); track h.environment) {\n <div class=\"jc-card jc-health-card\" [class.jc-selected]=\"h.environment === ctx.environment()\">\n <div class=\"jc-row\"><strong>{{ h.environment ?? 'no environment' }}</strong><jc-health [state]=\"h.state\" [stale]=\"h.stale\" /></div>\n <ul class=\"jc-reasons\">\n @for (r of h.reasons; track r) {\n <li>\n @if (dependencyOf(r); as dep) {\n {{ r | reason }} — <a [routerLink]=\"['services', dep.id] | jcLink\" [queryParams]=\"ctx.keep()\">open</a>\n } @else { {{ r | reason }} }\n </li>\n }\n </ul>\n <div class=\"jc-muted jc-small\">since {{ h.since | ago }} · evaluated {{ h.evaluatedAt | ago }}</div>\n @if (h.environment && h.environment !== ctx.environment()) {\n <button mat-button (click)=\"ctx.set({ env: h.environment })\">Investigate {{ h.environment }}</button>\n }\n </div>\n } @empty { <p class=\"jc-muted\">No health evaluated yet: no data.</p> }\n </div>\n\n <p><a mat-stroked-button [routerLink]=\"['timeline'] | jcLink\" [queryParams]=\"ctx.keep({ service: id() })\" data-testid=\"service-timeline\">Timeline</a></p>\n <mat-tab-group [selectedIndex]=\"tabIndex()\" (selectedIndexChange)=\"openTab($event)\" animationDuration=\"0ms\" mat-stretch-tabs=\"false\">\n <mat-tab label=\"Overview\">\n @if (tabIndex() === 0) {\n <jc-service-stats [query]=\"{ serviceId: id() }\" />\n <h3>Instances</h3>\n <table class=\"jc-table\">\n <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>\n <tbody>\n @for (i of instances(); track i.id) {\n <tr>\n <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>\n <td>{{ i.environment }}</td>\n <td><jc-health [state]=\"i.health\" /></td>\n <td>{{ i.serviceVersion ?? '—' }}</td>\n <td>{{ i.state }}</td>\n <td>{{ i.lastSeen | ago }}</td>\n <td class=\"jc-nowrap\">\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', instance: i.id })\">traces</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'logs', instance: i.id })\">logs</a>\n @if (i.hostResourceId) { · <a [routerLink]=\"['resources', i.hostResourceId] | jcLink\" [queryParams]=\"ctx.keep()\">host</a> }\n </td>\n </tr>\n } @empty { <tr><td colspan=\"7\" class=\"jc-muted\">No instances.</td></tr> }\n </tbody>\n </table>\n }\n </mat-tab>\n\n <mat-tab label=\"Operations\">\n @if (tabIndex() === 1) {\n @if (operations.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n <table class=\"jc-table\">\n <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>\n <tbody>\n @for (o of operations.value()?.items ?? []; track o.name + o.kind) {\n <tr [class.jc-selected]=\"o.name === operation()\">\n <td><a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'operations', op: o.name })\">{{ o.name }}</a></td>\n <td>{{ o.kind }}</td>\n <td>{{ o.count }}</td>\n <td>{{ o.requestsPerSecond.toFixed(2) }}</td>\n <td [class.jc-bad]=\"o.errorRate >= 0.05\">{{ o.errorRate | percent2 }}</td>\n <td>{{ o.p50Ms | duration }}</td><td>{{ o.p95Ms | duration }}</td><td>{{ o.p99Ms | duration }}</td>\n <td>{{ o.lastSeen | ago }}</td>\n <td class=\"jc-nowrap\">\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', op: o.name, status: 'error' })\">error traces</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', op: o.name, slow: o.p95Ms })\">slow traces</a>\n </td>\n </tr>\n } @empty { <tr><td colspan=\"10\" class=\"jc-muted\">No operations in this window.</td></tr> }\n </tbody>\n </table>\n @if (operation(); as op) {\n <h3>{{ op }}</h3>\n <jc-service-stats [query]=\"{ serviceId: id(), operation: op }\" />\n }\n }\n </mat-tab>\n\n <mat-tab label=\"Dependencies\">\n @if (tabIndex() === 2) {\n <h3>Calls and uses</h3>\n <table class=\"jc-table\">\n <thead><tr><th>Target</th><th>Relation</th><th>Evidence</th><th>Spans seen</th><th>Last seen</th><th></th></tr></thead>\n <tbody>\n @for (d of downstream(); track d.edge.id) {\n <tr>\n <td><a [routerLink]=\"d.link\" [queryParams]=\"ctx.keep()\">{{ d.name }}</a> <span class=\"jc-muted jc-small\">{{ d.type }}</span></td>\n <td>{{ d.edge.relation }}</td>\n <td><jc-confidence [confidence]=\"d.edge.confidence\" /></td>\n <td>{{ d.edge.spanCount }}</td>\n <td>{{ d.edge.lastSeen | ago }}</td>\n <td><a [routerLink]=\"['topology'] | jcLink\" [queryParams]=\"ctx.keep({ edge: d.edge.id })\">evidence</a> ·\n <a [routerLink]=\"[]\" [queryParams]=\"ctx.keep({ tab: 'traces', dependency: d.edge.id })\">traces</a></td>\n </tr>\n } @empty { <tr><td colspan=\"6\" class=\"jc-muted\">No downstream dependencies seen.</td></tr> }\n </tbody>\n </table>\n <h3>Called by</h3>\n <table class=\"jc-table\">\n <tbody>\n @for (d of upstream(); track d.edge.id) {\n <tr><td><a [routerLink]=\"d.link\" [queryParams]=\"ctx.keep()\">{{ d.name }}</a></td><td>{{ d.edge.relation }}</td>\n <td><jc-confidence [confidence]=\"d.edge.confidence\" /></td><td>{{ d.edge.lastSeen | ago }}</td>\n <td><a [routerLink]=\"['topology'] | jcLink\" [queryParams]=\"ctx.keep({ edge: d.edge.id })\">evidence</a></td></tr>\n } @empty { <tr><td class=\"jc-muted\">No callers seen.</td></tr> }\n </tbody>\n </table>\n }\n </mat-tab>\n\n <mat-tab label=\"Traces\">\n @if (tabIndex() === 3) {\n <div class=\"jc-filters\">\n @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> }\n </div>\n <jc-trace-table [query]=\"traceQuery()\" />\n }\n </mat-tab>\n\n <mat-tab label=\"Logs\">\n @if (tabIndex() === 4) { <jc-log-table [query]=\"{ serviceId: id(), instanceId: instanceFilter() }\" /> }\n </mat-tab>\n\n <mat-tab label=\"Metrics\">\n @if (tabIndex() === 5) { <jc-metric-explorer [scope]=\"{ serviceId: id(), instanceId: instanceFilter() }\" /> }\n </mat-tab>\n\n <mat-tab label=\"Capacity\">\n @if (tabIndex() === 6) { <jc-service-capacity [serviceId]=\"id()\" [environments]=\"environments()\" /> }\n </mat-tab>\n </mat-tab-group>\n }`,\n})\nexport class ServicePage {\n protected readonly ctx = inject(ContextService);\n private readonly api = inject(ApiService);\n private readonly router = inject(Router);\n private readonly links = inject(ObservabilityLinks);\n\n readonly id = input.required<string>();\n readonly tab = input<string>();\n readonly op = input<string>();\n readonly status = input<string>();\n readonly slow = input<string>();\n readonly instance = input<string>();\n readonly dependency = input<string>();\n\n protected readonly tabIndex = computed(() => Math.max(0, TABS.indexOf((this.tab() ?? 'overview') as typeof TABS[number])));\n protected readonly operation = computed(() => this.op() ?? null);\n protected readonly instanceFilter = computed(() => this.instance() ?? null);\n\n protected readonly service = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.resource(params) });\n protected readonly topology = apiResource({ params: () => this.ctx.environment(), stream: ({ params }) => this.api.topology(params || null) });\n protected readonly operations = apiResource({\n params: () => ({ id: this.id(), env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute(), on: this.tabIndex() === 1 }),\n stream: ({ params }) => this.api.operations({ serviceId: params.id, environment: params.env, ...this.ctx.window(), limit: 200 }),\n });\n\n protected readonly error = computed(() => problemText(this.service.error()));\n protected readonly health = computed(() => (this.service.value()?.health ?? []).filter(h => h.subjectType === 'service'));\n protected readonly instances = computed(() => {\n const env = this.ctx.environment();\n return (this.service.value()?.instances ?? []).filter(i => !env || i.environment === env);\n });\n\n protected readonly environments = computed(() => (this.service.value()?.instances ?? []).map(i => i.environment));\n\n private readonly nodes = computed(() => new Map((this.topology.value()?.nodes ?? []).map(n => [n.id, n])));\n private edgesWhere(match: 'source' | 'target') {\n const other = match === 'source' ? 'target' : 'source';\n return (this.topology.value()?.edges ?? []).filter(e => e[match] === this.id()).map(edge => {\n const node = this.nodes().get(edge[other]);\n return {\n edge, name: node?.displayName ?? edge[other], type: node?.type ?? '',\n link: this.links.to(node?.type === 'service' ? 'services' : 'resources', edge[other]),\n };\n });\n }\n protected readonly downstream = computed(() => this.edgesWhere('source'));\n protected readonly upstream = computed(() => this.edgesWhere('target'));\n\n /** `dependency_failed:shop/payments` links to that service when it is in the topology. */\n dependencyOf(reason: string) {\n const name = reason.match(/^(?:critical_)?dependency_(?:failed|unknown):(.+)$/)?.[1];\n if (!name) return null;\n const node = [...this.nodes().values()].find(n => n.type === 'service' && (n.displayName === name || name.endsWith(`/${n.displayName}`)));\n return node ? { id: node.id } : null;\n }\n\n protected readonly traceQuery = computed(() => ({\n serviceId: this.id(), operation: this.op(), status: this.status(), minDurationMs: this.slow(),\n instanceId: this.instance(), dependencyId: this.dependency(),\n }));\n /** The trace filters in effect, each removable on its own. */\n protected readonly traceFilters = computed(() => {\n const filters: { label: string; clear: Record<string, null> }[] = [];\n if (this.op()) filters.push({ label: `operation: ${this.op()}`, clear: { op: null } });\n if (this.status()) filters.push({ label: `status: ${this.status()}`, clear: { status: null } });\n if (this.slow()) filters.push({ label: `slower than ${Math.round(Number(this.slow()))} ms`, clear: { slow: null } });\n if (this.instance()) filters.push({ label: 'one instance', clear: { instance: null } });\n if (this.dependency()) filters.push({ label: 'through one dependency', clear: { dependency: null } });\n return filters;\n });\n\n openTab(index: number) {\n void this.router.navigate([], { queryParams: { tab: TABS[index] }, queryParamsHandling: 'merge' });\n }\n}\n"],"mappings":"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"}
|