@juice-js/observability 1.0.1-alpha.17 → 1.1.0-alpha.20
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 +3 -2
- package/fesm2022/juice-js-observability-service.page-rGAvUK_k.mjs +1223 -0
- package/fesm2022/juice-js-observability-service.page-rGAvUK_k.mjs.map +1 -0
- package/fesm2022/{juice-js-observability-topology.page-aMWEXyMB.mjs → juice-js-observability-topology.page-CUayiRVT.mjs} +55 -3
- package/fesm2022/juice-js-observability-topology.page-CUayiRVT.mjs.map +1 -0
- package/fesm2022/{juice-js-observability-trace.page-DUCWnZL5.mjs → juice-js-observability-trace.page-ByxGFLpE.mjs} +9 -7
- package/fesm2022/juice-js-observability-trace.page-ByxGFLpE.mjs.map +1 -0
- package/fesm2022/juice-js-observability.mjs +9 -3
- package/fesm2022/juice-js-observability.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/_observability.scss +20 -3
- package/types/juice-js-observability.d.ts +49 -0
- package/fesm2022/juice-js-observability-service.page-Dt8bN2Ry.mjs +0 -726
- package/fesm2022/juice-js-observability-service.page-Dt8bN2Ry.mjs.map +0 -1
- package/fesm2022/juice-js-observability-topology.page-aMWEXyMB.mjs.map +0 -1
- package/fesm2022/juice-js-observability-trace.page-DUCWnZL5.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"juice-js-observability-service.page-rGAvUK_k.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 { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\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],\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. 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 <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 private 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 <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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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ApiService, ContextService, LinkPipe, ObservabilityLinks } from "./juice-js-observability.mjs";
|
|
2
2
|
import { S as apiResource, _ as PercentPipe, d as HealthBadge, g as DurationPipe, h as AgoPipe, l as ConfidenceBadge, m as themeColor, o as problemText, p as onThemeChange, u as ContextBar } from "./juice-js-observability-telemetry-BftWSOaQ.mjs";
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
|
-
import { ChangeDetectionStrategy, Component, afterNextRender, computed, effect, inject, input, viewChild } from "@angular/core";
|
|
4
|
+
import { ChangeDetectionStrategy, Component, afterNextRender, computed, effect, inject, input, untracked, viewChild } from "@angular/core";
|
|
5
5
|
import { Router, RouterLink } from "@angular/router";
|
|
6
6
|
import * as i1 from "@angular/material/button";
|
|
7
7
|
import { MatButtonModule } from "@angular/material/button";
|
|
@@ -16,6 +16,8 @@ const HEALTH_TOKENS = {
|
|
|
16
16
|
Offline: "off",
|
|
17
17
|
Unknown: "none"
|
|
18
18
|
};
|
|
19
|
+
const EDGE_WARN_ERROR_RATE = .01;
|
|
20
|
+
const EDGE_BAD_ERROR_RATE = .05;
|
|
19
21
|
var TopologyPage = class TopologyPage {
|
|
20
22
|
ctx = inject(ContextService);
|
|
21
23
|
api = inject(ApiService);
|
|
@@ -35,6 +37,17 @@ var TopologyPage = class TopologyPage {
|
|
|
35
37
|
nodes = computed(() => new Map((this.topology.value()?.nodes ?? []).map((n) => [n.id, n])), ...ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []);
|
|
36
38
|
selectedEdge = computed(() => this.topology.value()?.edges.find((e) => e.id === this.edge()) ?? null, ...ngDevMode ? [{ debugName: "selectedEdge" }] : /* istanbul ignore next */ []);
|
|
37
39
|
selectedNode = computed(() => (this.node() ? this.nodes().get(this.node()) : null) ?? null, ...ngDevMode ? [{ debugName: "selectedNode" }] : /* istanbul ignore next */ []);
|
|
40
|
+
edgeStats = apiResource({
|
|
41
|
+
params: () => ({
|
|
42
|
+
env: this.ctx.environment(),
|
|
43
|
+
range: this.ctx.range(),
|
|
44
|
+
abs: this.ctx.absolute()
|
|
45
|
+
}),
|
|
46
|
+
stream: ({ params }) => this.api.dependencyStats({
|
|
47
|
+
environment: params.env,
|
|
48
|
+
...this.ctx.window()
|
|
49
|
+
})
|
|
50
|
+
});
|
|
38
51
|
evidence = apiResource({
|
|
39
52
|
params: () => this.edge() ? {
|
|
40
53
|
edge: this.edge(),
|
|
@@ -76,7 +89,14 @@ var TopologyPage = class TopologyPage {
|
|
|
76
89
|
});
|
|
77
90
|
this.draw(this.topology.value());
|
|
78
91
|
});
|
|
79
|
-
effect(() =>
|
|
92
|
+
effect(() => {
|
|
93
|
+
const topology = this.topology.value();
|
|
94
|
+
untracked(() => this.draw(topology));
|
|
95
|
+
});
|
|
96
|
+
effect(() => {
|
|
97
|
+
this.edgeStats.value();
|
|
98
|
+
this.colourEdges();
|
|
99
|
+
});
|
|
80
100
|
effect(() => {
|
|
81
101
|
const id = this.edge() ?? this.node();
|
|
82
102
|
this.cy?.elements().unselect();
|
|
@@ -106,6 +126,12 @@ var TopologyPage = class TopologyPage {
|
|
|
106
126
|
}).run();
|
|
107
127
|
const id = this.edge() ?? this.node();
|
|
108
128
|
if (id) this.cy.getElementById(id).select();
|
|
129
|
+
this.colourEdges();
|
|
130
|
+
}
|
|
131
|
+
colourEdges() {
|
|
132
|
+
if (!this.cy) return;
|
|
133
|
+
this.cy.edges().data("traffic", null);
|
|
134
|
+
for (const s of this.edgeStats.value()?.items ?? []) this.cy.getElementById(s.dependencyId).data("traffic", s.count === 0 ? "idle" : s.errorRate >= EDGE_BAD_ERROR_RATE ? "bad" : s.errorRate >= EDGE_WARN_ERROR_RATE ? "warn" : "ok");
|
|
109
135
|
}
|
|
110
136
|
color(token) {
|
|
111
137
|
return themeColor(token, this.graph().nativeElement);
|
|
@@ -171,6 +197,26 @@ var TopologyPage = class TopologyPage {
|
|
|
171
197
|
"target-arrow-color": line
|
|
172
198
|
}
|
|
173
199
|
},
|
|
200
|
+
{
|
|
201
|
+
selector: "edge[traffic = \"warn\"]",
|
|
202
|
+
style: {
|
|
203
|
+
"line-color": this.color("warn"),
|
|
204
|
+
"target-arrow-color": this.color("warn"),
|
|
205
|
+
width: 3
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
selector: "edge[traffic = \"bad\"]",
|
|
210
|
+
style: {
|
|
211
|
+
"line-color": this.color("bad"),
|
|
212
|
+
"target-arrow-color": this.color("bad"),
|
|
213
|
+
width: 3
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
selector: "edge[traffic = \"idle\"]",
|
|
218
|
+
style: { opacity: .35 }
|
|
219
|
+
},
|
|
174
220
|
{
|
|
175
221
|
selector: ":selected",
|
|
176
222
|
style: {
|
|
@@ -250,10 +296,13 @@ var TopologyPage = class TopologyPage {
|
|
|
250
296
|
<div #graph class="jc-graph" aria-label="Dependency graph"></div>
|
|
251
297
|
<div class="jc-graph-legend">
|
|
252
298
|
<span><i class="jc-line"></i> observed</span><span><i class="jc-line jc-dashed"></i> inferred</span>
|
|
299
|
+
<span><i class="jc-line jc-line-warn"></i> errors ≥ 1%</span><span><i class="jc-line jc-line-bad"></i> errors ≥ 5%</span>
|
|
300
|
+
<span><i class="jc-line jc-line-idle"></i> no calls in window</span>
|
|
253
301
|
<span>■ service ◆ database ⬣ destination</span>
|
|
254
302
|
<button mat-button (click)="fit()">Fit</button>
|
|
255
303
|
</div>
|
|
256
304
|
@if (topology.value()?.truncated) { <p class="jc-muted">Showing the most recent edges only.</p> }
|
|
305
|
+
@if (edgeStats.value()?.truncated) { <p class="jc-muted">Only the most recent edges are coloured.</p> }
|
|
257
306
|
@if (topology.hasValue() && topology.value()!.nodes.length === 0) { <p class="jc-muted">No dependencies seen{{ ctx.environment() ? ' in ' + ctx.environment() : '' }}.</p> }
|
|
258
307
|
</div>
|
|
259
308
|
|
|
@@ -415,10 +464,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
415
464
|
<div #graph class="jc-graph" aria-label="Dependency graph"></div>
|
|
416
465
|
<div class="jc-graph-legend">
|
|
417
466
|
<span><i class="jc-line"></i> observed</span><span><i class="jc-line jc-dashed"></i> inferred</span>
|
|
467
|
+
<span><i class="jc-line jc-line-warn"></i> errors ≥ 1%</span><span><i class="jc-line jc-line-bad"></i> errors ≥ 5%</span>
|
|
468
|
+
<span><i class="jc-line jc-line-idle"></i> no calls in window</span>
|
|
418
469
|
<span>■ service ◆ database ⬣ destination</span>
|
|
419
470
|
<button mat-button (click)="fit()">Fit</button>
|
|
420
471
|
</div>
|
|
421
472
|
@if (topology.value()?.truncated) { <p class="jc-muted">Showing the most recent edges only.</p> }
|
|
473
|
+
@if (edgeStats.value()?.truncated) { <p class="jc-muted">Only the most recent edges are coloured.</p> }
|
|
422
474
|
@if (topology.hasValue() && topology.value()!.nodes.length === 0) { <p class="jc-muted">No dependencies seen{{ ctx.environment() ? ' in ' + ctx.environment() : '' }}.</p> }
|
|
423
475
|
</div>
|
|
424
476
|
|
|
@@ -483,4 +535,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
483
535
|
});
|
|
484
536
|
export { TopologyPage };
|
|
485
537
|
|
|
486
|
-
//# sourceMappingURL=juice-js-observability-topology.page-
|
|
538
|
+
//# sourceMappingURL=juice-js-observability-topology.page-CUayiRVT.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"juice-js-observability-topology.page-CUayiRVT.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/topology.page.ts"],"sourcesContent":["import { LinkPipe, ObservabilityLinks } from '../core/options';\nimport {\n ChangeDetectionStrategy, Component, ElementRef, OnDestroy, afterNextRender, computed, effect, inject, input, untracked, viewChild,\n} from '@angular/core';\nimport { apiResource } from '../shared/resource';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { Router, RouterLink } from '@angular/router';\nimport type { Core, StylesheetJson } from 'cytoscape';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { Topology } from '../core/models';\nimport { AgoPipe, DurationPipe, PercentPipe } from '../shared/format';\nimport { ConfidenceBadge, ContextBar, HealthBadge } from '../shared/ui';\nimport { problemText } from '../shared/telemetry';\nimport { onThemeChange, themeColor } from '../shared/theme';\n\n/** A service's health → its --jc-* colour from the app's theme (observability.colors). */\nconst HEALTH_TOKENS: Record<string, string> = {\n Healthy: 'ok', Degraded: 'warn', Unhealthy: 'bad', Critical: 'crit', Offline: 'off', Unknown: 'none',\n};\n\n/** The error rates of the health policy's defaults (phase 0 decision D4). */\nconst EDGE_WARN_ERROR_RATE = 0.01;\nconst EDGE_BAD_ERROR_RATE = 0.05;\n\n/**\n * Dependency topology (Console requirements §9): services, databases and destinations; zoom, pan and selection.\n * Observed edges are solid, inferred ones dashed and labelled, so an inference never reads as a confirmed call. A\n * selected edge shows its evidence and representative traces; the selection is in the URL. Edges are coloured by their error\n * rate in the selected window, and fade when they had no calls in it.\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [LinkPipe, RouterLink, MatButtonModule, MatIconModule, MatProgressBarModule, ContextBar, HealthBadge, ConfidenceBadge,\n AgoPipe, DurationPipe, PercentPipe],\n template: `\n <jc-context-bar><h1>Dependencies</h1></jc-context-bar>\n @if (topology.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (topology.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n <div class=\"jc-topology\">\n <div class=\"jc-graph-wrap\">\n <div #graph class=\"jc-graph\" aria-label=\"Dependency graph\"></div>\n <div class=\"jc-graph-legend\">\n <span><i class=\"jc-line\"></i> observed</span><span><i class=\"jc-line jc-dashed\"></i> inferred</span>\n <span><i class=\"jc-line jc-line-warn\"></i> errors ≥ 1%</span><span><i class=\"jc-line jc-line-bad\"></i> errors ≥ 5%</span>\n <span><i class=\"jc-line jc-line-idle\"></i> no calls in window</span>\n <span>■ service ◆ database ⬣ destination</span>\n <button mat-button (click)=\"fit()\">Fit</button>\n </div>\n @if (topology.value()?.truncated) { <p class=\"jc-muted\">Showing the most recent edges only.</p> }\n @if (edgeStats.value()?.truncated) { <p class=\"jc-muted\">Only the most recent edges are coloured.</p> }\n @if (topology.hasValue() && topology.value()!.nodes.length === 0) { <p class=\"jc-muted\">No dependencies seen{{ ctx.environment() ? ' in ' + ctx.environment() : '' }}.</p> }\n </div>\n\n <aside class=\"jc-card jc-side\">\n @if (selectedEdge(); as e) {\n <h2>{{ name(e.source) }} → {{ name(e.target) }}</h2>\n <p>{{ e.relation }} <jc-confidence [confidence]=\"e.confidence\" /></p>\n @if (e.confidence.toLowerCase() !== 'observed') {\n <p class=\"jc-muted jc-small\">Inferred from span attributes (for example db.system.name or messaging.destination.name), not a call\n seen between two instrumented services.</p>\n }\n @if (evidence.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (evidence.value(); as ev) {\n <dl class=\"jc-facts\">\n <dt>Calls in window</dt><dd>{{ ev.count }}</dd>\n <dt>Error rate</dt><dd [class.jc-bad]=\"ev.errorRate >= 0.05\">{{ ev.count ? (ev.errorRate | percent2) : 'no data' }}</dd>\n <dt>p95</dt><dd>{{ ev.p95Ms | duration }}</dd>\n <dt>Last seen</dt><dd>{{ ev.lastSeen ?? e.lastSeen | ago }}</dd>\n </dl>\n <h3>Representative traces</h3>\n @for (t of ev.errorTraces; track t) { <div><a [routerLink]=\"['traces', t] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-mono\">{{ t.slice(0, 16) }}…</a> <span class=\"jc-badge jc-health-unhealthy\">error</span></div> }\n @for (t of ev.slowestTraces; track t) { <div><a [routerLink]=\"['traces', t] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-mono\">{{ t.slice(0, 16) }}…</a> <span class=\"jc-muted jc-small\">slow</span></div> }\n @if (!ev.errorTraces.length && !ev.slowestTraces.length) { <p class=\"jc-muted\">No calls in this window.</p> }\n <a mat-stroked-button [routerLink]=\"['traces'] | jcLink\" [queryParams]=\"ctx.keep({ dependency: e.id })\">All traces through this edge</a>\n }\n <p><a [routerLink]=\"link(e.source)\" [queryParams]=\"ctx.keep()\">Open {{ name(e.source) }}</a> ·\n <a [routerLink]=\"link(e.target)\" [queryParams]=\"ctx.keep()\">Open {{ name(e.target) }}</a></p>\n } @else if (selectedNode(); as n) {\n <h2>{{ n.displayName }}</h2>\n <p>{{ n.type }} <jc-health [state]=\"n.health\" /></p>\n <a mat-stroked-button [routerLink]=\"link(n.id)\" [queryParams]=\"ctx.keep()\">Open</a>\n } @else {\n <p class=\"jc-muted\">Select a node or an edge. Scroll to zoom, drag to pan.</p>\n }\n </aside>\n </div>`,\n})\nexport class TopologyPage implements OnDestroy {\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 edge = input<string>();\n readonly node = input<string>();\n\n private readonly graph = viewChild.required<ElementRef<HTMLDivElement>>('graph');\n private cy?: Core;\n private stopThemeWatch?: () => void;\n private destroyed = false;\n\n protected readonly topology = apiResource({ params: () => this.ctx.environment(), stream: ({ params }) => this.api.topology(params || null) });\n protected readonly error = computed(() => problemText(this.topology.error()));\n private readonly nodes = computed(() => new Map((this.topology.value()?.nodes ?? []).map(n => [n.id, n])));\n protected readonly selectedEdge = computed(() => this.topology.value()?.edges.find(e => e.id === this.edge()) ?? null);\n protected readonly selectedNode = computed(() => (this.node() ? this.nodes().get(this.node()!) : null) ?? null);\n protected readonly edgeStats = apiResource({\n params: () => ({ env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute() }),\n stream: ({ params }) => this.api.dependencyStats({ environment: params.env, ...this.ctx.window() }),\n });\n protected readonly evidence = apiResource({\n // No edge selected: undefined params leave the resource idle.\n params: () => this.edge() ? { edge: this.edge()!, env: this.ctx.environment(), range: this.ctx.range(), abs: this.ctx.absolute() } : undefined,\n stream: ({ params }) => this.api.dependency(params.edge, { environment: params.env, ...this.ctx.window() }),\n });\n\n constructor() {\n afterNextRender(async () => {\n const cytoscape = (await import('cytoscape')).default;\n // Left the page while the graph library loaded: its container is detached (no size, so cytoscape's first layout\n // throws), and a graph made now would outlive ngOnDestroy.\n if (this.destroyed) return;\n this.cy = cytoscape({\n container: this.graph().nativeElement,\n wheelSensitivity: 0.3,\n style: this.graphStyle(),\n });\n // The app switched theme (light/dark): restyle and recolour the nodes.\n this.stopThemeWatch = onThemeChange(() => {\n this.cy?.style(this.graphStyle());\n this.draw(this.topology.value());\n });\n this.cy.on('tap', 'edge', e => this.choose({ edge: e.target.id(), node: null }));\n this.cy.on('tap', 'node', e => this.choose({ node: e.target.id(), edge: null }));\n this.cy.on('tap', e => { if (e.target === this.cy) this.choose({ node: null, edge: null }); });\n this.draw(this.topology.value());\n });\n // Redraw (and so re-layout and re-fit) on new topology only: draw() also reads the selection and the edge figures,\n // which would otherwise redraw the graph on every click and time-range change. The effects below handle those.\n effect(() => { const topology = this.topology.value(); untracked(() => this.draw(topology)); });\n effect(() => { this.edgeStats.value(); this.colourEdges(); });\n effect(() => {\n const id = this.edge() ?? this.node();\n this.cy?.elements().unselect();\n if (id) this.cy?.getElementById(id).select();\n });\n }\n\n private draw(topology: Topology | undefined) {\n if (!this.cy || !topology) return;\n this.cy.elements().remove();\n this.cy.add([\n ...topology.nodes.map(n => ({ data: {\n id: n.id, label: n.displayName, type: n.type,\n color: this.color(n.type === 'service' ? HEALTH_TOKENS[n.health ?? 'Unknown'] ?? 'none' : 'muted') } })),\n ...topology.edges.filter(e => this.nodes().has(e.source) && this.nodes().has(e.target)).map(e => ({ data: {\n id: e.id, source: e.source, target: e.target, confidence: e.confidence.toLowerCase(),\n label: e.confidence.toLowerCase() === 'inferred' ? `${e.relation} (inferred)` : e.relation } })),\n ]);\n this.cy.layout({ name: 'breadthfirst', directed: true, padding: 30, spacingFactor: 1.2 }).run();\n const id = this.edge() ?? this.node();\n if (id) this.cy.getElementById(id).select();\n this.colourEdges();\n }\n\n /** Separate from drawing, so a new time range doesn't move the graph. An edge with no figures keeps the plain look. */\n private colourEdges() {\n if (!this.cy) return;\n this.cy.edges().data('traffic', null);\n for (const s of this.edgeStats.value()?.items ?? []) {\n this.cy.getElementById(s.dependencyId).data('traffic', s.count === 0 ? 'idle'\n : s.errorRate >= EDGE_BAD_ERROR_RATE ? 'bad' : s.errorRate >= EDGE_WARN_ERROR_RATE ? 'warn' : 'ok');\n }\n }\n\n private color(token: string) { return themeColor(token, this.graph().nativeElement); }\n\n /** The graph's look, in the app's theme colours. */\n private graphStyle() {\n const [text, muted, line, surface, selected] = [this.color('text'), this.color('muted'), this.color('border'),\n this.color('surface'), this.color('accent')];\n return [\n { selector: 'node', style: {\n label: 'data(label)', 'font-size': 11, color: text, 'text-valign': 'bottom', 'text-margin-y': 4,\n 'background-color': 'data(color)', width: 26, height: 26, 'border-width': 2, 'border-color': surface } },\n { selector: 'node[type = \"database\"]', style: { shape: 'diamond' } },\n { selector: 'node[type = \"messaging-destination\"]', style: { shape: 'hexagon' } },\n { selector: 'node[type = \"service\"]', style: { shape: 'round-rectangle' } },\n { selector: 'edge', style: {\n width: 2, 'curve-style': 'bezier', 'target-arrow-shape': 'triangle', 'line-color': muted,\n 'target-arrow-color': muted, label: 'data(label)', 'font-size': 9, color: muted, 'text-rotation': 'autorotate',\n 'text-background-color': surface, 'text-background-opacity': 0.8, 'text-background-padding': '1px' } },\n { selector: 'edge[confidence = \"inferred\"]', style: { 'line-style': 'dashed', 'line-color': line, 'target-arrow-color': line } },\n { selector: 'edge[traffic = \"warn\"]', style: { 'line-color': this.color('warn'), 'target-arrow-color': this.color('warn'), width: 3 } },\n { selector: 'edge[traffic = \"bad\"]', style: { 'line-color': this.color('bad'), 'target-arrow-color': this.color('bad'), width: 3 } },\n { selector: 'edge[traffic = \"idle\"]', style: { opacity: 0.35 } },\n { selector: ':selected', style: { 'line-color': selected, 'target-arrow-color': selected, 'border-color': selected, 'border-width': 3 } },\n ] as StylesheetJson;\n }\n\n fit() { this.cy?.fit(undefined, 30); }\n\n private choose(selection: { edge?: string | null; node?: string | null }) {\n void this.router.navigate([], { queryParams: selection, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n name(id: string) { return this.nodes().get(id)?.displayName ?? id; }\n\n link(id: string) { return this.links.to(this.nodes().get(id)?.type === 'service' ? 'services' : 'resources', id); }\n\n ngOnDestroy() {\n this.destroyed = true;\n this.stopThemeWatch?.();\n this.cy?.destroy();\n }\n}\n"],"mappings":"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"}
|
|
@@ -309,15 +309,16 @@ var TracePage = class TracePage {
|
|
|
309
309
|
@if (s.statusMessage) { <p class="jc-error">{{ s.statusMessage }}</p> }
|
|
310
310
|
|
|
311
311
|
<nav class="jc-actions">
|
|
312
|
-
@if (s.serviceId) { <a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep()">Service {{ s.service }}</a> }
|
|
312
|
+
@if (s.serviceId) { <a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep()" [title]="'Service ' + s.service">Service {{ s.service }}</a> }
|
|
313
313
|
@if (s.target; as target) {
|
|
314
|
-
<a mat-stroked-button [routerLink]="(target.kind === 'service' ? ['services', target.id] : ['resources', target.id]) | jcLink" [queryParams]="ctx.keep()"
|
|
314
|
+
<a mat-stroked-button [routerLink]="(target.kind === 'service' ? ['services', target.id] : ['resources', target.id]) | jcLink" [queryParams]="ctx.keep()"
|
|
315
|
+
[title]="(target.kind === 'service' ? 'Calls ' : 'Uses ') + target.name">
|
|
315
316
|
{{ target.kind === 'service' ? 'Calls' : 'Uses' }} {{ target.name }}</a>
|
|
316
317
|
}
|
|
317
318
|
@if (s.serviceId) {
|
|
318
319
|
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="aroundSpan(s, { tab: 'metrics' })">Metrics around</a>
|
|
319
320
|
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="aroundSpan(s, { tab: 'logs' })">Service logs around</a>
|
|
320
|
-
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep({ tab: 'traces', op: s.name })">Other traces of {{ s.name }}</a>
|
|
321
|
+
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep({ tab: 'traces', op: s.name })" [title]="'Other traces of ' + s.name">Other traces of {{ s.name }}</a>
|
|
321
322
|
}
|
|
322
323
|
</nav>
|
|
323
324
|
|
|
@@ -577,15 +578,16 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
577
578
|
@if (s.statusMessage) { <p class="jc-error">{{ s.statusMessage }}</p> }
|
|
578
579
|
|
|
579
580
|
<nav class="jc-actions">
|
|
580
|
-
@if (s.serviceId) { <a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep()">Service {{ s.service }}</a> }
|
|
581
|
+
@if (s.serviceId) { <a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep()" [title]="'Service ' + s.service">Service {{ s.service }}</a> }
|
|
581
582
|
@if (s.target; as target) {
|
|
582
|
-
<a mat-stroked-button [routerLink]="(target.kind === 'service' ? ['services', target.id] : ['resources', target.id]) | jcLink" [queryParams]="ctx.keep()"
|
|
583
|
+
<a mat-stroked-button [routerLink]="(target.kind === 'service' ? ['services', target.id] : ['resources', target.id]) | jcLink" [queryParams]="ctx.keep()"
|
|
584
|
+
[title]="(target.kind === 'service' ? 'Calls ' : 'Uses ') + target.name">
|
|
583
585
|
{{ target.kind === 'service' ? 'Calls' : 'Uses' }} {{ target.name }}</a>
|
|
584
586
|
}
|
|
585
587
|
@if (s.serviceId) {
|
|
586
588
|
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="aroundSpan(s, { tab: 'metrics' })">Metrics around</a>
|
|
587
589
|
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="aroundSpan(s, { tab: 'logs' })">Service logs around</a>
|
|
588
|
-
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep({ tab: 'traces', op: s.name })">Other traces of {{ s.name }}</a>
|
|
590
|
+
<a mat-stroked-button [routerLink]="['services', s.serviceId] | jcLink" [queryParams]="ctx.keep({ tab: 'traces', op: s.name })" [title]="'Other traces of ' + s.name">Other traces of {{ s.name }}</a>
|
|
589
591
|
}
|
|
590
592
|
</nav>
|
|
591
593
|
|
|
@@ -648,4 +650,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
648
650
|
});
|
|
649
651
|
export { TracePage };
|
|
650
652
|
|
|
651
|
-
//# sourceMappingURL=juice-js-observability-trace.page-
|
|
653
|
+
//# sourceMappingURL=juice-js-observability-trace.page-ByxGFLpE.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"juice-js-observability-trace.page-ByxGFLpE.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/pages/trace.page.ts"],"sourcesContent":["import { LinkPipe, ObservabilityLinks } from '../core/options';\nimport { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';\nimport { apiResource } from '../shared/resource';\nimport { DatePipe } from '@angular/common';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatProgressBarModule } from '@angular/material/progress-bar';\nimport { Router, RouterLink } from '@angular/router';\nimport { ApiService } from '../core/api.service';\nimport { ContextService } from '../core/context.service';\nimport { Span } from '../core/models';\nimport { DurationPipe, formatDuration, plainTraceId, preciseTime } from '../shared/format';\nimport { ContextBar, KeyValueTable } from '../shared/ui';\nimport { InstanceMetric, LogTable, problemText } from '../shared/telemetry';\nimport { themeColor } from '../shared/theme';\n\ninterface Row { span: Span; depth: number; children: number; offset: number; selfError: boolean; slowest: boolean; }\n\nconst MaxUsageInstances = 8;\nconst UsageMarginMs = 2 * 60_000;\n\n/**\n * .NET's built-in System.Runtime meter (.NET 9+). Counters arrive as a rate per second. The thread pool's queue length is\n * a level that .NET reports as a counter.\n */\nconst UsageMetrics = [\n { title: 'CPU', name: 'dotnet.process.cpu.time', level: false, format: (v: number) => `${Math.round(v * 100)} % of a core` },\n { title: 'Memory', name: 'dotnet.process.memory.working_set', level: false, format: (v: number) => `${Math.round(v / 1048576)} MiB` },\n { title: 'GC pause', name: 'dotnet.gc.pause.time', level: false, format: (v: number) => `${Math.round(v * 10000) / 10} ms/s` },\n { title: 'Thread pool queue', name: 'dotnet.thread_pool.queue.length', level: true, format: (v: number) => String(Math.round(v * 10) / 10) },\n];\n\n/** A stable colour per service, from a small palette that reads in both themes. */\n\n/**\n * A trace as a waterfall (Console requirements §6): the span hierarchy with timing relative to the trace start, so\n * parallel calls show side by side; errors and the slowest spans marked; subtrees collapsible. The selected span is\n * in the URL (`span`), with its details, its logs, and links to its service, its dependency, and the metrics and logs\n * around it (§7).\n */\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n // Fills the window; the waterfall and the span scroll on their own (styles.scss).\n host: { class: 'jc-fill-page' },\n imports: [LinkPipe, RouterLink, DatePipe, MatButtonModule, MatIconModule, MatProgressBarModule, ContextBar, KeyValueTable, LogTable, DurationPipe, InstanceMetric],\n template: `\n <jc-context-bar>\n <a [routerLink]=\"['traces'] | jcLink\" [queryParams]=\"ctx.keep()\" class=\"jc-crumb\">Traces</a>\n <span class=\"jc-crumb-sep\">›</span>\n <h1 class=\"jc-mono\">{{ id() }}</h1>\n </jc-context-bar>\n @if (trace.isLoading()) { <mat-progress-bar mode=\"indeterminate\" /> }\n @if (trace.error()) { <p class=\"jc-error\">{{ error() }}</p> }\n\n @if (trace.value(); as t) {\n <div class=\"jc-summary\">\n <span><strong>{{ root()?.service }}</strong> · {{ root()?.name }}</span>\n <span>{{ start() | date: 'MMM d, HH:mm:ss.SSS' }}</span>\n <span>{{ total() | duration }}</span>\n <span>{{ t.spans.length }} spans · {{ services().length }} services</span>\n @if (errors() > 0) { <span class=\"jc-badge jc-health-unhealthy\">{{ errors() }} failed</span> }\n <button mat-button (click)=\"showErrorsOnly.set(!showErrorsOnly())\">{{ showErrorsOnly() ? 'Show all spans' : 'Error paths only' }}</button>\n <a mat-button [routerLink]=\"['logs'] | jcLink\" [queryParams]=\"ctx.keep({ trace: id() })\">Logs of this trace</a>\n <button mat-button [attr.aria-expanded]=\"showUsage()\" (click)=\"toggleUsage()\">Resource usage</button>\n @if (auditLink(); as link) {\n @if ('route' in link) { <a mat-button [routerLink]=\"link.route\" data-testid=\"audit-link\">Audit records ↗</a> }\n @else { <a mat-button [href]=\"link.href\" target=\"_blank\" rel=\"noopener\" data-testid=\"audit-link\">Audit records ↗</a> }\n }\n @if (t.truncated) { <span class=\"jc-muted\">Showing the first {{ t.spans.length }} spans.</span> }\n </div>\n <div class=\"jc-legend\">@for (s of services(); track s) { <span><i [style.background]=\"color(s)\"></i>{{ s }}</span> }</div>\n\n @if (showUsage()) {\n <section class=\"jc-card jc-trace-usage\" aria-label=\"Resource usage\">\n @for (i of instances(); track i.id) {\n <h3>{{ i.service }} · {{ i.name }}</h3>\n <div class=\"jc-usage-charts\">\n @for (m of usageMetrics; track m.name) {\n <jc-instance-metric [title]=\"m.title\" [name]=\"m.name\" [format]=\"m.format\" [level]=\"m.level\" [instanceId]=\"i.id\"\n [from]=\"usageWindow().from\" [to]=\"usageWindow().to\" [marks]=\"usageMarks()\" />\n }\n </div>\n } @empty {\n <p class=\"jc-muted\">The spans of this trace name no known instance.</p>\n }\n @if (hiddenInstances() > 0) { <p class=\"jc-muted\">And {{ hiddenInstances() }} more instances.</p> }\n <p class=\"jc-muted jc-small\">Dashed lines mark the trace. No data: the host does not collect the <code>System.Runtime</code> meter.</p>\n </section>\n }\n\n <div class=\"jc-trace\" [class.jc-with-detail]=\"selected()\">\n <div class=\"jc-waterfall\" role=\"tree\">\n <div class=\"jc-wf-head\"><span>Span</span><span class=\"jc-wf-scale\">\n @for (tick of ticks(); track tick.at) { <span [style.left.%]=\"tick.at\">{{ tick.label }}</span> }</span></div>\n @for (r of visible(); track r.span.spanId) {\n <div class=\"jc-wf-row\" role=\"treeitem\" [class.jc-selected]=\"r.span.spanId === selectedId()\"\n [class.jc-wf-error]=\"r.selfError\" (click)=\"select(r.span.spanId)\" tabindex=\"0\" (keydown.enter)=\"select(r.span.spanId)\">\n <span class=\"jc-wf-name\" [style.padding-left.px]=\"r.depth * 14\">\n @if (r.children > 0) {\n <button class=\"jc-wf-toggle\" (click)=\"toggle(r.span.spanId, $event)\" [attr.aria-label]=\"collapsed().has(r.span.spanId) ? 'Expand' : 'Collapse'\">\n {{ collapsed().has(r.span.spanId) ? '▸' : '▾' }}</button>\n } @else { <span class=\"jc-wf-toggle\"></span> }\n <i class=\"jc-dot\" [style.background]=\"color(r.span.service)\"></i>\n <span class=\"jc-wf-service\">{{ r.span.service }}</span> <span>{{ r.span.name }}</span>\n @if (r.selfError) { <mat-icon class=\"jc-wf-icon jc-bad\" title=\"Failed\">error</mat-icon> }\n @if (r.slowest) { <mat-icon class=\"jc-wf-icon\" title=\"Among the slowest spans\">schedule</mat-icon> }\n @if (collapsed().has(r.span.spanId)) { <span class=\"jc-muted jc-small\">+{{ r.children }}</span> }\n </span>\n <span class=\"jc-wf-track\">\n <span class=\"jc-wf-bar\" [style.left.%]=\"r.offset\" [style.width.%]=\"width(r.span)\" [style.background]=\"color(r.span.service)\"></span>\n <span class=\"jc-wf-time\" [style.left.%]=\"r.offset\">{{ r.span.durationMs | duration }}</span>\n </span>\n </div>\n }\n </div>\n\n @if (selected(); as s) {\n <aside class=\"jc-card jc-span-detail\">\n <div class=\"jc-row\"><h2>{{ s.name }}</h2><button mat-icon-button (click)=\"select(null)\" aria-label=\"Close\"><mat-icon>close</mat-icon></button></div>\n <div class=\"jc-span-facts\">\n <span class=\"jc-badge\" [class.jc-health-unhealthy]=\"s.status === 'Error'\" [class.jc-health-healthy]=\"s.status === 'Ok'\">{{ s.status }}</span>\n <span>{{ s.kind }}</span><span>{{ s.durationMs | duration }}</span>\n <span>at +{{ offsetMs(s) | duration }}</span>\n </div>\n @if (s.statusMessage) { <p class=\"jc-error\">{{ s.statusMessage }}</p> }\n\n <nav class=\"jc-actions\">\n @if (s.serviceId) { <a mat-stroked-button [routerLink]=\"['services', s.serviceId] | jcLink\" [queryParams]=\"ctx.keep()\" [title]=\"'Service ' + s.service\">Service {{ s.service }}</a> }\n @if (s.target; as target) {\n <a mat-stroked-button [routerLink]=\"(target.kind === 'service' ? ['services', target.id] : ['resources', target.id]) | jcLink\" [queryParams]=\"ctx.keep()\"\n [title]=\"(target.kind === 'service' ? 'Calls ' : 'Uses ') + target.name\">\n {{ target.kind === 'service' ? 'Calls' : 'Uses' }} {{ target.name }}</a>\n }\n @if (s.serviceId) {\n <a mat-stroked-button [routerLink]=\"['services', s.serviceId] | jcLink\" [queryParams]=\"aroundSpan(s, { tab: 'metrics' })\">Metrics around</a>\n <a mat-stroked-button [routerLink]=\"['services', s.serviceId] | jcLink\" [queryParams]=\"aroundSpan(s, { tab: 'logs' })\">Service logs around</a>\n <a mat-stroked-button [routerLink]=\"['services', s.serviceId] | jcLink\" [queryParams]=\"ctx.keep({ tab: 'traces', op: s.name })\" [title]=\"'Other traces of ' + s.name\">Other traces of {{ s.name }}</a>\n }\n </nav>\n\n <h3>Logs of this span</h3>\n <jc-log-table [query]=\"spanLogQuery()\" [compact]=\"true\" />\n\n <h3>Span</h3>\n <jc-kv [values]=\"{ 'service': s.service, 'environment': s.environment, 'instance': s.instance, 'host': s.host,\n 'trace id': id(), 'span id': s.spanId, 'parent span id': s.parentSpanId, 'started': s.startedAt,\n 'instrumentation scope': s.scope ? s.scope + (s.scopeVersion ? ' ' + s.scopeVersion : '') : null }\" />\n <h3>Attributes</h3>\n <jc-kv [values]=\"s.attributes\" empty=\"No attributes.\" />\n @if (s.events.length) {\n <h3>Events</h3>\n @for (e of s.events; track $index) {\n <div class=\"jc-event\" [class.jc-wf-error]=\"e.name === 'exception'\">\n <strong>{{ e.name }}</strong> <span class=\"jc-muted\">+{{ eventOffset(s, e.at) | duration }}</span>\n <jc-kv [values]=\"e.attributes\" empty=\"\" />\n </div>\n }\n }\n @if (s.links.length) {\n <h3>Links</h3>\n @for (l of s.links; track l.spanId) {\n <a [routerLink]=\"['traces', l.traceId] | jcLink\" [queryParams]=\"ctx.keep({ span: l.spanId })\" class=\"jc-mono\">{{ l.traceId }} / {{ l.spanId }}</a>\n }\n }\n </aside>\n }\n </div>\n }`,\n})\nexport class TracePage {\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 /** A trace id, or a W3C traceparent such as a Juice.Audit record before 10.4 holds; links from either open the trace. */\n readonly traceId = input.required<string>();\n readonly span = input<string>();\n /** 'on' shows the resource usage strip; in the URL so a shared link keeps it. */\n readonly usage = input<string>();\n\n protected readonly id = computed(() => plainTraceId(this.traceId()));\n protected readonly trace = apiResource({ params: () => this.id(), stream: ({ params }) => this.api.trace(params) });\n protected readonly error = computed(() => problemText(this.trace.error()));\n /** The host's audit records of this trace (`auditLink`): the requests it served and the data they changed. */\n protected readonly auditLink = computed(() => this.links.audit(this.id()));\n protected readonly collapsed = signal(new Set<string>());\n protected readonly showErrorsOnly = signal(false);\n protected readonly selectedId = computed(() => this.span() ?? null);\n\n private readonly spans = computed(() => this.trace.value()?.spans ?? []);\n // Sub-millisecond precision: spans of a fast in-process flow start microseconds apart.\n protected readonly start = computed(() => Math.min(...this.spans().map(s => preciseTime(s.startedAt))));\n protected readonly total = computed(() =>\n Math.max(0.001, Math.max(...this.spans().map(s => preciseTime(s.startedAt) + s.durationMs)) - this.start()));\n protected readonly root = computed(() => this.spans().find(s => !s.parentSpanId) ?? this.spans()[0]);\n protected readonly services = computed(() => [...new Set(this.spans().map(s => s.service))]);\n protected readonly errors = computed(() => this.spans().filter(s => s.status === 'Error').length);\n protected readonly selected = computed(() => this.spans().find(s => s.spanId === this.selectedId()) ?? null);\n protected readonly spanLogQuery = computed(() => {\n const s = this.selected();\n return s ? { spanId: s.spanId, ...this.windowAround(s) } : {};\n });\n\n protected readonly showUsage = computed(() => this.usage() === 'on');\n /** The instances that served this trace, in the order they joined it. */\n private readonly allInstances = computed(() => {\n const byId = new Map<string, { id: string; name: string; service: string }>();\n for (const s of this.spans()) {\n if (s.instanceId && !byId.has(s.instanceId)) byId.set(s.instanceId, { id: s.instanceId, name: s.instance ?? s.instanceId, service: s.service });\n }\n return [...byId.values()];\n });\n protected readonly instances = computed(() => this.allInstances().slice(0, MaxUsageInstances));\n protected readonly hiddenInstances = computed(() => this.allInstances().length - this.instances().length);\n /** Metrics are exported every few seconds, so a short trace needs a margin to have any points beside it. */\n protected readonly usageWindow = computed(() => ({\n from: new Date(this.start() - UsageMarginMs).toISOString(),\n to: new Date(this.start() + this.total() + UsageMarginMs).toISOString(),\n }));\n protected readonly usageMarks = computed(() => [new Date(this.start()).toISOString(), new Date(this.start() + this.total()).toISOString()]);\n protected readonly usageMetrics = UsageMetrics;\n\n /** Depth-first order with depth; spans whose parent is missing are roots. Children start-ordered. */\n private readonly rows = computed<Row[]>(() => {\n const spans = this.spans();\n const ids = new Set(spans.map(s => s.spanId));\n const byParent = new Map<string | null, Span[]>();\n for (const s of spans) {\n const parent = s.parentSpanId && ids.has(s.parentSpanId) ? s.parentSpanId : null;\n byParent.set(parent, [...(byParent.get(parent) ?? []), s]);\n }\n const slowest = new Set([...spans].sort((a, b) => b.durationMs - a.durationMs).slice(0, Math.max(1, Math.ceil(spans.length * 0.05))).map(s => s.spanId));\n const rows: Row[] = [];\n const walk = (parent: string | null, depth: number) => {\n for (const s of (byParent.get(parent) ?? []).sort((a, b) => a.startedAt.localeCompare(b.startedAt))) {\n rows.push({\n span: s, depth, children: byParent.get(s.spanId)?.length ?? 0,\n offset: 100 * (preciseTime(s.startedAt) - this.start()) / this.total(),\n selfError: s.status === 'Error', slowest: slowest.has(s.spanId) && spans.length > 3,\n });\n walk(s.spanId, depth + 1);\n }\n };\n walk(null, 0);\n return rows;\n });\n\n /** Hides collapsed subtrees; \"error paths only\" keeps failed spans and their ancestors. */\n protected readonly visible = computed(() => {\n const rows = this.rows();\n const parentOf = new Map(this.spans().map(s => [s.spanId, s.parentSpanId]));\n let keep: Set<string> | null = null;\n if (this.showErrorsOnly()) {\n keep = new Set<string>();\n for (const r of rows.filter(r => r.selfError)) {\n for (let id: string | null | undefined = r.span.spanId; id; id = parentOf.get(id)) keep.add(id);\n }\n }\n const out: Row[] = [];\n let hiddenBelow: number | null = null;\n for (const r of rows) {\n if (hiddenBelow !== null && r.depth > hiddenBelow) continue;\n hiddenBelow = null;\n if (keep && !keep.has(r.span.spanId)) continue;\n out.push(r);\n if (this.collapsed().has(r.span.spanId)) hiddenBelow = r.depth;\n }\n return out;\n });\n\n protected readonly ticks = computed(() => [0, 25, 50, 75].map(at => ({ at, label: formatDuration(this.total() * at / 100) })));\n\n /** The app's categorical colours (observability.colors $series), as CSS variables: a theme switch repaints them. */\n private readonly seriesCount = Math.max(1, Number(themeColor('series-count')) || 1);\n\n color(service: string) {\n return `var(--jc-series-${this.services().indexOf(service) % this.seriesCount + 1})`;\n }\n\n width(span: Span) {\n return Math.max(0.3, 100 * span.durationMs / this.total());\n }\n\n offsetMs(span: Span) {\n return preciseTime(span.startedAt) - this.start();\n }\n\n eventOffset(span: Span, at: string) {\n return preciseTime(at) - preciseTime(span.startedAt);\n }\n\n toggle(spanId: string, event: Event) {\n event.stopPropagation();\n const next = new Set(this.collapsed());\n if (!next.delete(spanId)) next.add(spanId);\n this.collapsed.set(next);\n }\n\n toggleUsage() {\n void this.router.navigate([], { queryParams: { usage: this.showUsage() ? null : 'on' }, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n select(spanId: string | null) {\n void this.router.navigate([], { queryParams: { span: spanId }, queryParamsHandling: 'merge', replaceUrl: true });\n }\n\n private windowAround(span: Span) {\n const start = new Date(span.startedAt).getTime();\n return { from: new Date(start - 60_000).toISOString(), to: new Date(start + span.durationMs + 60_000).toISOString() };\n }\n\n /** The service page over a window around this span, so its metrics and logs line up with it. */\n aroundSpan(span: Span, extra: Record<string, string>) {\n return { ...this.ctx.around(span.startedAt, span.durationMs), ...extra };\n }\n}\n"],"mappings":"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"}
|
|
@@ -185,6 +185,12 @@ var ApiService = class ApiService {
|
|
|
185
185
|
serviceStats(query) {
|
|
186
186
|
return this.get("/telemetry/service-stats", query);
|
|
187
187
|
}
|
|
188
|
+
capacity(query) {
|
|
189
|
+
return this.get("/telemetry/capacity", query);
|
|
190
|
+
}
|
|
191
|
+
dependencyStats(query) {
|
|
192
|
+
return this.get("/telemetry/dependencies", query);
|
|
193
|
+
}
|
|
188
194
|
dependency(id, query) {
|
|
189
195
|
return this.get(`/telemetry/dependencies/${id}`, query);
|
|
190
196
|
}
|
|
@@ -325,10 +331,10 @@ function page(path, title, load) {
|
|
|
325
331
|
const OBSERVABILITY_ROUTES = [
|
|
326
332
|
page("", "Overview", () => import("./juice-js-observability-overview.page-DXZl_fUh.mjs").then((m) => m.OverviewPage)),
|
|
327
333
|
page("services", "Services", () => import("./juice-js-observability-services.page-B1huLe_a.mjs").then((m) => m.ServicesPage)),
|
|
328
|
-
page("services/:id", "Service", () => import("./juice-js-observability-service.page-
|
|
334
|
+
page("services/:id", "Service", () => import("./juice-js-observability-service.page-rGAvUK_k.mjs").then((m) => m.ServicePage)),
|
|
329
335
|
page("traces", "Traces", () => import("./juice-js-observability-traces.page-DXSDdeTH.mjs").then((m) => m.TracesPage)),
|
|
330
|
-
page("traces/:traceId", "Trace", () => import("./juice-js-observability-trace.page-
|
|
331
|
-
page("topology", "Dependencies", () => import("./juice-js-observability-topology.page-
|
|
336
|
+
page("traces/:traceId", "Trace", () => import("./juice-js-observability-trace.page-ByxGFLpE.mjs").then((m) => m.TracePage)),
|
|
337
|
+
page("topology", "Dependencies", () => import("./juice-js-observability-topology.page-CUayiRVT.mjs").then((m) => m.TopologyPage)),
|
|
332
338
|
page("logs", "Logs", () => import("./juice-js-observability-logs.page-i76xbJLH.mjs").then((m) => m.LogsPage)),
|
|
333
339
|
page("metrics", "Metrics", () => import("./juice-js-observability-metrics.page-Cq5S0yGJ.mjs").then((m) => m.MetricsPage)),
|
|
334
340
|
page("resources", "Resources", () => import("./juice-js-observability-resources.page-CukW8vVc.mjs").then((m) => m.ResourcesPage)),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"juice-js-observability.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/core/options.ts","../../../../projects/juice-js/observability/src/lib/core/api.service.ts","../../../../projects/juice-js/observability/src/lib/core/context.service.ts","../../../../projects/juice-js/observability/src/lib/observability.routes.ts"],"sourcesContent":["import { EnvironmentProviders, Injectable, InjectionToken, Pipe, PipeTransform, inject, makeEnvironmentProviders } from '@angular/core';\n\n/** How the host app mounts Juice Observability. */\nexport interface ObservabilityOptions {\n /** The Juice API origin; empty when the API serves the app (same origin). */\n apiBase: string;\n /** Where the host mounts {@link OBSERVABILITY_ROUTES}: `/` for the whole app, or a path such as `/observability`. */\n basePath: string;\n /**\n * A link to the audit records of a trace, shown on the trace page: for the Juice Audit pages mounted at `/audit`,\n * `id => ['/audit/traces', id]`. A string is an external URL. Without it, no link.\n */\n auditLink?: ((traceId: string) => unknown[] | string | null) | null;\n}\n\nexport const OBSERVABILITY_OPTIONS = new InjectionToken<ObservabilityOptions>('OBSERVABILITY_OPTIONS', {\n providedIn: 'root',\n factory: () => ({ apiBase: '', basePath: '/', auditLink: null }),\n});\n\n/** Configures the pages: where the API is, where the host mounted the routes, and where a trace's audit records open. */\nexport function provideJuiceObservability(options: Partial<ObservabilityOptions> = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: OBSERVABILITY_OPTIONS,\n useValue: {\n apiBase: options.apiBase ?? '',\n basePath: options.basePath ?? '/',\n auditLink: options.auditLink ?? null,\n } satisfies ObservabilityOptions,\n },\n ]);\n}\n\n/** `/observability/` → `/observability`; `` and `/` → `/`. */\nexport function normalizeBasePath(basePath: string): string {\n const trimmed = basePath.trim().replace(/^\\/+|\\/+$/g, '');\n return trimmed ? `/${trimmed}` : '/';\n}\n\n/** Router links to the pages, under the host's mount path. */\n@Injectable({ providedIn: 'root' })\nexport class ObservabilityLinks {\n private readonly options = inject(OBSERVABILITY_OPTIONS);\n readonly base = normalizeBasePath(this.options.basePath);\n\n /** `to('services', id)` → `['/observability', 'services', id]`. */\n to(...segments: unknown[]): unknown[] { return [this.base, ...segments]; }\n\n /** The host's link to the audit records of a trace, if it gave one. */\n audit(traceId: string | null | undefined): { route: unknown[] } | { href: string } | null {\n if (!traceId || !this.options.auditLink) return null;\n const link = this.options.auditLink(traceId);\n if (!link) return null;\n return typeof link === 'string' ? { href: link } : { route: link };\n }\n}\n\n/** `['services', id] | jcLink`: the same as {@link ObservabilityLinks.to} in a template. */\n@Pipe({ name: 'jcLink' })\nexport class LinkPipe implements PipeTransform {\n private readonly links = inject(ObservabilityLinks);\n transform(segments: unknown[]): unknown[] { return this.links.to(...segments); }\n}\n","import { HttpClient, HttpParams } from '@angular/common/http';\nimport { Injectable, inject } from '@angular/core';\nimport { EMPTY, Observable, expand, map, reduce, shareReplay, switchMap } from 'rxjs';\nimport { OBSERVABILITY_OPTIONS } from './options';\nimport {\n AlertDetail, Alert, Application, DependencyEvidence, DeploymentEnvironment, Health, LogRecord, MetricInfo, Operation,\n Page, ResourceDetail, ResourceSummary, Rule, SearchHit, Series, ServiceStats, Tenant, TelemetryPage, Topology, Trace,\n TraceSummary,\n} from './models';\n\nexport type Query = Record<string, string | number | boolean | string[] | null | undefined>;\n\n/**\n * The Console's only backend: the Juice API (Console requirements §3). One deployment serves one tenant, the one the\n * signed-in user's token names.\n */\n@Injectable({ providedIn: 'root' })\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly base = `${inject(OBSERVABILITY_OPTIONS).apiBase}/api/v1`;\n\n /** The caller's tenant, fetched once. */\n readonly tenant$: Observable<Tenant> = this.http.get<Page<Tenant>>(`${this.base}/tenants`).pipe(\n map(page => {\n if (page.items.length === 0) throw new Error('No tenant is authorized for this user.');\n return page.items[0];\n }),\n shareReplay(1),\n );\n\n private get<T>(path: string, query: Query = {}): Observable<T> {\n let params = new HttpParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined || value === '') continue;\n for (const item of Array.isArray(value) ? value : [value]) params = params.append(key, String(item));\n }\n return this.tenant$.pipe(switchMap(t => this.http.get<T>(`${this.base}/tenants/${t.id}${path}`, { params })));\n }\n\n /** Every page of a cursor-paged list (bounded: at most `maxPages` requests). */\n private all<T>(path: string, query: Query = {}, maxPages = 25): Observable<Page<T>> {\n let pages = 0;\n return this.get<Page<T>>(path, { ...query, limit: 200 }).pipe(\n expand(page => page.nextCursor && ++pages < maxPages ? this.get<Page<T>>(path, { ...query, limit: 200, cursor: page.nextCursor }) : EMPTY),\n reduce((all, page) => ({ items: [...all.items, ...page.items], nextCursor: page.nextCursor }), { items: [] as T[], nextCursor: null as string | null }),\n );\n }\n\n environments() { return this.get<Page<DeploymentEnvironment>>('/environments'); }\n applications() { return this.get<Page<Application>>('/applications', { limit: 200 }); }\n resources(type?: string) { return this.all<ResourceSummary>('/resources', { type }); }\n resource(id: string) { return this.get<ResourceDetail>(`/resources/${id}`); }\n health(query: Query) { return this.all<Health>('/health', query); }\n topology(environment?: string | null) { return this.get<Topology>('/topology', { environment }); }\n\n rules() { return this.get<Page<Rule>>('/rules', { limit: 200 }); }\n /** One page of alerts, newest first; pass the previous page's `nextCursor` as `cursor` for the next. */\n alerts(query: Query) { return this.get<Page<Alert>>('/alerts', { limit: 100, ...query }); }\n /** Every alert matching the query (bounded, like the other complete lists). */\n allAlerts(query: Query) { return this.all<Alert>('/alerts', query); }\n alert(id: string) { return this.get<AlertDetail>(`/alerts/${id}`); }\n acknowledge(id: string, comment: string): Observable<Alert> {\n return this.tenant$.pipe(switchMap(t => this.http.post<Alert>(`${this.base}/tenants/${t.id}/alerts/${id}/acknowledgement`,\n { comment }, { headers: { 'Idempotency-Key': crypto.randomUUID() } })));\n }\n\n traces(query: Query) { return this.get<TelemetryPage<TraceSummary>>('/telemetry/traces', query); }\n trace(traceId: string) { return this.get<Trace>(`/telemetry/traces/${traceId}`); }\n logs(query: Query) { return this.get<TelemetryPage<LogRecord>>('/telemetry/logs', query); }\n operations(query: Query) { return this.get<TelemetryPage<Operation>>('/telemetry/operations', query); }\n metrics(query: Query) { return this.get<TelemetryPage<MetricInfo>>('/telemetry/metrics', query); }\n series(query: Query) { return this.get<Series>('/telemetry/metrics/series', query); }\n serviceStats(query: Query) { return this.get<ServiceStats>('/telemetry/service-stats', query); }\n dependency(id: string, query: Query) { return this.get<DependencyEvidence>(`/telemetry/dependencies/${id}`, query); }\n search(q: string) { return this.get<TelemetryPage<SearchHit>>('/search', { q }); }\n}\n","import { Injectable, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';\nimport { filter, map, startWith } from 'rxjs';\n\nexport const RANGES: Record<string, number> = {\n '15m': 15 * 60_000, '1h': 3_600_000, '6h': 6 * 3_600_000, '24h': 24 * 3_600_000, '7d': 7 * 24 * 3_600_000,\n};\n\n/** Search ranges are at most 24 hours (the API's bound); series and operations take up to 7 days. */\nexport const MAX_SEARCH_MS = RANGES['24h'];\n\n/**\n * The investigation context that follows the user between pages (Console requirements §17): environment and time\n * range. It lives in the URL (`env`, and `range` or `from`/`to`), so every page is a deep link and the browser's back\n * button restores it.\n */\n@Injectable({ providedIn: 'root' })\nexport class ContextService {\n private readonly router = inject(Router);\n\n // Created on first use, after the first navigation may already have ended: start from the current URL.\n private readonly params = toSignal(this.router.events.pipe(\n filter(e => e instanceof NavigationEnd),\n startWith(null),\n map(() => this.leaf().snapshot.queryParams),\n ), { initialValue: {} as Params });\n\n readonly environment = computed(() => (this.params()['env'] as string | undefined) ?? '');\n readonly range = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? 'custom' : ((p['range'] as string | undefined) ?? '1h');\n });\n readonly absolute = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? { from: p['from'] as string, to: p['to'] as string } : null;\n });\n\n /** The window as ISO strings, evaluated now for a relative range. */\n window(maxMs?: number): { from: string; to: string } {\n const absolute = this.absolute();\n if (absolute) return absolute;\n const to = Date.now();\n const span = Math.min(RANGES[this.range()] ?? RANGES['1h'], maxMs ?? Number.MAX_SAFE_INTEGER);\n return { from: new Date(to - span).toISOString(), to: new Date(to).toISOString() };\n }\n\n /** Query parameters for a link: the context plus the target page's own. */\n keep(extra: Params = {}): Params {\n const p = this.params();\n const context: Params = {};\n for (const key of ['env', 'range', 'from', 'to']) if (p[key]) context[key] = p[key];\n return { ...context, ...extra };\n }\n\n /** Changes the context on the current page, keeping its other parameters. */\n set(change: { env?: string | null; range?: string | null; from?: string | null; to?: string | null }) {\n const queryParams: Params = { ...change };\n if (change.range) Object.assign(queryParams, { from: null, to: null });\n void this.router.navigate([], { relativeTo: this.leaf(), queryParams, queryParamsHandling: 'merge' });\n }\n\n /** A time window centred on a moment, for \"around this span\" links. */\n around(at: string, durationMs: number, paddingMs = 5 * 60_000): Params {\n const start = new Date(at).getTime();\n return this.keep({\n from: new Date(start - paddingMs).toISOString(),\n to: new Date(start + durationMs + paddingMs).toISOString(),\n range: null,\n });\n }\n\n private leaf(): ActivatedRoute {\n let route = this.router.routerState.root;\n while (route.firstChild) route = route.firstChild;\n return route;\n }\n}\n","import { Routes } from '@angular/router';\nimport { Menus } from '@juice-js/core';\nimport { normalizeBasePath } from './core/options';\n\nfunction page(path: string, title: string, load: Routes[number]['loadComponent']): Routes[number] {\n return { path, title: `${title} · Juice Observability`, loadComponent: load };\n}\n\n/**\n * The pages, investigation first (Console requirements §4): services, traces, topology, logs and metrics; alerts and\n * rules after. Detail pages are deep links (§17) and carry the environment and time range in their query parameters.\n * The host mounts them at its `basePath` (see {@link provideJuiceObservability}) and adds its own guards.\n */\nexport const OBSERVABILITY_ROUTES: Routes = [\n page('', 'Overview', () => import('./pages/overview.page').then(m => m.OverviewPage)),\n page('services', 'Services', () => import('./pages/services.page').then(m => m.ServicesPage)),\n page('services/:id', 'Service', () => import('./pages/service.page').then(m => m.ServicePage)),\n page('traces', 'Traces', () => import('./pages/traces.page').then(m => m.TracesPage)),\n page('traces/:traceId', 'Trace', () => import('./pages/trace.page').then(m => m.TracePage)),\n page('topology', 'Dependencies', () => import('./pages/topology.page').then(m => m.TopologyPage)),\n page('logs', 'Logs', () => import('./pages/logs.page').then(m => m.LogsPage)),\n page('metrics', 'Metrics', () => import('./pages/metrics.page').then(m => m.MetricsPage)),\n page('resources', 'Resources', () => import('./pages/resources.page').then(m => m.ResourcesPage)),\n page('resources/:id', 'Resource', () => import('./pages/resource.page').then(m => m.ResourcePage)),\n page('alerts', 'Alerts', () => import('./pages/alerts.page').then(m => m.AlertsPage)),\n page('alerts/:id', 'Alert', () => import('./pages/alert.page').then(m => m.AlertPage)),\n page('rules', 'Rules', () => import('./pages/rules.page').then(m => m.RulesPage)),\n];\n\n/** The side menu entries (juice-layout), investigation pages first, linking under `basePath`. */\nexport function observabilityMenus(basePath = '/', order = 1): Menus {\n const base = normalizeBasePath(basePath);\n const link = (path: string) => (base === '/' ? `/${path}` : path ? `${base}/${path}` : base);\n return [\n { id: 'overview', label: 'Overview', icon: 'dashboard', routerLink: link('') },\n { id: 'services', label: 'Services', icon: 'hub', routerLink: link('services') },\n { id: 'traces', label: 'Traces', icon: 'timeline', routerLink: link('traces') },\n { id: 'topology', label: 'Dependencies', icon: 'account_tree', routerLink: link('topology') },\n { id: 'logs', label: 'Logs', icon: 'notes', routerLink: link('logs') },\n { id: 'metrics', label: 'Metrics', icon: 'show_chart', routerLink: link('metrics') },\n { id: 'resources', label: 'Resources', icon: 'dns', routerLink: link('resources') },\n { id: 'alerts', label: 'Alerts', icon: 'notifications', routerLink: link('alerts') },\n { id: 'rules', label: 'Rules', icon: 'rule', routerLink: link('rules') },\n ].map((menu, index) => ({ ...menu, order: order + index }));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"juice-js-observability.mjs","names":[],"sources":["../../../../projects/juice-js/observability/src/lib/core/options.ts","../../../../projects/juice-js/observability/src/lib/core/api.service.ts","../../../../projects/juice-js/observability/src/lib/core/context.service.ts","../../../../projects/juice-js/observability/src/lib/observability.routes.ts"],"sourcesContent":["import { EnvironmentProviders, Injectable, InjectionToken, Pipe, PipeTransform, inject, makeEnvironmentProviders } from '@angular/core';\n\n/** How the host app mounts Juice Observability. */\nexport interface ObservabilityOptions {\n /** The Juice API origin; empty when the API serves the app (same origin). */\n apiBase: string;\n /** Where the host mounts {@link OBSERVABILITY_ROUTES}: `/` for the whole app, or a path such as `/observability`. */\n basePath: string;\n /**\n * A link to the audit records of a trace, shown on the trace page: for the Juice Audit pages mounted at `/audit`,\n * `id => ['/audit/traces', id]`. A string is an external URL. Without it, no link.\n */\n auditLink?: ((traceId: string) => unknown[] | string | null) | null;\n}\n\nexport const OBSERVABILITY_OPTIONS = new InjectionToken<ObservabilityOptions>('OBSERVABILITY_OPTIONS', {\n providedIn: 'root',\n factory: () => ({ apiBase: '', basePath: '/', auditLink: null }),\n});\n\n/** Configures the pages: where the API is, where the host mounted the routes, and where a trace's audit records open. */\nexport function provideJuiceObservability(options: Partial<ObservabilityOptions> = {}): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: OBSERVABILITY_OPTIONS,\n useValue: {\n apiBase: options.apiBase ?? '',\n basePath: options.basePath ?? '/',\n auditLink: options.auditLink ?? null,\n } satisfies ObservabilityOptions,\n },\n ]);\n}\n\n/** `/observability/` → `/observability`; `` and `/` → `/`. */\nexport function normalizeBasePath(basePath: string): string {\n const trimmed = basePath.trim().replace(/^\\/+|\\/+$/g, '');\n return trimmed ? `/${trimmed}` : '/';\n}\n\n/** Router links to the pages, under the host's mount path. */\n@Injectable({ providedIn: 'root' })\nexport class ObservabilityLinks {\n private readonly options = inject(OBSERVABILITY_OPTIONS);\n readonly base = normalizeBasePath(this.options.basePath);\n\n /** `to('services', id)` → `['/observability', 'services', id]`. */\n to(...segments: unknown[]): unknown[] { return [this.base, ...segments]; }\n\n /** The host's link to the audit records of a trace, if it gave one. */\n audit(traceId: string | null | undefined): { route: unknown[] } | { href: string } | null {\n if (!traceId || !this.options.auditLink) return null;\n const link = this.options.auditLink(traceId);\n if (!link) return null;\n return typeof link === 'string' ? { href: link } : { route: link };\n }\n}\n\n/** `['services', id] | jcLink`: the same as {@link ObservabilityLinks.to} in a template. */\n@Pipe({ name: 'jcLink' })\nexport class LinkPipe implements PipeTransform {\n private readonly links = inject(ObservabilityLinks);\n transform(segments: unknown[]): unknown[] { return this.links.to(...segments); }\n}\n","import { HttpClient, HttpParams } from '@angular/common/http';\nimport { Injectable, inject } from '@angular/core';\nimport { EMPTY, Observable, expand, map, reduce, shareReplay, switchMap } from 'rxjs';\nimport { OBSERVABILITY_OPTIONS } from './options';\nimport {\n AlertDetail, Alert, Application, DependencyEvidence, DependencyStatsPage, DeploymentEnvironment, Health, LogRecord, MetricInfo, Operation,\n Page, ResourceDetail, ResourceSummary, Rule, SearchHit, Series, Capacity, ServiceStats, Tenant, TelemetryPage, Topology, Trace,\n TraceSummary,\n} from './models';\n\nexport type Query = Record<string, string | number | boolean | string[] | null | undefined>;\n\n/**\n * The Console's only backend: the Juice API (Console requirements §3). One deployment serves one tenant, the one the\n * signed-in user's token names.\n */\n@Injectable({ providedIn: 'root' })\nexport class ApiService {\n private readonly http = inject(HttpClient);\n private readonly base = `${inject(OBSERVABILITY_OPTIONS).apiBase}/api/v1`;\n\n /** The caller's tenant, fetched once. */\n readonly tenant$: Observable<Tenant> = this.http.get<Page<Tenant>>(`${this.base}/tenants`).pipe(\n map(page => {\n if (page.items.length === 0) throw new Error('No tenant is authorized for this user.');\n return page.items[0];\n }),\n shareReplay(1),\n );\n\n private get<T>(path: string, query: Query = {}): Observable<T> {\n let params = new HttpParams();\n for (const [key, value] of Object.entries(query)) {\n if (value === null || value === undefined || value === '') continue;\n for (const item of Array.isArray(value) ? value : [value]) params = params.append(key, String(item));\n }\n return this.tenant$.pipe(switchMap(t => this.http.get<T>(`${this.base}/tenants/${t.id}${path}`, { params })));\n }\n\n /** Every page of a cursor-paged list (bounded: at most `maxPages` requests). */\n private all<T>(path: string, query: Query = {}, maxPages = 25): Observable<Page<T>> {\n let pages = 0;\n return this.get<Page<T>>(path, { ...query, limit: 200 }).pipe(\n expand(page => page.nextCursor && ++pages < maxPages ? this.get<Page<T>>(path, { ...query, limit: 200, cursor: page.nextCursor }) : EMPTY),\n reduce((all, page) => ({ items: [...all.items, ...page.items], nextCursor: page.nextCursor }), { items: [] as T[], nextCursor: null as string | null }),\n );\n }\n\n environments() { return this.get<Page<DeploymentEnvironment>>('/environments'); }\n applications() { return this.get<Page<Application>>('/applications', { limit: 200 }); }\n resources(type?: string) { return this.all<ResourceSummary>('/resources', { type }); }\n resource(id: string) { return this.get<ResourceDetail>(`/resources/${id}`); }\n health(query: Query) { return this.all<Health>('/health', query); }\n topology(environment?: string | null) { return this.get<Topology>('/topology', { environment }); }\n\n rules() { return this.get<Page<Rule>>('/rules', { limit: 200 }); }\n /** One page of alerts, newest first; pass the previous page's `nextCursor` as `cursor` for the next. */\n alerts(query: Query) { return this.get<Page<Alert>>('/alerts', { limit: 100, ...query }); }\n /** Every alert matching the query (bounded, like the other complete lists). */\n allAlerts(query: Query) { return this.all<Alert>('/alerts', query); }\n alert(id: string) { return this.get<AlertDetail>(`/alerts/${id}`); }\n acknowledge(id: string, comment: string): Observable<Alert> {\n return this.tenant$.pipe(switchMap(t => this.http.post<Alert>(`${this.base}/tenants/${t.id}/alerts/${id}/acknowledgement`,\n { comment }, { headers: { 'Idempotency-Key': crypto.randomUUID() } })));\n }\n\n traces(query: Query) { return this.get<TelemetryPage<TraceSummary>>('/telemetry/traces', query); }\n trace(traceId: string) { return this.get<Trace>(`/telemetry/traces/${traceId}`); }\n logs(query: Query) { return this.get<TelemetryPage<LogRecord>>('/telemetry/logs', query); }\n operations(query: Query) { return this.get<TelemetryPage<Operation>>('/telemetry/operations', query); }\n metrics(query: Query) { return this.get<TelemetryPage<MetricInfo>>('/telemetry/metrics', query); }\n series(query: Query) { return this.get<Series>('/telemetry/metrics/series', query); }\n serviceStats(query: Query) { return this.get<ServiceStats>('/telemetry/service-stats', query); }\n capacity(query: Query) { return this.get<Capacity>('/telemetry/capacity', query); }\n dependencyStats(query: Query) { return this.get<DependencyStatsPage>('/telemetry/dependencies', query); }\n dependency(id: string, query: Query) { return this.get<DependencyEvidence>(`/telemetry/dependencies/${id}`, query); }\n search(q: string) { return this.get<TelemetryPage<SearchHit>>('/search', { q }); }\n}\n","import { Injectable, computed, inject } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router';\nimport { filter, map, startWith } from 'rxjs';\n\nexport const RANGES: Record<string, number> = {\n '15m': 15 * 60_000, '1h': 3_600_000, '6h': 6 * 3_600_000, '24h': 24 * 3_600_000, '7d': 7 * 24 * 3_600_000,\n};\n\n/** Search ranges are at most 24 hours (the API's bound); series and operations take up to 7 days. */\nexport const MAX_SEARCH_MS = RANGES['24h'];\n\n/**\n * The investigation context that follows the user between pages (Console requirements §17): environment and time\n * range. It lives in the URL (`env`, and `range` or `from`/`to`), so every page is a deep link and the browser's back\n * button restores it.\n */\n@Injectable({ providedIn: 'root' })\nexport class ContextService {\n private readonly router = inject(Router);\n\n // Created on first use, after the first navigation may already have ended: start from the current URL.\n private readonly params = toSignal(this.router.events.pipe(\n filter(e => e instanceof NavigationEnd),\n startWith(null),\n map(() => this.leaf().snapshot.queryParams),\n ), { initialValue: {} as Params });\n\n readonly environment = computed(() => (this.params()['env'] as string | undefined) ?? '');\n readonly range = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? 'custom' : ((p['range'] as string | undefined) ?? '1h');\n });\n readonly absolute = computed(() => {\n const p = this.params();\n return p['from'] && p['to'] ? { from: p['from'] as string, to: p['to'] as string } : null;\n });\n\n /** The window as ISO strings, evaluated now for a relative range. */\n window(maxMs?: number): { from: string; to: string } {\n const absolute = this.absolute();\n if (absolute) return absolute;\n const to = Date.now();\n const span = Math.min(RANGES[this.range()] ?? RANGES['1h'], maxMs ?? Number.MAX_SAFE_INTEGER);\n return { from: new Date(to - span).toISOString(), to: new Date(to).toISOString() };\n }\n\n /** Query parameters for a link: the context plus the target page's own. */\n keep(extra: Params = {}): Params {\n const p = this.params();\n const context: Params = {};\n for (const key of ['env', 'range', 'from', 'to']) if (p[key]) context[key] = p[key];\n return { ...context, ...extra };\n }\n\n /** Changes the context on the current page, keeping its other parameters. */\n set(change: { env?: string | null; range?: string | null; from?: string | null; to?: string | null }) {\n const queryParams: Params = { ...change };\n if (change.range) Object.assign(queryParams, { from: null, to: null });\n void this.router.navigate([], { relativeTo: this.leaf(), queryParams, queryParamsHandling: 'merge' });\n }\n\n /** A time window centred on a moment, for \"around this span\" links. */\n around(at: string, durationMs: number, paddingMs = 5 * 60_000): Params {\n const start = new Date(at).getTime();\n return this.keep({\n from: new Date(start - paddingMs).toISOString(),\n to: new Date(start + durationMs + paddingMs).toISOString(),\n range: null,\n });\n }\n\n private leaf(): ActivatedRoute {\n let route = this.router.routerState.root;\n while (route.firstChild) route = route.firstChild;\n return route;\n }\n}\n","import { Routes } from '@angular/router';\nimport { Menus } from '@juice-js/core';\nimport { normalizeBasePath } from './core/options';\n\nfunction page(path: string, title: string, load: Routes[number]['loadComponent']): Routes[number] {\n return { path, title: `${title} · Juice Observability`, loadComponent: load };\n}\n\n/**\n * The pages, investigation first (Console requirements §4): services, traces, topology, logs and metrics; alerts and\n * rules after. Detail pages are deep links (§17) and carry the environment and time range in their query parameters.\n * The host mounts them at its `basePath` (see {@link provideJuiceObservability}) and adds its own guards.\n */\nexport const OBSERVABILITY_ROUTES: Routes = [\n page('', 'Overview', () => import('./pages/overview.page').then(m => m.OverviewPage)),\n page('services', 'Services', () => import('./pages/services.page').then(m => m.ServicesPage)),\n page('services/:id', 'Service', () => import('./pages/service.page').then(m => m.ServicePage)),\n page('traces', 'Traces', () => import('./pages/traces.page').then(m => m.TracesPage)),\n page('traces/:traceId', 'Trace', () => import('./pages/trace.page').then(m => m.TracePage)),\n page('topology', 'Dependencies', () => import('./pages/topology.page').then(m => m.TopologyPage)),\n page('logs', 'Logs', () => import('./pages/logs.page').then(m => m.LogsPage)),\n page('metrics', 'Metrics', () => import('./pages/metrics.page').then(m => m.MetricsPage)),\n page('resources', 'Resources', () => import('./pages/resources.page').then(m => m.ResourcesPage)),\n page('resources/:id', 'Resource', () => import('./pages/resource.page').then(m => m.ResourcePage)),\n page('alerts', 'Alerts', () => import('./pages/alerts.page').then(m => m.AlertsPage)),\n page('alerts/:id', 'Alert', () => import('./pages/alert.page').then(m => m.AlertPage)),\n page('rules', 'Rules', () => import('./pages/rules.page').then(m => m.RulesPage)),\n];\n\n/** The side menu entries (juice-layout), investigation pages first, linking under `basePath`. */\nexport function observabilityMenus(basePath = '/', order = 1): Menus {\n const base = normalizeBasePath(basePath);\n const link = (path: string) => (base === '/' ? `/${path}` : path ? `${base}/${path}` : base);\n return [\n { id: 'overview', label: 'Overview', icon: 'dashboard', routerLink: link('') },\n { id: 'services', label: 'Services', icon: 'hub', routerLink: link('services') },\n { id: 'traces', label: 'Traces', icon: 'timeline', routerLink: link('traces') },\n { id: 'topology', label: 'Dependencies', icon: 'account_tree', routerLink: link('topology') },\n { id: 'logs', label: 'Logs', icon: 'notes', routerLink: link('logs') },\n { id: 'metrics', label: 'Metrics', icon: 'show_chart', routerLink: link('metrics') },\n { id: 'resources', label: 'Resources', icon: 'dns', routerLink: link('resources') },\n { id: 'alerts', label: 'Alerts', icon: 'notifications', routerLink: link('alerts') },\n { id: 'rules', label: 'Rules', icon: 'rule', routerLink: link('rules') },\n ].map((menu, index) => ({ ...menu, order: order + index }));\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juice-js/observability",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0-alpha.20",
|
|
4
4
|
"description": "Juice Observability pages (services, traces, topology, logs, metrics, resources, alerts, rules) for an Angular app on juice-layout",
|
|
5
5
|
"author": "jack@creatorflow.io",
|
|
6
6
|
"license": "MIT",
|