@skyhook-io/radar-app 1.13.3 → 1.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/src/App.tsx +68 -23
- package/src/RadarApp.tsx +17 -1
- package/src/api/client.ts +28 -10
- package/src/api/diagnose.ts +61 -15
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +95 -0
- package/src/components/diagnose/DiagnoseContext.tsx +402 -77
- package/src/components/diagnose/DiagnoseSurface.test.tsx +53 -54
- package/src/components/diagnose/DiagnoseSurface.tsx +198 -286
- package/src/components/diagnose/Home.test.tsx +23 -1
- package/src/components/diagnose/Home.tsx +49 -3
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +1446 -5
- package/src/components/diagnose/InvestigationEvidencePane.tsx +1373 -126
- package/src/components/diagnose/InvestigationView.tsx +227 -5
- package/src/components/diagnose/LocalDiagnoseAction.tsx +2 -3
- 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.test.ts +3525 -17
- package/src/components/diagnose/investigationEvidence.ts +2246 -166
- 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 +6 -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 +537 -1
- package/src/components/diagnose/parts.tsx +486 -42
- package/src/components/resource/HPACharts.render.test.tsx +77 -0
- package/src/components/resource/HPACharts.tsx +32 -25
- package/src/components/resource/PrometheusChartsGrid.tsx +181 -79
- 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 +27 -2
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +14 -2
- package/src/index.ts +8 -5
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { pluralToKind } from "@skyhook-io/k8s-ui";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
DiagnosisEvidenceItem,
|
|
5
|
+
DiagnosisEvidenceRole,
|
|
6
|
+
DiagnosisEvidenceSubject,
|
|
7
|
+
DiagnosisRuledOut,
|
|
8
|
+
} from "../../api/diagnose";
|
|
9
|
+
import {
|
|
10
|
+
investigationEvidenceSubjectRef,
|
|
11
|
+
investigationSourceArgs,
|
|
12
|
+
isInvestigationEvidenceRef,
|
|
13
|
+
type InvestigationEvidenceGroup,
|
|
14
|
+
type InvestigationEvidenceObservation,
|
|
15
|
+
type InvestigationEvidenceProjection,
|
|
16
|
+
type InvestigationEvidenceSource,
|
|
17
|
+
} from "./investigationEvidence";
|
|
18
|
+
|
|
19
|
+
function nonEmptyString(value: unknown): value is string {
|
|
20
|
+
return typeof value === "string" && value !== "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type InvestigationCasePlacement = "card" | "revision" | "source";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* One linked item of the agent's case, resolved against the captured
|
|
27
|
+
* evidence. `card` and `revision` items are pinned to exactly one observation;
|
|
28
|
+
* a `source` item could not be pinned and renders only beside its source in
|
|
29
|
+
* Assessment details.
|
|
30
|
+
*/
|
|
31
|
+
export interface InvestigationCaseItem {
|
|
32
|
+
/** Position in Diagnosis.evidence; ruled-out entries point here. */
|
|
33
|
+
index: number;
|
|
34
|
+
role: DiagnosisEvidenceRole;
|
|
35
|
+
claim: string;
|
|
36
|
+
subject?: DiagnosisEvidenceSubject;
|
|
37
|
+
source: InvestigationEvidenceSource;
|
|
38
|
+
placement: InvestigationCasePlacement;
|
|
39
|
+
groupId?: string;
|
|
40
|
+
observation?: InvestigationEvidenceObservation;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface InvestigationCaseRuledOut {
|
|
44
|
+
hypothesis: string;
|
|
45
|
+
/** Always a card- or revision-placed item; unplaceable targets are not rendered. */
|
|
46
|
+
item: InvestigationCaseItem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface InvestigationCaseResolution {
|
|
50
|
+
items: InvestigationCaseItem[];
|
|
51
|
+
ruledOut: InvestigationCaseRuledOut[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Notes the agent put on cards in earlier assessments stay on those cards
|
|
56
|
+
* when a later turn takes over the pane's case, unless the later turn
|
|
57
|
+
* addressed the same card; a follow-up about one chart must not strip the
|
|
58
|
+
* initial assessment's reading from everything else. Ordering and the
|
|
59
|
+
* ruled-out list follow the live turn alone, and source-placed notes stay
|
|
60
|
+
* listed under their own assessment. `earlier` is newest first.
|
|
61
|
+
*/
|
|
62
|
+
export function mergeInvestigationCases(
|
|
63
|
+
live: InvestigationCaseResolution | undefined,
|
|
64
|
+
earlier: readonly (InvestigationCaseResolution | undefined)[],
|
|
65
|
+
): InvestigationCaseResolution | undefined {
|
|
66
|
+
const liveItems = live?.items ?? [];
|
|
67
|
+
const covered = new Set(
|
|
68
|
+
liveItems.flatMap((item) => (item.groupId ? [item.groupId] : [])),
|
|
69
|
+
);
|
|
70
|
+
const carried: InvestigationCaseItem[] = [];
|
|
71
|
+
for (const resolution of earlier) {
|
|
72
|
+
// The newest assessment that spoke about a group wins it outright, and it
|
|
73
|
+
// wins with everything it said: a card note and a pinned revision note are
|
|
74
|
+
// two readings of one group, not rivals. Coverage is therefore taken after
|
|
75
|
+
// the whole resolution, not as each of its items is carried.
|
|
76
|
+
const takenHere = new Set<string>();
|
|
77
|
+
for (const item of resolution?.items ?? []) {
|
|
78
|
+
if (item.placement === "source" || !item.groupId) continue;
|
|
79
|
+
if (covered.has(item.groupId)) continue;
|
|
80
|
+
takenHere.add(item.groupId);
|
|
81
|
+
carried.push(item);
|
|
82
|
+
}
|
|
83
|
+
for (const groupId of takenHere) covered.add(groupId);
|
|
84
|
+
}
|
|
85
|
+
if (carried.length === 0) return live;
|
|
86
|
+
return { items: [...liveItems, ...carried], ruledOut: live?.ruledOut ?? [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The subset of one assessment's items that survived into the case the pane
|
|
91
|
+
* actually renders. Matching is by value, not object identity: the merge is
|
|
92
|
+
* fed freshly resolved copies of every turn, so an identity test would report
|
|
93
|
+
* that an assessment's own note had vanished the moment any other turn was
|
|
94
|
+
* re-resolved.
|
|
95
|
+
*/
|
|
96
|
+
export function investigationCaseItemsStillRendered(
|
|
97
|
+
assessmentItems: readonly InvestigationCaseItem[] | undefined,
|
|
98
|
+
renderedItems: readonly InvestigationCaseItem[] | undefined,
|
|
99
|
+
): InvestigationCaseItem[] {
|
|
100
|
+
if (!assessmentItems?.length || !renderedItems?.length) return [];
|
|
101
|
+
const key = (item: InvestigationCaseItem) =>
|
|
102
|
+
[
|
|
103
|
+
item.index,
|
|
104
|
+
item.source.id,
|
|
105
|
+
item.placement,
|
|
106
|
+
item.groupId ?? "",
|
|
107
|
+
item.observation ? investigationCaseObservationKey(item.observation) : "",
|
|
108
|
+
].join("\u0000");
|
|
109
|
+
const rendered = new Set(renderedItems.map(key));
|
|
110
|
+
return assessmentItems.filter((item) => rendered.has(key(item)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One half of a Go↔TS contract: `evidenceRoles` in internal/ai/parse.go and the
|
|
115
|
+
* DiagnosisEvidenceRole union in api/diagnose.ts must list exactly these roles.
|
|
116
|
+
* A role the parser accepts but this set omits is bound server-side and then
|
|
117
|
+
* silently dropped here. Change all three together.
|
|
118
|
+
*/
|
|
119
|
+
export const EVIDENCE_ROLES: ReadonlySet<string> =
|
|
120
|
+
new Set<DiagnosisEvidenceRole>([
|
|
121
|
+
"cause",
|
|
122
|
+
"symptom",
|
|
123
|
+
"context",
|
|
124
|
+
"benign",
|
|
125
|
+
"demoted",
|
|
126
|
+
"rules_out",
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
export function investigationCaseObservationKey(
|
|
130
|
+
observation: Pick<InvestigationEvidenceObservation, "source" | "revision">,
|
|
131
|
+
): string {
|
|
132
|
+
return `${observation.source.id}#${observation.revision}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolves the agent's case for one assessment. Refs are re-validated against
|
|
137
|
+
* the assessment turn exactly like root-cause refs; an item whose ref does not
|
|
138
|
+
* resolve is dropped on its own. Placement follows the observation the
|
|
139
|
+
* (ref, subject) pair names: exactly one match pins the claim to that
|
|
140
|
+
* observation, anything else falls back to the source row.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveInvestigationCase(
|
|
143
|
+
projection: InvestigationEvidenceProjection,
|
|
144
|
+
diagnosis:
|
|
145
|
+
| { evidence?: DiagnosisEvidenceItem[]; ruledOut?: DiagnosisRuledOut[] }
|
|
146
|
+
| null
|
|
147
|
+
| undefined,
|
|
148
|
+
assessmentTurnIndex: number,
|
|
149
|
+
): InvestigationCaseResolution {
|
|
150
|
+
const evidence = diagnosis?.evidence ?? [];
|
|
151
|
+
if (evidence.length === 0) return { items: [], ruledOut: [] };
|
|
152
|
+
const byRef = new Map<string, InvestigationEvidenceSource[]>();
|
|
153
|
+
for (const source of projection.evidenceRefSources) {
|
|
154
|
+
if (source.turnIndex !== assessmentTurnIndex || !source.evidenceRef)
|
|
155
|
+
continue;
|
|
156
|
+
const matches = byRef.get(source.evidenceRef) ?? [];
|
|
157
|
+
matches.push(source);
|
|
158
|
+
byRef.set(source.evidenceRef, matches);
|
|
159
|
+
}
|
|
160
|
+
const citableSourceIds = new Set(
|
|
161
|
+
projection.citableSources
|
|
162
|
+
.filter((source) => source.turnIndex === assessmentTurnIndex)
|
|
163
|
+
.map((source) => source.id),
|
|
164
|
+
);
|
|
165
|
+
const items: InvestigationCaseItem[] = [];
|
|
166
|
+
const byIndex = new Map<number, InvestigationCaseItem>();
|
|
167
|
+
evidence.forEach((entry, index) => {
|
|
168
|
+
if (
|
|
169
|
+
entry.status !== "linked" ||
|
|
170
|
+
!entry.ref ||
|
|
171
|
+
!isInvestigationEvidenceRef(entry.ref) ||
|
|
172
|
+
!entry.role ||
|
|
173
|
+
!EVIDENCE_ROLES.has(entry.role)
|
|
174
|
+
) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const matches = byRef.get(entry.ref);
|
|
178
|
+
if (matches?.length !== 1 || !citableSourceIds.has(matches[0].id)) return;
|
|
179
|
+
const source = matches[0];
|
|
180
|
+
const subject = validCaseSubject(entry.subject);
|
|
181
|
+
// A subject the agent supplied but got wrong is not the same as one it
|
|
182
|
+
// deliberately omitted. Omission asks Radar to place the note anywhere the
|
|
183
|
+
// cited call produced; a malformed subject asked for something specific
|
|
184
|
+
// that does not resolve, so the note stays beside its source rather than
|
|
185
|
+
// landing on whichever observation happens to be the only one.
|
|
186
|
+
const subjectUnusable = entry.subject !== undefined && !subject;
|
|
187
|
+
const item: InvestigationCaseItem = {
|
|
188
|
+
index,
|
|
189
|
+
role: entry.role,
|
|
190
|
+
claim: typeof entry.claim === "string" ? entry.claim.trim() : "",
|
|
191
|
+
...(subject ? { subject } : {}),
|
|
192
|
+
source,
|
|
193
|
+
placement: "source",
|
|
194
|
+
};
|
|
195
|
+
const candidates = subjectUnusable
|
|
196
|
+
? []
|
|
197
|
+
: projection.groups.flatMap((group) =>
|
|
198
|
+
group.observations
|
|
199
|
+
.filter(
|
|
200
|
+
(observation) =>
|
|
201
|
+
observation.source.id === source.id &&
|
|
202
|
+
(!subject ||
|
|
203
|
+
observationMatchesSubject(group, observation, subject)),
|
|
204
|
+
)
|
|
205
|
+
.map((observation) => ({ group, observation })),
|
|
206
|
+
);
|
|
207
|
+
if (candidates.length === 1) {
|
|
208
|
+
const { group, observation } = candidates[0];
|
|
209
|
+
item.groupId = group.id;
|
|
210
|
+
item.observation = observation;
|
|
211
|
+
// The claim stays bound to the exact observation the agent cited. Only
|
|
212
|
+
// the card's authoritative observation carries it on the card head; a
|
|
213
|
+
// superseded read keeps its own revision row even when it displays the
|
|
214
|
+
// same, because display equivalence ignores counts and times.
|
|
215
|
+
item.placement = observation === group.latest ? "card" : "revision";
|
|
216
|
+
}
|
|
217
|
+
items.push(item);
|
|
218
|
+
byIndex.set(index, item);
|
|
219
|
+
});
|
|
220
|
+
const ruledOut: InvestigationCaseRuledOut[] = [];
|
|
221
|
+
const seenRuledOut = new Set<string>();
|
|
222
|
+
for (const entry of diagnosis?.ruledOut ?? []) {
|
|
223
|
+
if (
|
|
224
|
+
typeof entry.hypothesis !== "string" ||
|
|
225
|
+
!entry.hypothesis.trim() ||
|
|
226
|
+
!Number.isInteger(entry.evidenceIndex)
|
|
227
|
+
) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const item = byIndex.get(entry.evidenceIndex);
|
|
231
|
+
if (!item || item.placement === "source") continue;
|
|
232
|
+
const hypothesis = entry.hypothesis.trim();
|
|
233
|
+
const dedupeKey = `${entry.evidenceIndex}\u0000${hypothesis}`;
|
|
234
|
+
if (seenRuledOut.has(dedupeKey)) continue;
|
|
235
|
+
seenRuledOut.add(dedupeKey);
|
|
236
|
+
ruledOut.push({ hypothesis, item });
|
|
237
|
+
}
|
|
238
|
+
return { items, ruledOut };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validCaseSubject(
|
|
242
|
+
value: DiagnosisEvidenceSubject | undefined,
|
|
243
|
+
): DiagnosisEvidenceSubject | undefined {
|
|
244
|
+
if (!value || !nonEmptyString(value.kind) || !nonEmptyString(value.name))
|
|
245
|
+
return undefined;
|
|
246
|
+
const optional = (field: unknown) =>
|
|
247
|
+
field === undefined || typeof field === "string";
|
|
248
|
+
if (
|
|
249
|
+
!optional(value.group) ||
|
|
250
|
+
!optional(value.namespace) ||
|
|
251
|
+
!optional(value.container) ||
|
|
252
|
+
!optional(value.observation) ||
|
|
253
|
+
(value.stream !== undefined &&
|
|
254
|
+
value.stream !== "current" &&
|
|
255
|
+
value.stream !== "previous")
|
|
256
|
+
) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface ObservationSubjectIdentity {
|
|
263
|
+
kind: string;
|
|
264
|
+
/** Empty string is a known core group; undefined means the producer did not say. */
|
|
265
|
+
group?: string;
|
|
266
|
+
/** Empty string is known cluster scope; undefined means the producer did not say. */
|
|
267
|
+
namespace?: string;
|
|
268
|
+
name: string;
|
|
269
|
+
container?: string;
|
|
270
|
+
stream?: "current" | "previous";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* What an observation is about, for placing an agent claim. Observations that
|
|
275
|
+
* state no resource of their own (events, changes without a subject, receipts)
|
|
276
|
+
* inherit the resource their producing call was asked about, so a claim can
|
|
277
|
+
* name "the events of Deployment api" inside a diagnose bundle.
|
|
278
|
+
*
|
|
279
|
+
* A stated resource identity knows its API group and scope: a core resource
|
|
280
|
+
* has group "" and a cluster-scoped one has namespace "". Only args-derived
|
|
281
|
+
* identities leave those undefined, and only then do they act as wildcards.
|
|
282
|
+
*/
|
|
283
|
+
function observationSubjectIdentity(
|
|
284
|
+
observation: InvestigationEvidenceObservation,
|
|
285
|
+
): ObservationSubjectIdentity | undefined {
|
|
286
|
+
const { data } = observation;
|
|
287
|
+
const stated = investigationEvidenceSubjectRef(data);
|
|
288
|
+
if (stated) {
|
|
289
|
+
// Producers that state a resource from its own object, or from the
|
|
290
|
+
// investigation target, know the group exactly; a subject ref copied from
|
|
291
|
+
// another producer's payload may not.
|
|
292
|
+
const groupKnown =
|
|
293
|
+
data.type === "resource" ||
|
|
294
|
+
data.type === "issue" ||
|
|
295
|
+
data.type === "logs" ||
|
|
296
|
+
data.type === "crash" ||
|
|
297
|
+
data.type === "startup" ||
|
|
298
|
+
data.type === "helm" ||
|
|
299
|
+
data.type === "permissions" ||
|
|
300
|
+
data.type === "metrics";
|
|
301
|
+
return {
|
|
302
|
+
kind: stated.kind,
|
|
303
|
+
group: stated.group ?? (groupKnown ? "" : undefined),
|
|
304
|
+
namespace: stated.namespace ?? "",
|
|
305
|
+
name: stated.name,
|
|
306
|
+
...(data.type === "logs"
|
|
307
|
+
? {
|
|
308
|
+
container: data.container,
|
|
309
|
+
stream: data.previous ? "previous" : "current",
|
|
310
|
+
}
|
|
311
|
+
: {}),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (data.type === "changes" && data.subject?.kind) {
|
|
315
|
+
return {
|
|
316
|
+
kind: data.subject.kind,
|
|
317
|
+
namespace: data.subject.namespace,
|
|
318
|
+
name: data.subject.name,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const args = investigationSourceArgs(observation.source);
|
|
322
|
+
if (!args || !nonEmptyString(args.kind) || !nonEmptyString(args.name))
|
|
323
|
+
return undefined;
|
|
324
|
+
return {
|
|
325
|
+
kind: args.kind,
|
|
326
|
+
group: nonEmptyString(args.group) ? args.group : undefined,
|
|
327
|
+
namespace: nonEmptyString(args.namespace) ? args.namespace : undefined,
|
|
328
|
+
name: args.name,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function sameKind(left: string, right: string): boolean {
|
|
333
|
+
return pluralToKind(left).toLowerCase() === pluralToKind(right).toLowerCase();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* A discriminator the agent omits is a wildcard, and so is one the producer
|
|
338
|
+
* did not state; every discriminator both sides supply must match. Uniqueness
|
|
339
|
+
* of the match, not completeness of the subject, is what places a claim.
|
|
340
|
+
*/
|
|
341
|
+
function observationMatchesSubject(
|
|
342
|
+
group: InvestigationEvidenceGroup,
|
|
343
|
+
observation: InvestigationEvidenceObservation,
|
|
344
|
+
subject: DiagnosisEvidenceSubject,
|
|
345
|
+
): boolean {
|
|
346
|
+
if (subject.observation !== undefined) {
|
|
347
|
+
// A diagnose bundle captures several vitals charts for one resource, so
|
|
348
|
+
// "metrics" alone cannot name one; "metrics:<category>" picks the chart
|
|
349
|
+
// whose identity ends in that category. An agent-run query is one chart,
|
|
350
|
+
// so a qualifier on it carries no meaning.
|
|
351
|
+
const [kind, qualifier] = subject.observation.toLowerCase().split(":", 2);
|
|
352
|
+
if (kind !== observation.data.type) return false;
|
|
353
|
+
if (
|
|
354
|
+
qualifier !== undefined &&
|
|
355
|
+
observation.data.type === "metrics" &&
|
|
356
|
+
observation.data.origin === "diagnose" &&
|
|
357
|
+
!group.identity.endsWith(`:${qualifier}`)
|
|
358
|
+
) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const identities = observationIdentities(observation);
|
|
363
|
+
// An observation that states no resource of its own (an agent-run query
|
|
364
|
+
// whose selectors do not name the target exactly, a topology summary) can
|
|
365
|
+
// only be named by its evidence kind; the ref and the uniqueness rule do
|
|
366
|
+
// the rest.
|
|
367
|
+
if (identities.length === 0) return subject.observation !== undefined;
|
|
368
|
+
return identities.some((identity) =>
|
|
369
|
+
identityMatchesSubject(identity, observation, subject),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A log stream is identified by its pod, and also by the workload its
|
|
375
|
+
* producing call was asked about: an agent naming "the Deployment's
|
|
376
|
+
* container logs" means the stream a diagnose bundle read for that
|
|
377
|
+
* Deployment, and uniqueness still decides whether that names one.
|
|
378
|
+
*/
|
|
379
|
+
function observationIdentities(
|
|
380
|
+
observation: InvestigationEvidenceObservation,
|
|
381
|
+
): ObservationSubjectIdentity[] {
|
|
382
|
+
const stated = observationSubjectIdentity(observation);
|
|
383
|
+
const identities = stated ? [stated] : [];
|
|
384
|
+
if (observation.data.type === "logs" && stated) {
|
|
385
|
+
const args = investigationSourceArgs(observation.source);
|
|
386
|
+
if (args && nonEmptyString(args.kind) && nonEmptyString(args.name)) {
|
|
387
|
+
identities.push({
|
|
388
|
+
kind: args.kind,
|
|
389
|
+
group: nonEmptyString(args.group) ? args.group : undefined,
|
|
390
|
+
namespace: nonEmptyString(args.namespace) ? args.namespace : undefined,
|
|
391
|
+
name: args.name,
|
|
392
|
+
container: stated.container,
|
|
393
|
+
stream: stated.stream,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return identities;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function identityMatchesSubject(
|
|
401
|
+
identity: ObservationSubjectIdentity,
|
|
402
|
+
observation: InvestigationEvidenceObservation,
|
|
403
|
+
subject: DiagnosisEvidenceSubject,
|
|
404
|
+
): boolean {
|
|
405
|
+
if (!sameKind(identity.kind, subject.kind) || identity.name !== subject.name)
|
|
406
|
+
return false;
|
|
407
|
+
if (
|
|
408
|
+
subject.group !== undefined &&
|
|
409
|
+
identity.group !== undefined &&
|
|
410
|
+
subject.group.toLowerCase() !== identity.group.toLowerCase()
|
|
411
|
+
) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
subject.namespace !== undefined &&
|
|
416
|
+
identity.namespace !== undefined &&
|
|
417
|
+
subject.namespace !== identity.namespace
|
|
418
|
+
) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
// Container and stream are log-stream dimensions. Without an observation
|
|
422
|
+
// kind they say "a log stream"; with one stated for another kind (a
|
|
423
|
+
// container-scoped metrics query, say) they are descriptive only.
|
|
424
|
+
const streamDimensions =
|
|
425
|
+
observation.data.type === "logs" || subject.observation === undefined;
|
|
426
|
+
if (
|
|
427
|
+
streamDimensions &&
|
|
428
|
+
subject.container !== undefined &&
|
|
429
|
+
identity.container !== subject.container
|
|
430
|
+
)
|
|
431
|
+
return false;
|
|
432
|
+
if (
|
|
433
|
+
streamDimensions &&
|
|
434
|
+
subject.stream !== undefined &&
|
|
435
|
+
identity.stream !== subject.stream
|
|
436
|
+
)
|
|
437
|
+
return false;
|
|
438
|
+
return true;
|
|
439
|
+
}
|