@skyhook-io/radar-app 1.13.4 → 1.14.0
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/package.json +8 -8
- package/src/App.tsx +60 -24
- package/src/RadarApp.tsx +5 -0
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +325 -16
- package/src/api/diagnose.ts +62 -15
- package/src/api/drain-plan.test.ts +86 -0
- package/src/api/quotas.test.ts +26 -0
- package/src/api/quotas.ts +20 -0
- package/src/api/workloadMetrics.test.ts +40 -0
- package/src/api/workloadMetrics.ts +68 -0
- package/src/components/CloudConnectFlow.tsx +179 -42
- package/src/components/CloudFunnelButton.tsx +107 -36
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +140 -0
- package/src/components/cloudConnectHandoff.ts +124 -0
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.tsx +2 -1
- package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
- package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
- package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
- package/src/components/diagnose/InvestigationView.tsx +222 -5
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
- package/src/components/diagnose/investigationCase.test.tsx +1568 -0
- package/src/components/diagnose/investigationCase.ts +439 -0
- package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
- package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
- package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
- package/src/components/diagnose/investigationEvidence/index.ts +55 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
- package/src/components/diagnose/investigationEvidence/types.ts +512 -0
- package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
- package/src/components/diagnose/investigationMetrics.test.ts +712 -0
- package/src/components/diagnose/investigationMetrics.ts +393 -0
- package/src/components/diagnose/investigationSourceFocus.ts +2 -0
- package/src/components/diagnose/investigationState.test.ts +322 -0
- package/src/components/diagnose/investigationState.ts +147 -25
- package/src/components/diagnose/parts.test.tsx +482 -0
- package/src/components/diagnose/parts.tsx +418 -41
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
- package/src/components/resource/PrometheusCharts.tsx +4 -3
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
- package/src/components/resource/RightsizingStrip.test.ts +38 -0
- package/src/components/resource/RightsizingStrip.tsx +15 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
- package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
- package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/PodFilePreview.test.tsx +131 -0
- package/src/components/resources/PodFilePreview.tsx +394 -0
- package/src/components/resources/PodFilesystemModal.tsx +157 -67
- package/src/components/resources/ResourcesView.tsx +44 -6
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +94 -14
- package/src/components/traffic/TrafficFlowList.tsx +229 -70
- package/src/components/traffic/TrafficGraph.tsx +6 -6
- package/src/components/traffic/TrafficView.tsx +10 -9
- package/src/components/traffic/trafficFilters.test.ts +28 -1
- package/src/components/traffic/trafficFilters.ts +31 -0
- package/src/components/workload/WorkloadView.tsx +19 -8
- package/src/context/ConnectionContext.tsx +48 -3
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +15 -2
- package/src/index.css +17 -0
- package/src/index.ts +1 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
- package/src/utils/topology-namespace.test.ts +64 -0
- package/src/utils/topology-namespace.ts +18 -0
- package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
- package/src/components/diagnose/investigationEvidence.ts +0 -4784
- package/src/components/gitops/GitOpsActions.tsx +0 -1
- package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
- package/src/components/gitops/ManagedResourcesList.tsx +0 -1
- package/src/components/gitops/SyncCountdown.tsx +0 -1
- package/src/components/gitops/index.ts +0 -4
- package/src/components/helm/ManifestDiffViewer.tsx +0 -69
- package/src/components/logs/JsonLogLine.tsx +0 -1
- package/src/components/logs/LogCore.tsx +0 -2
- package/src/components/logs/useLogBuffer.ts +0 -2
- package/src/components/logs/useLogSearch.ts +0 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
- package/src/components/resources/drawer-components.tsx +0 -1
- package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
- package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/EventRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/JobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/VPARenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
- package/src/components/resources/renderers/argo-cells.tsx +0 -1
- package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
- package/src/components/resources/renderers/eso-cells.tsx +0 -1
- package/src/components/resources/renderers/flux-cells.tsx +0 -1
- package/src/components/resources/renderers/index.ts +0 -94
- package/src/components/resources/renderers/istio-cells.tsx +0 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
- package/src/components/resources/renderers/keda-cells.tsx +0 -1
- package/src/components/resources/renderers/knative-cells.tsx +0 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
- package/src/components/resources/renderers/traefik-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-shared.tsx +0 -1
- package/src/components/resources/renderers/velero-cells.tsx +0 -1
- package/src/components/resources/resource-utils-argo.ts +0 -2
- package/src/components/resources/resource-utils-certmanager.ts +0 -2
- package/src/components/resources/resource-utils-eso.ts +0 -2
- package/src/components/resources/resource-utils-flux.ts +0 -2
- package/src/components/resources/resource-utils-istio.ts +0 -2
- package/src/components/resources/resource-utils-karpenter.ts +0 -2
- package/src/components/resources/resource-utils-knative.ts +0 -2
- package/src/components/resources/resource-utils-kyverno.ts +0 -2
- package/src/components/resources/resource-utils-prometheus.ts +0 -2
- package/src/components/resources/resource-utils-traefik.ts +0 -1
- package/src/components/resources/resource-utils-trivy.ts +0 -2
- package/src/components/resources/resource-utils-velero.ts +0 -2
- package/src/components/shared/EditableYamlView.tsx +0 -24
- package/src/components/timeline/DiffViewer.tsx +0 -1
- package/src/components/traffic/index.ts +0 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
- package/src/components/ui/MetricsChart.tsx +0 -1
- package/src/components/ui/ResourceBar.tsx +0 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { displayKind, type IssueRecentChange } from "@skyhook-io/k8s-ui";
|
|
2
|
+
import type { ChartAnnotation } from "@skyhook-io/k8s-ui/components/charts";
|
|
3
|
+
import { apiVersionToGroup } from "../../utils/navigation";
|
|
4
|
+
import type { DiagnosisResourceRef } from "./diagnoseEvidenceTypes";
|
|
5
|
+
import type {
|
|
6
|
+
InvestigationEvidenceGroup,
|
|
7
|
+
InvestigationEvidenceObservation,
|
|
8
|
+
InvestigationMetricsEvidence,
|
|
9
|
+
InvestigationMetricsSelector,
|
|
10
|
+
} from "./investigationEvidence";
|
|
11
|
+
|
|
12
|
+
function metricUnitSuffix(metric: string): "bytes" | "seconds" | undefined {
|
|
13
|
+
// _sum is the sum of the observed values, so it measures what the histogram
|
|
14
|
+
// measures. _bucket and _count are counts of observations: only the `le`
|
|
15
|
+
// boundary carries the unit, and nothing here reads it, so a bucket series
|
|
16
|
+
// has no unit this can print.
|
|
17
|
+
if (metric.endsWith("_bucket") || metric.endsWith("_count")) return undefined;
|
|
18
|
+
let base = metric;
|
|
19
|
+
for (const suffix of ["_total", "_sum"]) {
|
|
20
|
+
if (base.endsWith(suffix)) base = base.slice(0, -suffix.length);
|
|
21
|
+
}
|
|
22
|
+
if (base.endsWith("_bytes")) return "bytes";
|
|
23
|
+
if (base.endsWith("_seconds")) return "seconds";
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* PromQL an axis label can survive. Anything outside this list leaves the axis
|
|
29
|
+
* unitless, because a unit is a claim about what the numbers mean and only the
|
|
30
|
+
* expression can support it.
|
|
31
|
+
*
|
|
32
|
+
* Aggregation operators and the `_over_time` functions that return one of the
|
|
33
|
+
* samples keep the metric's unit; so does the change in a counter. Everything
|
|
34
|
+
* else either produces a different quantity (a count, a ratio, a variance, a
|
|
35
|
+
* boolean) or is something this has not reasoned about, and both are better
|
|
36
|
+
* unlabelled than labelled wrong.
|
|
37
|
+
*/
|
|
38
|
+
const UNIT_PRESERVING_FUNCTIONS = new Set([
|
|
39
|
+
"sum",
|
|
40
|
+
"min",
|
|
41
|
+
"max",
|
|
42
|
+
"avg",
|
|
43
|
+
"by",
|
|
44
|
+
"without",
|
|
45
|
+
"abs",
|
|
46
|
+
"ceil",
|
|
47
|
+
"floor",
|
|
48
|
+
"round",
|
|
49
|
+
"clamp",
|
|
50
|
+
"clamp_max",
|
|
51
|
+
"clamp_min",
|
|
52
|
+
"label_replace",
|
|
53
|
+
"label_join",
|
|
54
|
+
"topk",
|
|
55
|
+
"bottomk",
|
|
56
|
+
"sort",
|
|
57
|
+
"sort_desc",
|
|
58
|
+
"sort_by_label",
|
|
59
|
+
"sort_by_label_desc",
|
|
60
|
+
"quantile",
|
|
61
|
+
"min_over_time",
|
|
62
|
+
"max_over_time",
|
|
63
|
+
"avg_over_time",
|
|
64
|
+
"sum_over_time",
|
|
65
|
+
"last_over_time",
|
|
66
|
+
"quantile_over_time",
|
|
67
|
+
"increase",
|
|
68
|
+
"delta",
|
|
69
|
+
"idelta",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/** The functions that divide a quantity by time. */
|
|
73
|
+
const RATE_FUNCTIONS = new Set(["rate", "irate", "deriv"]);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Blanks string literals and comments in one left-to-right pass, so neither
|
|
77
|
+
* can be read as the other. Scanning for comments first lets a `#` inside a
|
|
78
|
+
* label value swallow the rest of the query, which once left `a_bytes{note="#"}
|
|
79
|
+
* > bool 0` looking like a plain metric and gave a dimensionless 0/1 a bytes
|
|
80
|
+
* axis. PromQL strings come in three quotes; only the first two take
|
|
81
|
+
* backslash escapes.
|
|
82
|
+
*/
|
|
83
|
+
function withoutStringsAndComments(query: string): string {
|
|
84
|
+
let out = "";
|
|
85
|
+
let quote: string | undefined;
|
|
86
|
+
for (let index = 0; index < query.length; index += 1) {
|
|
87
|
+
const char = query[index];
|
|
88
|
+
if (quote) {
|
|
89
|
+
if (char === "\\" && quote !== "`") {
|
|
90
|
+
index += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === quote) quote = undefined;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
97
|
+
quote = char;
|
|
98
|
+
out += '""';
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (char === "#") {
|
|
102
|
+
while (index < query.length && query[index] !== "\n") index += 1;
|
|
103
|
+
out += " ";
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
out += char;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The unit is what every metric in the expression states through its name, as
|
|
113
|
+
* the producer's selector inventory lists them, and it only survives the
|
|
114
|
+
* operations that leave that meaning intact.
|
|
115
|
+
*
|
|
116
|
+
* `sum(x_bytes)` is bytes. `rate(x_bytes[5m])` is bytes per second, and a rate
|
|
117
|
+
* of seconds is a ratio with no unit worth printing. Everything else is barred
|
|
118
|
+
* rather than guessed: arithmetic and comparisons produce a quantity the
|
|
119
|
+
* metric's name no longer describes (`x_bytes > bool 0` is a 0 or a 1,
|
|
120
|
+
* `stdvar(x_bytes)` is bytes squared), a set operator mixes two expressions
|
|
121
|
+
* whose units may differ, `atan2` returns an angle, a second rate divides by
|
|
122
|
+
* time twice, and an unlisted function is simply unknown. Strings, comments, label matchers and
|
|
123
|
+
* range selectors are blanked before the expression is read, so nothing
|
|
124
|
+
* inside them can be mistaken for an operator or a call.
|
|
125
|
+
*/
|
|
126
|
+
export function metricsUnitForExpression(
|
|
127
|
+
query: string,
|
|
128
|
+
selectors: readonly InvestigationMetricsSelector[],
|
|
129
|
+
): string {
|
|
130
|
+
if (selectors.length === 0) return "";
|
|
131
|
+
const units = new Set(
|
|
132
|
+
selectors.map((selector) => metricUnitSuffix(selector.metric)),
|
|
133
|
+
);
|
|
134
|
+
if (units.size !== 1) return "";
|
|
135
|
+
const [unit] = units;
|
|
136
|
+
if (!unit) return "";
|
|
137
|
+
const expression = withoutStringsAndComments(query)
|
|
138
|
+
.replace(/\{[^{}]*\}/g, "")
|
|
139
|
+
.replace(/\[[^\]]*\]/g, "");
|
|
140
|
+
// Arithmetic, comparison and the set operators all produce something the
|
|
141
|
+
// metric's name no longer describes, or mix two things that disagree.
|
|
142
|
+
if (
|
|
143
|
+
/[-+/*%^]|[<>!=]=|[<>]|\b(?:bool|and|or|unless|atan2)\b/.test(expression)
|
|
144
|
+
) {
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
// An aggregation may put its grouping clause before the parenthesis, as in
|
|
148
|
+
// `count by (pod) (...)`, so the name is not always adjacent to it.
|
|
149
|
+
const functions = [
|
|
150
|
+
...expression.matchAll(
|
|
151
|
+
/([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:(?:by|without)\s*\([^()]*\)\s*)?\(/g,
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
.map((match) => match[1])
|
|
155
|
+
.filter((name) => !selectors.some((selector) => selector.metric === name));
|
|
156
|
+
const rates = functions.filter((name) => RATE_FUNCTIONS.has(name)).length;
|
|
157
|
+
if (rates > 1) return "";
|
|
158
|
+
for (const name of functions) {
|
|
159
|
+
if (RATE_FUNCTIONS.has(name)) continue;
|
|
160
|
+
if (!UNIT_PRESERVING_FUNCTIONS.has(name)) return "";
|
|
161
|
+
}
|
|
162
|
+
if (rates === 1) return unit === "bytes" ? "bytes/s" : "";
|
|
163
|
+
return unit;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function metricsDomain(
|
|
167
|
+
data: Pick<InvestigationMetricsEvidence, "mode" | "start" | "end">,
|
|
168
|
+
): { start: number; end: number } | undefined {
|
|
169
|
+
if (data.mode !== "range" || !data.start || !data.end) return undefined;
|
|
170
|
+
const start = Date.parse(data.start) / 1000;
|
|
171
|
+
const end = Date.parse(data.end) / 1000;
|
|
172
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start)
|
|
173
|
+
return undefined;
|
|
174
|
+
return { start, end };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function refKey(ref: {
|
|
178
|
+
kind: string;
|
|
179
|
+
group?: string;
|
|
180
|
+
namespace?: string;
|
|
181
|
+
name: string;
|
|
182
|
+
}): string {
|
|
183
|
+
return [
|
|
184
|
+
ref.kind.toLowerCase(),
|
|
185
|
+
(ref.group ?? "").toLowerCase(),
|
|
186
|
+
ref.namespace ?? "",
|
|
187
|
+
ref.name,
|
|
188
|
+
].join("/");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function looseKey(ref: {
|
|
192
|
+
kind: string;
|
|
193
|
+
namespace?: string;
|
|
194
|
+
name: string;
|
|
195
|
+
}): string {
|
|
196
|
+
return [ref.kind.toLowerCase(), ref.namespace ?? "", ref.name].join("/");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A change names its resource exactly when it carries an apiVersion. Helm
|
|
201
|
+
* history rows and older stores omit it, so those match on kind, namespace
|
|
202
|
+
* and name alone, which is unambiguous only against the subject and its
|
|
203
|
+
* producer-established relatives.
|
|
204
|
+
*/
|
|
205
|
+
function changeMatchesRelative(
|
|
206
|
+
change: IssueRecentChange,
|
|
207
|
+
strictKeys: ReadonlySet<string>,
|
|
208
|
+
looseKeys: ReadonlySet<string>,
|
|
209
|
+
): boolean {
|
|
210
|
+
if (change.apiVersion) {
|
|
211
|
+
return strictKeys.has(
|
|
212
|
+
refKey({
|
|
213
|
+
kind: change.kind,
|
|
214
|
+
group: apiVersionToGroup(change.apiVersion),
|
|
215
|
+
namespace: change.namespace,
|
|
216
|
+
name: change.name,
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return looseKeys.has(looseKey(change));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function sameResource(
|
|
224
|
+
left: DiagnosisResourceRef,
|
|
225
|
+
right: DiagnosisResourceRef,
|
|
226
|
+
): boolean {
|
|
227
|
+
return refKey(left) === refKey(right);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The resources a change may be recorded against and still count as a change
|
|
232
|
+
* to the chart's subject: the subject itself and the relatives producers named
|
|
233
|
+
* in the same turn (the configuration it consumes, and the pods and ReplicaSets
|
|
234
|
+
* diagnosis rows identified). Name prefixes never qualify.
|
|
235
|
+
*/
|
|
236
|
+
function producerRelatives(
|
|
237
|
+
groups: readonly InvestigationEvidenceGroup[],
|
|
238
|
+
subject: DiagnosisResourceRef,
|
|
239
|
+
turnIndex: number,
|
|
240
|
+
): DiagnosisResourceRef[] {
|
|
241
|
+
const relatives: DiagnosisResourceRef[] = [subject];
|
|
242
|
+
for (const group of groups) {
|
|
243
|
+
for (const observation of group.observations) {
|
|
244
|
+
if (
|
|
245
|
+
observation.source.turnIndex !== turnIndex ||
|
|
246
|
+
observation.relevance === "broader"
|
|
247
|
+
) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const { data } = observation;
|
|
251
|
+
switch (data.type) {
|
|
252
|
+
case "resource": {
|
|
253
|
+
const ref: DiagnosisResourceRef = {
|
|
254
|
+
kind: data.resource.kind,
|
|
255
|
+
group: apiVersionToGroup(data.resource.apiVersion) || undefined,
|
|
256
|
+
namespace: data.resource.metadata.namespace,
|
|
257
|
+
name: data.resource.metadata.name,
|
|
258
|
+
};
|
|
259
|
+
if (!sameResource(ref, subject)) break;
|
|
260
|
+
const uses = data.resourceContext?.uses;
|
|
261
|
+
relatives.push(...(uses?.configMaps ?? []), ...(uses?.secrets ?? []));
|
|
262
|
+
if (data.resourceContext?.owner) {
|
|
263
|
+
relatives.push(data.resourceContext.owner);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
case "startup":
|
|
268
|
+
if (data.subject) relatives.push(data.subject);
|
|
269
|
+
break;
|
|
270
|
+
case "logs":
|
|
271
|
+
if (data.namespace) {
|
|
272
|
+
relatives.push({
|
|
273
|
+
kind: "Pod",
|
|
274
|
+
namespace: data.namespace,
|
|
275
|
+
name: data.pod,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case "crash":
|
|
280
|
+
if (data.namespace) {
|
|
281
|
+
for (const pod of data.crash.pods) {
|
|
282
|
+
relatives.push({
|
|
283
|
+
kind: "Pod",
|
|
284
|
+
namespace: data.namespace,
|
|
285
|
+
name: pod,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
default:
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return relatives;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Changes Radar recorded for the chart's subject inside the chart window,
|
|
300
|
+
* paired with whether that lookup was possible at all. An empty marker list
|
|
301
|
+
* means "none were recorded" only when `checked` is true; otherwise the chart
|
|
302
|
+
* had no subject to look changes up for, or the turn captured no changes to
|
|
303
|
+
* look through, and the chart must not claim a clean window.
|
|
304
|
+
*/
|
|
305
|
+
export type MetricsChangeCoverage = {
|
|
306
|
+
markers: ChartAnnotation[];
|
|
307
|
+
/** False when no change lookup was possible, so an empty list proves nothing. */
|
|
308
|
+
checked: boolean;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export function metricsChangeCoverage(
|
|
312
|
+
groups: readonly InvestigationEvidenceGroup[],
|
|
313
|
+
observation: InvestigationEvidenceObservation,
|
|
314
|
+
): MetricsChangeCoverage {
|
|
315
|
+
const { data } = observation;
|
|
316
|
+
// A broader chart's subject is a neighbour; the turn's non-broader relatives
|
|
317
|
+
// and changes all belong to the target, so marking them there would
|
|
318
|
+
// attribute the target's changes to the neighbour.
|
|
319
|
+
if (
|
|
320
|
+
data.type !== "metrics" ||
|
|
321
|
+
!data.subject ||
|
|
322
|
+
observation.relevance === "broader"
|
|
323
|
+
) {
|
|
324
|
+
return { markers: [], checked: false };
|
|
325
|
+
}
|
|
326
|
+
const domain = metricsDomain(data);
|
|
327
|
+
if (!domain) return { markers: [], checked: false };
|
|
328
|
+
const turnIndex = observation.source.turnIndex;
|
|
329
|
+
const relatives = producerRelatives(groups, data.subject, turnIndex);
|
|
330
|
+
const strictKeys = new Set(relatives.map(refKey));
|
|
331
|
+
const looseKeys = new Set(relatives.map(looseKey));
|
|
332
|
+
const seen = new Set<string>();
|
|
333
|
+
const markers: ChartAnnotation[] = [];
|
|
334
|
+
let checked = false;
|
|
335
|
+
for (const group of groups) {
|
|
336
|
+
for (const candidate of group.observations) {
|
|
337
|
+
if (
|
|
338
|
+
candidate.source.turnIndex !== turnIndex ||
|
|
339
|
+
candidate.relevance === "broader"
|
|
340
|
+
) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
// An authoritative empty change window is filed as a receipt, and counts
|
|
344
|
+
// as a check just as a change list does. A window the producer could not
|
|
345
|
+
// vouch for becomes a limitation instead, never a receipt, so it leaves
|
|
346
|
+
// `checked` false.
|
|
347
|
+
if (
|
|
348
|
+
candidate.data.type === "receipt" &&
|
|
349
|
+
candidate.data.checked === "changes"
|
|
350
|
+
) {
|
|
351
|
+
checked = true;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (candidate.data.type !== "changes") continue;
|
|
355
|
+
checked = true;
|
|
356
|
+
for (const change of candidate.data.changes) {
|
|
357
|
+
const timestamp = Date.parse(change.timestamp) / 1000;
|
|
358
|
+
if (
|
|
359
|
+
!Number.isFinite(timestamp) ||
|
|
360
|
+
timestamp < domain.start ||
|
|
361
|
+
timestamp > domain.end ||
|
|
362
|
+
!changeMatchesRelative(change, strictKeys, looseKeys)
|
|
363
|
+
) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const identity = `${change.apiVersion ?? ""}|${change.kind}|${change.namespace ?? ""}|${change.name}|${change.timestamp}`;
|
|
367
|
+
if (seen.has(identity)) continue;
|
|
368
|
+
seen.add(identity);
|
|
369
|
+
markers.push({
|
|
370
|
+
timestamp,
|
|
371
|
+
label: `${displayKind(change.kind)} ${change.name}`,
|
|
372
|
+
kind: "change",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
markers: markers.sort((left, right) => left.timestamp - right.timestamp),
|
|
379
|
+
checked,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Changes Radar recorded for the chart's subject inside the chart window.
|
|
385
|
+
* Callers that need to tell "none recorded" from "not looked up" want
|
|
386
|
+
* {@link metricsChangeCoverage} instead.
|
|
387
|
+
*/
|
|
388
|
+
export function metricsChangeMarkers(
|
|
389
|
+
groups: readonly InvestigationEvidenceGroup[],
|
|
390
|
+
observation: InvestigationEvidenceObservation,
|
|
391
|
+
): ChartAnnotation[] {
|
|
392
|
+
return metricsChangeCoverage(groups, observation).markers;
|
|
393
|
+
}
|