@saptools/cf-events 0.1.4 → 0.2.1
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 +31 -29
- package/dist/cli.js +253 -37
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +63 -15
- package/dist/index.js +244 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -10,8 +10,9 @@ interface CfCredentials {
|
|
|
10
10
|
readonly email: string;
|
|
11
11
|
readonly password: string;
|
|
12
12
|
}
|
|
13
|
-
/** A fully resolved
|
|
14
|
-
interface
|
|
13
|
+
/** A fully resolved application target. */
|
|
14
|
+
interface ResolvedAppSelector {
|
|
15
|
+
readonly kind: "app";
|
|
15
16
|
readonly raw: string;
|
|
16
17
|
readonly regionKey: RegionKey;
|
|
17
18
|
readonly apiEndpoint: string;
|
|
@@ -19,6 +20,16 @@ interface ResolvedSelector {
|
|
|
19
20
|
readonly spaceName: string;
|
|
20
21
|
readonly appName: string;
|
|
21
22
|
}
|
|
23
|
+
/** A fully resolved space target for space-wide audit-event queries. */
|
|
24
|
+
interface ResolvedSpaceSelector {
|
|
25
|
+
readonly kind: "space";
|
|
26
|
+
readonly raw: string;
|
|
27
|
+
readonly regionKey: RegionKey;
|
|
28
|
+
readonly apiEndpoint: string;
|
|
29
|
+
readonly orgName: string;
|
|
30
|
+
readonly spaceName: string;
|
|
31
|
+
}
|
|
32
|
+
type ResolvedSelector = ResolvedAppSelector | ResolvedSpaceSelector;
|
|
22
33
|
interface CfSessionInput {
|
|
23
34
|
readonly apiEndpoint: string;
|
|
24
35
|
readonly orgName: string;
|
|
@@ -104,8 +115,30 @@ interface AppHealth {
|
|
|
104
115
|
readonly instances: readonly ProcessInstanceStat[];
|
|
105
116
|
readonly lastEvent: AuditEvent | undefined;
|
|
106
117
|
}
|
|
107
|
-
interface
|
|
118
|
+
interface AppCrashSummary {
|
|
119
|
+
readonly appName: string;
|
|
120
|
+
readonly crashCount: number;
|
|
121
|
+
readonly lastCrashAt: string | undefined;
|
|
122
|
+
readonly lastCrashReason: string | undefined;
|
|
123
|
+
readonly crashes: readonly CrashRecord[];
|
|
124
|
+
}
|
|
125
|
+
interface SpaceCrashSummary {
|
|
126
|
+
readonly scope: "space";
|
|
127
|
+
readonly selector: string;
|
|
128
|
+
readonly crashCount: number;
|
|
129
|
+
readonly lastCrashAt: string | undefined;
|
|
130
|
+
readonly apps: readonly AppCrashSummary[];
|
|
131
|
+
}
|
|
132
|
+
type CrashReportSummary = CrashSummary | SpaceCrashSummary;
|
|
133
|
+
type AuditEventScope = {
|
|
134
|
+
readonly kind: "app";
|
|
108
135
|
readonly appGuid: string;
|
|
136
|
+
} | {
|
|
137
|
+
readonly kind: "space";
|
|
138
|
+
readonly spaceGuid: string;
|
|
139
|
+
};
|
|
140
|
+
interface FetchAuditEventsInput {
|
|
141
|
+
readonly scope: AuditEventScope;
|
|
109
142
|
readonly types: readonly string[] | undefined;
|
|
110
143
|
readonly createdAfter: string | undefined;
|
|
111
144
|
readonly limit: number;
|
|
@@ -113,8 +146,11 @@ interface FetchAuditEventsInput {
|
|
|
113
146
|
|
|
114
147
|
/** Calls a Cloud Foundry v3 API path and returns the raw response body. */
|
|
115
148
|
type CurlFn = (path: string) => Promise<string>;
|
|
149
|
+
declare function mapAuditEvent(value: unknown): AuditEvent;
|
|
116
150
|
declare function buildAuditEventsPath(input: FetchAuditEventsInput, perPage: number): string;
|
|
117
|
-
|
|
151
|
+
declare function resolveOrganizationGuid(orgName: string, curl: CurlFn): Promise<string>;
|
|
152
|
+
declare function resolveSpaceGuid(spaceName: string, orgGuid: string, curl: CurlFn): Promise<string>;
|
|
153
|
+
/** Fetches audit events for an app or space, following pagination up to `input.limit`. */
|
|
118
154
|
declare function fetchAuditEvents(input: FetchAuditEventsInput, curl: CurlFn): Promise<readonly AuditEvent[]>;
|
|
119
155
|
declare function fetchApp(appGuid: string, curl: CurlFn): Promise<AppSummary>;
|
|
120
156
|
declare function fetchSshEnabled(appGuid: string, curl: CurlFn): Promise<SshEnabled>;
|
|
@@ -149,7 +185,7 @@ declare function filterByTypes(events: readonly AuditEvent[], types: readonly st
|
|
|
149
185
|
declare function sortEventsNewestFirst(events: readonly AuditEvent[]): readonly AuditEvent[];
|
|
150
186
|
/** Parses a human duration such as `30m`, `6h`, or `7d` into milliseconds. */
|
|
151
187
|
declare function parseDuration(raw: string): number;
|
|
152
|
-
/** Converts a duration into
|
|
188
|
+
/** Converts a duration into a CF audit timestamp suitable for `created_ats[gt]`. */
|
|
153
189
|
declare function durationToCreatedAfter(raw: string, now: Date): string;
|
|
154
190
|
/**
|
|
155
191
|
* Parses a comma-separated `--type` filter. Accepts the shorthands `ssh` and
|
|
@@ -158,6 +194,7 @@ declare function durationToCreatedAfter(raw: string, now: Date): string;
|
|
|
158
194
|
declare function parseTypeFilter(raw: string | undefined): readonly string[];
|
|
159
195
|
declare function toCrashRecord(event: AuditEvent): CrashRecord;
|
|
160
196
|
declare function summarizeCrashes(appName: string, events: readonly AuditEvent[]): CrashSummary;
|
|
197
|
+
declare function summarizeSpaceCrashes(selector: string, events: readonly AuditEvent[]): SpaceCrashSummary;
|
|
161
198
|
|
|
162
199
|
declare function describeEventType(type: string): string;
|
|
163
200
|
declare function formatUptime(seconds: number | undefined): string;
|
|
@@ -166,8 +203,10 @@ declare function formatCpu(cpu: number | undefined): string;
|
|
|
166
203
|
declare function formatRelativeTime(timestamp: string, now: Date): string;
|
|
167
204
|
declare function formatEventLine(event: AuditEvent, now: Date): string;
|
|
168
205
|
declare function formatEventsReport(appName: string, events: readonly AuditEvent[], now: Date): string;
|
|
206
|
+
declare function formatSpaceEventLine(event: AuditEvent, now: Date): string;
|
|
207
|
+
declare function formatSpaceEventsReport(selector: string, events: readonly AuditEvent[], now: Date): string;
|
|
169
208
|
declare function formatSshStatusReport(status: SshStatus, now: Date): string;
|
|
170
|
-
declare function formatCrashReport(summary:
|
|
209
|
+
declare function formatCrashReport(summary: CrashReportSummary, now: Date): string;
|
|
171
210
|
declare function formatStatusReport(health: AppHealth, now: Date): string;
|
|
172
211
|
|
|
173
212
|
/** Typed accessor for the Cloud Foundry v3 API used by the runtime. */
|
|
@@ -176,15 +215,18 @@ interface CfClient {
|
|
|
176
215
|
readonly fetchApp: (appGuid: string) => Promise<AppSummary>;
|
|
177
216
|
readonly fetchSshEnabled: (appGuid: string) => Promise<SshEnabled>;
|
|
178
217
|
readonly fetchWebProcessStats: (appGuid: string) => Promise<readonly ProcessInstanceStat[]>;
|
|
218
|
+
readonly resolveOrganizationGuid: (orgName: string) => Promise<string>;
|
|
219
|
+
readonly resolveSpaceGuid: (spaceName: string, orgGuid: string) => Promise<string>;
|
|
179
220
|
}
|
|
180
|
-
interface
|
|
181
|
-
readonly
|
|
221
|
+
interface CfTargetSession {
|
|
222
|
+
readonly selector: ResolvedSelector;
|
|
182
223
|
readonly client: CfClient;
|
|
224
|
+
readonly resolveAppGuid: (appName: string) => Promise<string>;
|
|
183
225
|
}
|
|
184
226
|
/** Injection seam so the orchestration can be unit-tested without a real CF. */
|
|
185
227
|
interface CfEventsDependencies {
|
|
186
228
|
readonly resolveSelector: (raw: string) => Promise<ResolvedSelector>;
|
|
187
|
-
readonly
|
|
229
|
+
readonly withCfTarget: <T>(selector: ResolvedSelector, credentials: CfCredentials, work: (session: CfTargetSession) => Promise<T>) => Promise<T>;
|
|
188
230
|
readonly now: () => Date;
|
|
189
231
|
}
|
|
190
232
|
interface EventQueryOptions {
|
|
@@ -208,7 +250,7 @@ declare class CfEventsRuntime {
|
|
|
208
250
|
constructor(deps?: CfEventsDependencies);
|
|
209
251
|
fetchEvents(raw: string, credentials: CfCredentials, options: EventQueryOptions): Promise<readonly AuditEvent[]>;
|
|
210
252
|
getSshStatus(raw: string, credentials: CfCredentials, since: string): Promise<SshStatus>;
|
|
211
|
-
getCrashes(raw: string, credentials: CfCredentials, options: CrashQueryOptions): Promise<
|
|
253
|
+
getCrashes(raw: string, credentials: CfCredentials, options: CrashQueryOptions): Promise<CrashReportSummary>;
|
|
212
254
|
getStatus(raw: string, credentials: CfCredentials): Promise<AppHealth>;
|
|
213
255
|
watchEvents(raw: string, credentials: CfCredentials, options: WatchOptions, onEvent: (event: AuditEvent) => void, signal: AbortSignal): Promise<void>;
|
|
214
256
|
private resolveCreatedAfter;
|
|
@@ -218,20 +260,26 @@ interface ParsedAppName {
|
|
|
218
260
|
readonly kind: "appName";
|
|
219
261
|
readonly appName: string;
|
|
220
262
|
}
|
|
221
|
-
interface
|
|
222
|
-
readonly kind: "
|
|
263
|
+
interface ParsedAppPath {
|
|
264
|
+
readonly kind: "appPath";
|
|
223
265
|
readonly regionKey: string;
|
|
224
266
|
readonly orgName: string;
|
|
225
267
|
readonly spaceName: string;
|
|
226
268
|
readonly appName: string;
|
|
227
269
|
}
|
|
228
|
-
|
|
270
|
+
interface ParsedSpacePath {
|
|
271
|
+
readonly kind: "spacePath";
|
|
272
|
+
readonly regionKey: string;
|
|
273
|
+
readonly orgName: string;
|
|
274
|
+
readonly spaceName: string;
|
|
275
|
+
}
|
|
276
|
+
type ParsedSelector = ParsedAppName | ParsedAppPath | ParsedSpacePath;
|
|
229
277
|
/** Parses a raw selector string into its structural parts without any I/O. */
|
|
230
278
|
declare function parseSelector(raw: string): ParsedSelector;
|
|
231
279
|
/**
|
|
232
280
|
* Resolves a raw selector.
|
|
233
281
|
* - Bare app name: based strictly on the CURRENT CF target (no global search).
|
|
234
|
-
* -
|
|
282
|
+
* - Explicit paths: use known region-to-api mapping; trust the provided org/space/app.
|
|
235
283
|
*/
|
|
236
284
|
declare function resolveSelector(raw: string): Promise<ResolvedSelector>;
|
|
237
285
|
|
|
@@ -246,4 +294,4 @@ interface SshStatusInput {
|
|
|
246
294
|
}
|
|
247
295
|
declare function buildSshStatus(input: SshStatusInput): SshStatus;
|
|
248
296
|
|
|
249
|
-
export { APP_EVENT_TYPES, type AppEventType, type AppHealth, type AppSummary, type AuditEvent,
|
|
297
|
+
export { APP_EVENT_TYPES, type AppCrashSummary, type AppEventType, type AppHealth, type AppSummary, type AuditEvent, type AuditEventScope, CRASH_EVENT_TYPES, type CfCliContext, type CfClient, type CfCredentials, type CfEntityRef, type CfEventsDependencies, CfEventsRuntime, type CfSessionInput, type CfTargetSession, type CrashQueryOptions, type CrashRecord, type CrashReportSummary, type CrashSummary, type CurlFn, type CurrentCfTarget, type EventQueryOptions, type FetchAuditEventsInput, type ParsedAppName, type ParsedAppPath, type ParsedSelector, type ParsedSpacePath, type ProcessInstanceStat, type RegionKey, type ResolvedAppSelector, type ResolvedSelector, type ResolvedSpaceSelector, SSH_EVENT_TYPES, type SpaceCrashSummary, type SshEnabled, type SshSession, type SshStatus, type SshStatusInput, type WatchOptions, buildAuditEventsPath, buildSshStatus, cfAppGuid, cfCurl, createDefaultDependencies, describeEventType, durationToCreatedAfter, fetchApp, fetchAuditEvents, fetchSshEnabled, fetchWebProcessStats, filterByTypes, formatBytes, formatCpu, formatCrashReport, formatEventLine, formatEventsReport, formatRelativeTime, formatSpaceEventLine, formatSpaceEventsReport, formatSshStatusReport, formatStatusReport, formatUptime, getApiEndpointForRegion, getRegionKeyForApi, inferSshSessions, isCrashEvent, isSshEvent, mapAuditEvent, parseCfTargetOutput, parseDuration, parseSelector, parseTypeFilter, prepareCfCliSession, readCurrentCfTarget, resolveOrganizationGuid, resolveSelector, resolveSpaceGuid, runCfCommand, sortEventsNewestFirst, summarizeCrashes, summarizeSpaceCrashes, toCrashRecord, withCfSession };
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,12 @@ function asRecord(value) {
|
|
|
18
18
|
function asArray(value) {
|
|
19
19
|
return Array.isArray(value) ? value : [];
|
|
20
20
|
}
|
|
21
|
+
function requireArray(value, message) {
|
|
22
|
+
if (!Array.isArray(value)) {
|
|
23
|
+
throw new Error(message);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
21
27
|
function asString(value) {
|
|
22
28
|
return typeof value === "string" ? value : "";
|
|
23
29
|
}
|
|
@@ -67,7 +73,7 @@ function mapProcessStat(value) {
|
|
|
67
73
|
}
|
|
68
74
|
function buildAuditEventsPath(input, perPage) {
|
|
69
75
|
const params = new URLSearchParams();
|
|
70
|
-
params
|
|
76
|
+
applyAuditEventScope(params, input.scope);
|
|
71
77
|
params.set("order_by", "-created_at");
|
|
72
78
|
params.set("per_page", perPage.toString());
|
|
73
79
|
if (input.types !== void 0 && input.types.length > 0) {
|
|
@@ -78,6 +84,60 @@ function buildAuditEventsPath(input, perPage) {
|
|
|
78
84
|
}
|
|
79
85
|
return `/v3/audit_events?${params.toString()}`;
|
|
80
86
|
}
|
|
87
|
+
function applyAuditEventScope(params, scope) {
|
|
88
|
+
if (scope.kind === "app") {
|
|
89
|
+
params.set("target_guids", scope.appGuid);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
params.set("space_guids", scope.spaceGuid);
|
|
93
|
+
}
|
|
94
|
+
async function resolveOrganizationGuid(orgName, curl) {
|
|
95
|
+
const params = new URLSearchParams();
|
|
96
|
+
params.set("names", orgName);
|
|
97
|
+
const body = asRecord(parseJson(await curl(`/v3/organizations?${params.toString()}`)));
|
|
98
|
+
const first = asRecord(asArray(body["resources"])[0]);
|
|
99
|
+
const guid = asString(first["guid"]);
|
|
100
|
+
if (guid.length === 0) {
|
|
101
|
+
throw new Error(`Could not resolve the GUID for organization "${orgName}".`);
|
|
102
|
+
}
|
|
103
|
+
return guid;
|
|
104
|
+
}
|
|
105
|
+
async function resolveSpaceGuid(spaceName, orgGuid, curl) {
|
|
106
|
+
const params = new URLSearchParams();
|
|
107
|
+
params.set("names", spaceName);
|
|
108
|
+
params.set("organization_guids", orgGuid);
|
|
109
|
+
const body = asRecord(parseJson(await curl(`/v3/spaces?${params.toString()}`)));
|
|
110
|
+
const first = asRecord(asArray(body["resources"])[0]);
|
|
111
|
+
const guid = asString(first["guid"]);
|
|
112
|
+
if (guid.length === 0) {
|
|
113
|
+
throw new Error(`Could not resolve the GUID for space "${spaceName}".`);
|
|
114
|
+
}
|
|
115
|
+
return guid;
|
|
116
|
+
}
|
|
117
|
+
function formatCfApiError(error) {
|
|
118
|
+
const record = asRecord(error);
|
|
119
|
+
const title = asString(record["title"]);
|
|
120
|
+
const detail = asString(record["detail"]);
|
|
121
|
+
if (title.length > 0 && detail.length > 0) {
|
|
122
|
+
return `${title}: ${detail}`;
|
|
123
|
+
}
|
|
124
|
+
if (title.length > 0) {
|
|
125
|
+
return title;
|
|
126
|
+
}
|
|
127
|
+
if (detail.length > 0) {
|
|
128
|
+
return detail;
|
|
129
|
+
}
|
|
130
|
+
const code = asNumber(record["code"]);
|
|
131
|
+
return code === void 0 ? "unknown CF API error" : `code ${code.toString()}`;
|
|
132
|
+
}
|
|
133
|
+
function throwIfCfApiErrors(body) {
|
|
134
|
+
const errors = asRecord(body)["errors"];
|
|
135
|
+
if (errors === void 0) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const details = requireArray(errors, "The CF API returned an invalid errors response.").map(formatCfApiError).filter((detail) => detail.length > 0);
|
|
139
|
+
throw new Error(`CF API error ${details.length > 0 ? details.join("; ") : "unknown CF API error"}`);
|
|
140
|
+
}
|
|
81
141
|
function nextPagePath(body) {
|
|
82
142
|
const next = asRecord(asRecord(asRecord(body)["pagination"])["next"]);
|
|
83
143
|
const href = next["href"];
|
|
@@ -98,7 +158,11 @@ async function fetchAuditEvents(input, curl) {
|
|
|
98
158
|
let path = buildAuditEventsPath(input, perPage);
|
|
99
159
|
while (path !== void 0 && events.length < limit) {
|
|
100
160
|
const body = parseJson(await curl(path));
|
|
101
|
-
|
|
161
|
+
throwIfCfApiErrors(body);
|
|
162
|
+
for (const resource of requireArray(
|
|
163
|
+
asRecord(body)["resources"],
|
|
164
|
+
"The CF API returned an invalid audit events response: missing resources array."
|
|
165
|
+
)) {
|
|
102
166
|
events.push(mapAuditEvent(resource));
|
|
103
167
|
if (events.length >= limit) {
|
|
104
168
|
break;
|
|
@@ -426,8 +490,11 @@ function parseDuration(raw) {
|
|
|
426
490
|
}
|
|
427
491
|
return amount * unitMs;
|
|
428
492
|
}
|
|
493
|
+
function toCfAuditTimestamp(date) {
|
|
494
|
+
return date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
495
|
+
}
|
|
429
496
|
function durationToCreatedAfter(raw, now) {
|
|
430
|
-
return new Date(now.getTime() - parseDuration(raw))
|
|
497
|
+
return toCfAuditTimestamp(new Date(now.getTime() - parseDuration(raw)));
|
|
431
498
|
}
|
|
432
499
|
function parseTypeFilter(raw) {
|
|
433
500
|
if (raw === void 0) {
|
|
@@ -475,6 +542,39 @@ function summarizeCrashes(appName, events) {
|
|
|
475
542
|
crashes
|
|
476
543
|
};
|
|
477
544
|
}
|
|
545
|
+
function crashAppName(event) {
|
|
546
|
+
if (event.target.name.length > 0) {
|
|
547
|
+
return event.target.name;
|
|
548
|
+
}
|
|
549
|
+
if (event.target.guid.length > 0) {
|
|
550
|
+
return event.target.guid;
|
|
551
|
+
}
|
|
552
|
+
return "(unknown app)";
|
|
553
|
+
}
|
|
554
|
+
function summarizeSpaceCrashes(selector, events) {
|
|
555
|
+
const byApp = /* @__PURE__ */ new Map();
|
|
556
|
+
for (const event of events.filter(isCrashEvent)) {
|
|
557
|
+
const appName = crashAppName(event);
|
|
558
|
+
byApp.set(appName, [...byApp.get(appName) ?? [], event]);
|
|
559
|
+
}
|
|
560
|
+
const apps = [...byApp.entries()].map(([appName, appEvents]) => summarizeCrashes(appName, appEvents)).map(({ appName, crashCount, lastCrashAt, lastCrashReason, crashes }) => ({
|
|
561
|
+
appName,
|
|
562
|
+
crashCount,
|
|
563
|
+
lastCrashAt,
|
|
564
|
+
lastCrashReason,
|
|
565
|
+
crashes
|
|
566
|
+
})).sort((left, right) => {
|
|
567
|
+
const byTime = (right.lastCrashAt ?? "").localeCompare(left.lastCrashAt ?? "");
|
|
568
|
+
return byTime === 0 ? left.appName.localeCompare(right.appName) : byTime;
|
|
569
|
+
});
|
|
570
|
+
return {
|
|
571
|
+
scope: "space",
|
|
572
|
+
selector,
|
|
573
|
+
crashCount: apps.reduce((sum, app) => sum + app.crashCount, 0),
|
|
574
|
+
lastCrashAt: apps[0]?.lastCrashAt,
|
|
575
|
+
apps
|
|
576
|
+
};
|
|
577
|
+
}
|
|
478
578
|
|
|
479
579
|
// src/format.ts
|
|
480
580
|
var EVENT_LABELS = {
|
|
@@ -575,6 +675,34 @@ function formatEventsReport(appName, events, now) {
|
|
|
575
675
|
...events.map((event) => formatEventLine(event, now))
|
|
576
676
|
].join("\n");
|
|
577
677
|
}
|
|
678
|
+
function eventTargetLabel(event) {
|
|
679
|
+
if (event.target.name.length > 0) {
|
|
680
|
+
return event.target.name;
|
|
681
|
+
}
|
|
682
|
+
if (event.target.type.length > 0) {
|
|
683
|
+
return event.target.type;
|
|
684
|
+
}
|
|
685
|
+
return event.target.guid.length > 0 ? event.target.guid : "(unknown target)";
|
|
686
|
+
}
|
|
687
|
+
function formatSpaceEventLine(event, now) {
|
|
688
|
+
const actor = event.actor.name.length > 0 ? event.actor.name : event.actor.type;
|
|
689
|
+
return [
|
|
690
|
+
` ${event.createdAt.padEnd(26)}`,
|
|
691
|
+
formatRelativeTime(event.createdAt, now).padEnd(11),
|
|
692
|
+
describeEventType(event.type).padEnd(24),
|
|
693
|
+
eventTargetLabel(event).padEnd(18),
|
|
694
|
+
actor.length > 0 ? actor : "(unknown actor)"
|
|
695
|
+
].join(" ");
|
|
696
|
+
}
|
|
697
|
+
function formatSpaceEventsReport(selector, events, now) {
|
|
698
|
+
if (events.length === 0) {
|
|
699
|
+
return `No audit events found for space ${selector}.`;
|
|
700
|
+
}
|
|
701
|
+
return [
|
|
702
|
+
`Audit events for space ${selector} (${events.length.toString()}):`,
|
|
703
|
+
...events.map((event) => formatSpaceEventLine(event, now))
|
|
704
|
+
].join("\n");
|
|
705
|
+
}
|
|
578
706
|
function formatSshStatusReport(status, now) {
|
|
579
707
|
const sessionLines = status.sessions.map((session) => {
|
|
580
708
|
const tag = session.likelyActive ? "[active]" : "[past] ";
|
|
@@ -599,7 +727,7 @@ function formatSshStatusReport(status, now) {
|
|
|
599
727
|
" are inferred from recent ssh-authorized audit events."
|
|
600
728
|
].join("\n");
|
|
601
729
|
}
|
|
602
|
-
function
|
|
730
|
+
function formatAppCrashReport(summary, now) {
|
|
603
731
|
if (summary.crashCount === 0) {
|
|
604
732
|
return `No crashes found for ${summary.appName}.`;
|
|
605
733
|
}
|
|
@@ -620,6 +748,35 @@ function formatCrashReport(summary, now) {
|
|
|
620
748
|
...crashLines
|
|
621
749
|
].join("\n");
|
|
622
750
|
}
|
|
751
|
+
function formatSpaceCrashReport(summary, now) {
|
|
752
|
+
if (summary.crashCount === 0) {
|
|
753
|
+
return `No crashes found for space ${summary.selector}.`;
|
|
754
|
+
}
|
|
755
|
+
const appLines = summary.apps.flatMap((app) => [
|
|
756
|
+
` ${app.appName} (${app.crashCount.toString()})`,
|
|
757
|
+
...app.crashes.map((crash) => {
|
|
758
|
+
const index = crash.index === void 0 ? "-" : `#${crash.index.toString()}`;
|
|
759
|
+
const reason = crash.reason ?? "unknown";
|
|
760
|
+
const exit = crash.exitStatus === void 0 ? "" : ` exit ${crash.exitStatus.toString()}`;
|
|
761
|
+
return ` ${crash.at} instance ${index} ${reason}${exit}`;
|
|
762
|
+
})
|
|
763
|
+
]);
|
|
764
|
+
return [
|
|
765
|
+
`Crash report for space ${summary.selector}`,
|
|
766
|
+
"",
|
|
767
|
+
` Crashes: ${summary.crashCount.toString()}`,
|
|
768
|
+
` Apps affected: ${summary.apps.length.toString()}`,
|
|
769
|
+
...summary.lastCrashAt === void 0 ? [] : [` Last crash: ${summary.lastCrashAt} (${formatRelativeTime(summary.lastCrashAt, now)})`],
|
|
770
|
+
"",
|
|
771
|
+
...appLines
|
|
772
|
+
].join("\n");
|
|
773
|
+
}
|
|
774
|
+
function formatCrashReport(summary, now) {
|
|
775
|
+
if ("scope" in summary) {
|
|
776
|
+
return formatSpaceCrashReport(summary, now);
|
|
777
|
+
}
|
|
778
|
+
return formatAppCrashReport(summary, now);
|
|
779
|
+
}
|
|
623
780
|
function formatInstanceLine(instance) {
|
|
624
781
|
return [
|
|
625
782
|
` #${instance.index.toString()}`.padEnd(6),
|
|
@@ -648,7 +805,12 @@ function formatStatusReport(health, now) {
|
|
|
648
805
|
import { setTimeout as delayPromise } from "timers/promises";
|
|
649
806
|
|
|
650
807
|
// src/selector.ts
|
|
651
|
-
var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
|
|
808
|
+
var SELECTOR_USAGE = 'use "region/org/space", "region/org/space/app", or a bare app name';
|
|
809
|
+
function assertNonEmpty(raw, parts, shape) {
|
|
810
|
+
if (parts.some((part) => part.length === 0)) {
|
|
811
|
+
throw new Error(`Invalid selector "${raw}": every ${shape} segment must be non-empty.`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
652
814
|
function parseSelector(raw) {
|
|
653
815
|
const trimmed = raw.trim();
|
|
654
816
|
if (trimmed.length === 0) {
|
|
@@ -662,12 +824,21 @@ function parseSelector(raw) {
|
|
|
662
824
|
}
|
|
663
825
|
return { kind: "appName", appName };
|
|
664
826
|
}
|
|
827
|
+
if (parts.length === 3) {
|
|
828
|
+
assertNonEmpty(raw, parts, "region/org/space");
|
|
829
|
+
const [regionKey, orgName, spaceName] = parts;
|
|
830
|
+
return { kind: "spacePath", regionKey: regionKey ?? "", orgName: orgName ?? "", spaceName: spaceName ?? "" };
|
|
831
|
+
}
|
|
665
832
|
if (parts.length === 4) {
|
|
833
|
+
assertNonEmpty(raw, parts, "region/org/space/app");
|
|
666
834
|
const [regionKey, orgName, spaceName, appName] = parts;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
835
|
+
return {
|
|
836
|
+
kind: "appPath",
|
|
837
|
+
regionKey: regionKey ?? "",
|
|
838
|
+
orgName: orgName ?? "",
|
|
839
|
+
spaceName: spaceName ?? "",
|
|
840
|
+
appName: appName ?? ""
|
|
841
|
+
};
|
|
671
842
|
}
|
|
672
843
|
throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
|
|
673
844
|
}
|
|
@@ -681,6 +852,7 @@ async function resolveSelector(raw) {
|
|
|
681
852
|
);
|
|
682
853
|
}
|
|
683
854
|
return {
|
|
855
|
+
kind: "app",
|
|
684
856
|
raw,
|
|
685
857
|
regionKey: current.regionKey ?? "",
|
|
686
858
|
apiEndpoint: current.apiEndpoint,
|
|
@@ -695,7 +867,18 @@ async function resolveSelector(raw) {
|
|
|
695
867
|
`Unknown region "${parsed.regionKey}". Use a bare app name (requires current CF target) or a known region key.`
|
|
696
868
|
);
|
|
697
869
|
}
|
|
870
|
+
if (parsed.kind === "spacePath") {
|
|
871
|
+
return {
|
|
872
|
+
kind: "space",
|
|
873
|
+
raw,
|
|
874
|
+
regionKey: parsed.regionKey,
|
|
875
|
+
apiEndpoint,
|
|
876
|
+
orgName: parsed.orgName,
|
|
877
|
+
spaceName: parsed.spaceName
|
|
878
|
+
};
|
|
879
|
+
}
|
|
698
880
|
return {
|
|
881
|
+
kind: "app",
|
|
699
882
|
raw,
|
|
700
883
|
regionKey: parsed.regionKey,
|
|
701
884
|
apiEndpoint,
|
|
@@ -746,10 +929,12 @@ function createCfClient(curl) {
|
|
|
746
929
|
fetchAuditEvents: (input) => fetchAuditEvents(input, curl),
|
|
747
930
|
fetchApp: (appGuid) => fetchApp(appGuid, curl),
|
|
748
931
|
fetchSshEnabled: (appGuid) => fetchSshEnabled(appGuid, curl),
|
|
749
|
-
fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl)
|
|
932
|
+
fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl),
|
|
933
|
+
resolveOrganizationGuid: (orgName) => resolveOrganizationGuid(orgName, curl),
|
|
934
|
+
resolveSpaceGuid: (spaceName, orgGuid) => resolveSpaceGuid(spaceName, orgGuid, curl)
|
|
750
935
|
};
|
|
751
936
|
}
|
|
752
|
-
async function
|
|
937
|
+
async function defaultWithCfTarget(selector, credentials, work) {
|
|
753
938
|
return await withCfSession(async (ctx) => {
|
|
754
939
|
await prepareCfCliSession(
|
|
755
940
|
{
|
|
@@ -760,15 +945,14 @@ async function defaultWithCfApp(selector, credentials, work) {
|
|
|
760
945
|
},
|
|
761
946
|
ctx
|
|
762
947
|
);
|
|
763
|
-
const appGuid = await cfAppGuid(selector.appName, ctx);
|
|
764
948
|
const client = createCfClient((path) => cfCurl(path, ctx));
|
|
765
|
-
return await work({
|
|
949
|
+
return await work({ selector, client, resolveAppGuid: (appName) => cfAppGuid(appName, ctx) });
|
|
766
950
|
});
|
|
767
951
|
}
|
|
768
952
|
function createDefaultDependencies() {
|
|
769
953
|
return {
|
|
770
954
|
resolveSelector,
|
|
771
|
-
|
|
955
|
+
withCfTarget: defaultWithCfTarget,
|
|
772
956
|
now: () => /* @__PURE__ */ new Date()
|
|
773
957
|
};
|
|
774
958
|
}
|
|
@@ -778,6 +962,24 @@ async function delay(ms, signal) {
|
|
|
778
962
|
} catch {
|
|
779
963
|
}
|
|
780
964
|
}
|
|
965
|
+
function requireAppSelector(selector, command) {
|
|
966
|
+
if (selector.kind === "app") {
|
|
967
|
+
return selector;
|
|
968
|
+
}
|
|
969
|
+
throw new Error(
|
|
970
|
+
`The ${command} command requires an app selector. Use region/org/space/app or a bare app name.`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
async function resolveAuditScope(session) {
|
|
974
|
+
if (session.selector.kind === "app") {
|
|
975
|
+
return { kind: "app", appGuid: await session.resolveAppGuid(session.selector.appName) };
|
|
976
|
+
}
|
|
977
|
+
const orgGuid = await session.client.resolveOrganizationGuid(session.selector.orgName);
|
|
978
|
+
return {
|
|
979
|
+
kind: "space",
|
|
980
|
+
spaceGuid: await session.client.resolveSpaceGuid(session.selector.spaceName, orgGuid)
|
|
981
|
+
};
|
|
982
|
+
}
|
|
781
983
|
var CfEventsRuntime = class {
|
|
782
984
|
deps;
|
|
783
985
|
constructor(deps = createDefaultDependencies()) {
|
|
@@ -785,9 +987,9 @@ var CfEventsRuntime = class {
|
|
|
785
987
|
}
|
|
786
988
|
async fetchEvents(raw, credentials, options) {
|
|
787
989
|
const selector = await this.deps.resolveSelector(raw);
|
|
788
|
-
return await this.deps.
|
|
789
|
-
return await client.fetchAuditEvents({
|
|
790
|
-
|
|
990
|
+
return await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
991
|
+
return await session.client.fetchAuditEvents({
|
|
992
|
+
scope: await resolveAuditScope(session),
|
|
791
993
|
types: options.types.length > 0 ? options.types : void 0,
|
|
792
994
|
createdAfter: this.resolveCreatedAfter(options.since),
|
|
793
995
|
limit: options.limit
|
|
@@ -795,12 +997,13 @@ var CfEventsRuntime = class {
|
|
|
795
997
|
});
|
|
796
998
|
}
|
|
797
999
|
async getSshStatus(raw, credentials, since) {
|
|
798
|
-
const selector = await this.deps.resolveSelector(raw);
|
|
799
|
-
return await this.deps.
|
|
1000
|
+
const selector = requireAppSelector(await this.deps.resolveSelector(raw), "ssh-status");
|
|
1001
|
+
return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
|
|
1002
|
+
const appGuid = await resolveAppGuid(selector.appName);
|
|
800
1003
|
const [sshEnabled, events] = await Promise.all([
|
|
801
1004
|
client.fetchSshEnabled(appGuid),
|
|
802
1005
|
client.fetchAuditEvents({
|
|
803
|
-
appGuid,
|
|
1006
|
+
scope: { kind: "app", appGuid },
|
|
804
1007
|
types: [...SSH_EVENT_TYPES],
|
|
805
1008
|
createdAfter: durationToCreatedAfter(since, this.deps.now()),
|
|
806
1009
|
limit: SSH_STATUS_EVENT_LIMIT
|
|
@@ -817,24 +1020,28 @@ var CfEventsRuntime = class {
|
|
|
817
1020
|
}
|
|
818
1021
|
async getCrashes(raw, credentials, options) {
|
|
819
1022
|
const selector = await this.deps.resolveSelector(raw);
|
|
820
|
-
return await this.deps.
|
|
821
|
-
const events = await client.fetchAuditEvents({
|
|
822
|
-
|
|
1023
|
+
return await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
1024
|
+
const events = await session.client.fetchAuditEvents({
|
|
1025
|
+
scope: await resolveAuditScope(session),
|
|
823
1026
|
types: [...CRASH_EVENT_TYPES],
|
|
824
1027
|
createdAfter: this.resolveCreatedAfter(options.since),
|
|
825
1028
|
limit: options.limit
|
|
826
1029
|
});
|
|
827
|
-
|
|
1030
|
+
if (selector.kind === "app") {
|
|
1031
|
+
return summarizeCrashes(selector.appName, events);
|
|
1032
|
+
}
|
|
1033
|
+
return summarizeSpaceCrashes(`${selector.regionKey}/${selector.orgName}/${selector.spaceName}`, events);
|
|
828
1034
|
});
|
|
829
1035
|
}
|
|
830
1036
|
async getStatus(raw, credentials) {
|
|
831
|
-
const selector = await this.deps.resolveSelector(raw);
|
|
832
|
-
return await this.deps.
|
|
1037
|
+
const selector = requireAppSelector(await this.deps.resolveSelector(raw), "status");
|
|
1038
|
+
return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
|
|
1039
|
+
const appGuid = await resolveAppGuid(selector.appName);
|
|
833
1040
|
const [app, instances, sshEnabled, recent] = await Promise.all([
|
|
834
1041
|
client.fetchApp(appGuid),
|
|
835
1042
|
client.fetchWebProcessStats(appGuid),
|
|
836
1043
|
client.fetchSshEnabled(appGuid),
|
|
837
|
-
client.fetchAuditEvents({ appGuid, types: void 0, createdAfter: void 0, limit: 1 })
|
|
1044
|
+
client.fetchAuditEvents({ scope: { kind: "app", appGuid }, types: void 0, createdAfter: void 0, limit: 1 })
|
|
838
1045
|
]);
|
|
839
1046
|
return {
|
|
840
1047
|
appName: selector.appName,
|
|
@@ -848,12 +1055,13 @@ var CfEventsRuntime = class {
|
|
|
848
1055
|
}
|
|
849
1056
|
async watchEvents(raw, credentials, options, onEvent, signal) {
|
|
850
1057
|
const selector = await this.deps.resolveSelector(raw);
|
|
851
|
-
await this.deps.
|
|
1058
|
+
await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
1059
|
+
const scope = await resolveAuditScope(session);
|
|
852
1060
|
const seen = /* @__PURE__ */ new Set();
|
|
853
1061
|
let cursor = durationToCreatedAfter(options.lookback, this.deps.now());
|
|
854
1062
|
while (!signal.aborted) {
|
|
855
|
-
const events = await client.fetchAuditEvents({
|
|
856
|
-
|
|
1063
|
+
const events = await session.client.fetchAuditEvents({
|
|
1064
|
+
scope,
|
|
857
1065
|
types: options.types.length > 0 ? options.types : void 0,
|
|
858
1066
|
createdAfter: cursor,
|
|
859
1067
|
limit: WATCH_FETCH_LIMIT
|
|
@@ -903,6 +1111,8 @@ export {
|
|
|
903
1111
|
formatEventLine,
|
|
904
1112
|
formatEventsReport,
|
|
905
1113
|
formatRelativeTime,
|
|
1114
|
+
formatSpaceEventLine,
|
|
1115
|
+
formatSpaceEventsReport,
|
|
906
1116
|
formatSshStatusReport,
|
|
907
1117
|
formatStatusReport,
|
|
908
1118
|
formatUptime,
|
|
@@ -911,16 +1121,20 @@ export {
|
|
|
911
1121
|
inferSshSessions,
|
|
912
1122
|
isCrashEvent,
|
|
913
1123
|
isSshEvent,
|
|
1124
|
+
mapAuditEvent,
|
|
914
1125
|
parseCfTargetOutput,
|
|
915
1126
|
parseDuration,
|
|
916
1127
|
parseSelector,
|
|
917
1128
|
parseTypeFilter,
|
|
918
1129
|
prepareCfCliSession,
|
|
919
1130
|
readCurrentCfTarget,
|
|
1131
|
+
resolveOrganizationGuid,
|
|
920
1132
|
resolveSelector,
|
|
1133
|
+
resolveSpaceGuid,
|
|
921
1134
|
runCfCommand,
|
|
922
1135
|
sortEventsNewestFirst,
|
|
923
1136
|
summarizeCrashes,
|
|
1137
|
+
summarizeSpaceCrashes,
|
|
924
1138
|
toCrashRecord,
|
|
925
1139
|
withCfSession
|
|
926
1140
|
};
|