@saptools/cf-events 0.1.4 → 0.2.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/README.md +27 -29
- package/dist/cli.js +214 -35
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +62 -14
- package/dist/index.js +205 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -67,7 +67,7 @@ function mapProcessStat(value) {
|
|
|
67
67
|
}
|
|
68
68
|
function buildAuditEventsPath(input, perPage) {
|
|
69
69
|
const params = new URLSearchParams();
|
|
70
|
-
params
|
|
70
|
+
applyAuditEventScope(params, input.scope);
|
|
71
71
|
params.set("order_by", "-created_at");
|
|
72
72
|
params.set("per_page", perPage.toString());
|
|
73
73
|
if (input.types !== void 0 && input.types.length > 0) {
|
|
@@ -78,6 +78,36 @@ function buildAuditEventsPath(input, perPage) {
|
|
|
78
78
|
}
|
|
79
79
|
return `/v3/audit_events?${params.toString()}`;
|
|
80
80
|
}
|
|
81
|
+
function applyAuditEventScope(params, scope) {
|
|
82
|
+
if (scope.kind === "app") {
|
|
83
|
+
params.set("target_guids", scope.appGuid);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
params.set("space_guids", scope.spaceGuid);
|
|
87
|
+
}
|
|
88
|
+
async function resolveOrganizationGuid(orgName, curl) {
|
|
89
|
+
const params = new URLSearchParams();
|
|
90
|
+
params.set("names", orgName);
|
|
91
|
+
const body = asRecord(parseJson(await curl(`/v3/organizations?${params.toString()}`)));
|
|
92
|
+
const first = asRecord(asArray(body["resources"])[0]);
|
|
93
|
+
const guid = asString(first["guid"]);
|
|
94
|
+
if (guid.length === 0) {
|
|
95
|
+
throw new Error(`Could not resolve the GUID for organization "${orgName}".`);
|
|
96
|
+
}
|
|
97
|
+
return guid;
|
|
98
|
+
}
|
|
99
|
+
async function resolveSpaceGuid(spaceName, orgGuid, curl) {
|
|
100
|
+
const params = new URLSearchParams();
|
|
101
|
+
params.set("names", spaceName);
|
|
102
|
+
params.set("organization_guids", orgGuid);
|
|
103
|
+
const body = asRecord(parseJson(await curl(`/v3/spaces?${params.toString()}`)));
|
|
104
|
+
const first = asRecord(asArray(body["resources"])[0]);
|
|
105
|
+
const guid = asString(first["guid"]);
|
|
106
|
+
if (guid.length === 0) {
|
|
107
|
+
throw new Error(`Could not resolve the GUID for space "${spaceName}".`);
|
|
108
|
+
}
|
|
109
|
+
return guid;
|
|
110
|
+
}
|
|
81
111
|
function nextPagePath(body) {
|
|
82
112
|
const next = asRecord(asRecord(asRecord(body)["pagination"])["next"]);
|
|
83
113
|
const href = next["href"];
|
|
@@ -475,6 +505,39 @@ function summarizeCrashes(appName, events) {
|
|
|
475
505
|
crashes
|
|
476
506
|
};
|
|
477
507
|
}
|
|
508
|
+
function crashAppName(event) {
|
|
509
|
+
if (event.target.name.length > 0) {
|
|
510
|
+
return event.target.name;
|
|
511
|
+
}
|
|
512
|
+
if (event.target.guid.length > 0) {
|
|
513
|
+
return event.target.guid;
|
|
514
|
+
}
|
|
515
|
+
return "(unknown app)";
|
|
516
|
+
}
|
|
517
|
+
function summarizeSpaceCrashes(selector, events) {
|
|
518
|
+
const byApp = /* @__PURE__ */ new Map();
|
|
519
|
+
for (const event of events.filter(isCrashEvent)) {
|
|
520
|
+
const appName = crashAppName(event);
|
|
521
|
+
byApp.set(appName, [...byApp.get(appName) ?? [], event]);
|
|
522
|
+
}
|
|
523
|
+
const apps = [...byApp.entries()].map(([appName, appEvents]) => summarizeCrashes(appName, appEvents)).map(({ appName, crashCount, lastCrashAt, lastCrashReason, crashes }) => ({
|
|
524
|
+
appName,
|
|
525
|
+
crashCount,
|
|
526
|
+
lastCrashAt,
|
|
527
|
+
lastCrashReason,
|
|
528
|
+
crashes
|
|
529
|
+
})).sort((left, right) => {
|
|
530
|
+
const byTime = (right.lastCrashAt ?? "").localeCompare(left.lastCrashAt ?? "");
|
|
531
|
+
return byTime === 0 ? left.appName.localeCompare(right.appName) : byTime;
|
|
532
|
+
});
|
|
533
|
+
return {
|
|
534
|
+
scope: "space",
|
|
535
|
+
selector,
|
|
536
|
+
crashCount: apps.reduce((sum, app) => sum + app.crashCount, 0),
|
|
537
|
+
lastCrashAt: apps[0]?.lastCrashAt,
|
|
538
|
+
apps
|
|
539
|
+
};
|
|
540
|
+
}
|
|
478
541
|
|
|
479
542
|
// src/format.ts
|
|
480
543
|
var EVENT_LABELS = {
|
|
@@ -575,6 +638,34 @@ function formatEventsReport(appName, events, now) {
|
|
|
575
638
|
...events.map((event) => formatEventLine(event, now))
|
|
576
639
|
].join("\n");
|
|
577
640
|
}
|
|
641
|
+
function eventTargetLabel(event) {
|
|
642
|
+
if (event.target.name.length > 0) {
|
|
643
|
+
return event.target.name;
|
|
644
|
+
}
|
|
645
|
+
if (event.target.type.length > 0) {
|
|
646
|
+
return event.target.type;
|
|
647
|
+
}
|
|
648
|
+
return event.target.guid.length > 0 ? event.target.guid : "(unknown target)";
|
|
649
|
+
}
|
|
650
|
+
function formatSpaceEventLine(event, now) {
|
|
651
|
+
const actor = event.actor.name.length > 0 ? event.actor.name : event.actor.type;
|
|
652
|
+
return [
|
|
653
|
+
` ${event.createdAt.padEnd(26)}`,
|
|
654
|
+
formatRelativeTime(event.createdAt, now).padEnd(11),
|
|
655
|
+
describeEventType(event.type).padEnd(24),
|
|
656
|
+
eventTargetLabel(event).padEnd(18),
|
|
657
|
+
actor.length > 0 ? actor : "(unknown actor)"
|
|
658
|
+
].join(" ");
|
|
659
|
+
}
|
|
660
|
+
function formatSpaceEventsReport(selector, events, now) {
|
|
661
|
+
if (events.length === 0) {
|
|
662
|
+
return `No audit events found for space ${selector}.`;
|
|
663
|
+
}
|
|
664
|
+
return [
|
|
665
|
+
`Audit events for space ${selector} (${events.length.toString()}):`,
|
|
666
|
+
...events.map((event) => formatSpaceEventLine(event, now))
|
|
667
|
+
].join("\n");
|
|
668
|
+
}
|
|
578
669
|
function formatSshStatusReport(status, now) {
|
|
579
670
|
const sessionLines = status.sessions.map((session) => {
|
|
580
671
|
const tag = session.likelyActive ? "[active]" : "[past] ";
|
|
@@ -599,7 +690,7 @@ function formatSshStatusReport(status, now) {
|
|
|
599
690
|
" are inferred from recent ssh-authorized audit events."
|
|
600
691
|
].join("\n");
|
|
601
692
|
}
|
|
602
|
-
function
|
|
693
|
+
function formatAppCrashReport(summary, now) {
|
|
603
694
|
if (summary.crashCount === 0) {
|
|
604
695
|
return `No crashes found for ${summary.appName}.`;
|
|
605
696
|
}
|
|
@@ -620,6 +711,35 @@ function formatCrashReport(summary, now) {
|
|
|
620
711
|
...crashLines
|
|
621
712
|
].join("\n");
|
|
622
713
|
}
|
|
714
|
+
function formatSpaceCrashReport(summary, now) {
|
|
715
|
+
if (summary.crashCount === 0) {
|
|
716
|
+
return `No crashes found for space ${summary.selector}.`;
|
|
717
|
+
}
|
|
718
|
+
const appLines = summary.apps.flatMap((app) => [
|
|
719
|
+
` ${app.appName} (${app.crashCount.toString()})`,
|
|
720
|
+
...app.crashes.map((crash) => {
|
|
721
|
+
const index = crash.index === void 0 ? "-" : `#${crash.index.toString()}`;
|
|
722
|
+
const reason = crash.reason ?? "unknown";
|
|
723
|
+
const exit = crash.exitStatus === void 0 ? "" : ` exit ${crash.exitStatus.toString()}`;
|
|
724
|
+
return ` ${crash.at} instance ${index} ${reason}${exit}`;
|
|
725
|
+
})
|
|
726
|
+
]);
|
|
727
|
+
return [
|
|
728
|
+
`Crash report for space ${summary.selector}`,
|
|
729
|
+
"",
|
|
730
|
+
` Crashes: ${summary.crashCount.toString()}`,
|
|
731
|
+
` Apps affected: ${summary.apps.length.toString()}`,
|
|
732
|
+
...summary.lastCrashAt === void 0 ? [] : [` Last crash: ${summary.lastCrashAt} (${formatRelativeTime(summary.lastCrashAt, now)})`],
|
|
733
|
+
"",
|
|
734
|
+
...appLines
|
|
735
|
+
].join("\n");
|
|
736
|
+
}
|
|
737
|
+
function formatCrashReport(summary, now) {
|
|
738
|
+
if ("scope" in summary) {
|
|
739
|
+
return formatSpaceCrashReport(summary, now);
|
|
740
|
+
}
|
|
741
|
+
return formatAppCrashReport(summary, now);
|
|
742
|
+
}
|
|
623
743
|
function formatInstanceLine(instance) {
|
|
624
744
|
return [
|
|
625
745
|
` #${instance.index.toString()}`.padEnd(6),
|
|
@@ -648,7 +768,12 @@ function formatStatusReport(health, now) {
|
|
|
648
768
|
import { setTimeout as delayPromise } from "timers/promises";
|
|
649
769
|
|
|
650
770
|
// src/selector.ts
|
|
651
|
-
var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
|
|
771
|
+
var SELECTOR_USAGE = 'use "region/org/space", "region/org/space/app", or a bare app name';
|
|
772
|
+
function assertNonEmpty(raw, parts, shape) {
|
|
773
|
+
if (parts.some((part) => part.length === 0)) {
|
|
774
|
+
throw new Error(`Invalid selector "${raw}": every ${shape} segment must be non-empty.`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
652
777
|
function parseSelector(raw) {
|
|
653
778
|
const trimmed = raw.trim();
|
|
654
779
|
if (trimmed.length === 0) {
|
|
@@ -662,12 +787,21 @@ function parseSelector(raw) {
|
|
|
662
787
|
}
|
|
663
788
|
return { kind: "appName", appName };
|
|
664
789
|
}
|
|
790
|
+
if (parts.length === 3) {
|
|
791
|
+
assertNonEmpty(raw, parts, "region/org/space");
|
|
792
|
+
const [regionKey, orgName, spaceName] = parts;
|
|
793
|
+
return { kind: "spacePath", regionKey: regionKey ?? "", orgName: orgName ?? "", spaceName: spaceName ?? "" };
|
|
794
|
+
}
|
|
665
795
|
if (parts.length === 4) {
|
|
796
|
+
assertNonEmpty(raw, parts, "region/org/space/app");
|
|
666
797
|
const [regionKey, orgName, spaceName, appName] = parts;
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
798
|
+
return {
|
|
799
|
+
kind: "appPath",
|
|
800
|
+
regionKey: regionKey ?? "",
|
|
801
|
+
orgName: orgName ?? "",
|
|
802
|
+
spaceName: spaceName ?? "",
|
|
803
|
+
appName: appName ?? ""
|
|
804
|
+
};
|
|
671
805
|
}
|
|
672
806
|
throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
|
|
673
807
|
}
|
|
@@ -681,6 +815,7 @@ async function resolveSelector(raw) {
|
|
|
681
815
|
);
|
|
682
816
|
}
|
|
683
817
|
return {
|
|
818
|
+
kind: "app",
|
|
684
819
|
raw,
|
|
685
820
|
regionKey: current.regionKey ?? "",
|
|
686
821
|
apiEndpoint: current.apiEndpoint,
|
|
@@ -695,7 +830,18 @@ async function resolveSelector(raw) {
|
|
|
695
830
|
`Unknown region "${parsed.regionKey}". Use a bare app name (requires current CF target) or a known region key.`
|
|
696
831
|
);
|
|
697
832
|
}
|
|
833
|
+
if (parsed.kind === "spacePath") {
|
|
834
|
+
return {
|
|
835
|
+
kind: "space",
|
|
836
|
+
raw,
|
|
837
|
+
regionKey: parsed.regionKey,
|
|
838
|
+
apiEndpoint,
|
|
839
|
+
orgName: parsed.orgName,
|
|
840
|
+
spaceName: parsed.spaceName
|
|
841
|
+
};
|
|
842
|
+
}
|
|
698
843
|
return {
|
|
844
|
+
kind: "app",
|
|
699
845
|
raw,
|
|
700
846
|
regionKey: parsed.regionKey,
|
|
701
847
|
apiEndpoint,
|
|
@@ -746,10 +892,12 @@ function createCfClient(curl) {
|
|
|
746
892
|
fetchAuditEvents: (input) => fetchAuditEvents(input, curl),
|
|
747
893
|
fetchApp: (appGuid) => fetchApp(appGuid, curl),
|
|
748
894
|
fetchSshEnabled: (appGuid) => fetchSshEnabled(appGuid, curl),
|
|
749
|
-
fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl)
|
|
895
|
+
fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl),
|
|
896
|
+
resolveOrganizationGuid: (orgName) => resolveOrganizationGuid(orgName, curl),
|
|
897
|
+
resolveSpaceGuid: (spaceName, orgGuid) => resolveSpaceGuid(spaceName, orgGuid, curl)
|
|
750
898
|
};
|
|
751
899
|
}
|
|
752
|
-
async function
|
|
900
|
+
async function defaultWithCfTarget(selector, credentials, work) {
|
|
753
901
|
return await withCfSession(async (ctx) => {
|
|
754
902
|
await prepareCfCliSession(
|
|
755
903
|
{
|
|
@@ -760,15 +908,14 @@ async function defaultWithCfApp(selector, credentials, work) {
|
|
|
760
908
|
},
|
|
761
909
|
ctx
|
|
762
910
|
);
|
|
763
|
-
const appGuid = await cfAppGuid(selector.appName, ctx);
|
|
764
911
|
const client = createCfClient((path) => cfCurl(path, ctx));
|
|
765
|
-
return await work({
|
|
912
|
+
return await work({ selector, client, resolveAppGuid: (appName) => cfAppGuid(appName, ctx) });
|
|
766
913
|
});
|
|
767
914
|
}
|
|
768
915
|
function createDefaultDependencies() {
|
|
769
916
|
return {
|
|
770
917
|
resolveSelector,
|
|
771
|
-
|
|
918
|
+
withCfTarget: defaultWithCfTarget,
|
|
772
919
|
now: () => /* @__PURE__ */ new Date()
|
|
773
920
|
};
|
|
774
921
|
}
|
|
@@ -778,6 +925,24 @@ async function delay(ms, signal) {
|
|
|
778
925
|
} catch {
|
|
779
926
|
}
|
|
780
927
|
}
|
|
928
|
+
function requireAppSelector(selector, command) {
|
|
929
|
+
if (selector.kind === "app") {
|
|
930
|
+
return selector;
|
|
931
|
+
}
|
|
932
|
+
throw new Error(
|
|
933
|
+
`The ${command} command requires an app selector. Use region/org/space/app or a bare app name.`
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
async function resolveAuditScope(session) {
|
|
937
|
+
if (session.selector.kind === "app") {
|
|
938
|
+
return { kind: "app", appGuid: await session.resolveAppGuid(session.selector.appName) };
|
|
939
|
+
}
|
|
940
|
+
const orgGuid = await session.client.resolveOrganizationGuid(session.selector.orgName);
|
|
941
|
+
return {
|
|
942
|
+
kind: "space",
|
|
943
|
+
spaceGuid: await session.client.resolveSpaceGuid(session.selector.spaceName, orgGuid)
|
|
944
|
+
};
|
|
945
|
+
}
|
|
781
946
|
var CfEventsRuntime = class {
|
|
782
947
|
deps;
|
|
783
948
|
constructor(deps = createDefaultDependencies()) {
|
|
@@ -785,9 +950,9 @@ var CfEventsRuntime = class {
|
|
|
785
950
|
}
|
|
786
951
|
async fetchEvents(raw, credentials, options) {
|
|
787
952
|
const selector = await this.deps.resolveSelector(raw);
|
|
788
|
-
return await this.deps.
|
|
789
|
-
return await client.fetchAuditEvents({
|
|
790
|
-
|
|
953
|
+
return await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
954
|
+
return await session.client.fetchAuditEvents({
|
|
955
|
+
scope: await resolveAuditScope(session),
|
|
791
956
|
types: options.types.length > 0 ? options.types : void 0,
|
|
792
957
|
createdAfter: this.resolveCreatedAfter(options.since),
|
|
793
958
|
limit: options.limit
|
|
@@ -795,12 +960,13 @@ var CfEventsRuntime = class {
|
|
|
795
960
|
});
|
|
796
961
|
}
|
|
797
962
|
async getSshStatus(raw, credentials, since) {
|
|
798
|
-
const selector = await this.deps.resolveSelector(raw);
|
|
799
|
-
return await this.deps.
|
|
963
|
+
const selector = requireAppSelector(await this.deps.resolveSelector(raw), "ssh-status");
|
|
964
|
+
return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
|
|
965
|
+
const appGuid = await resolveAppGuid(selector.appName);
|
|
800
966
|
const [sshEnabled, events] = await Promise.all([
|
|
801
967
|
client.fetchSshEnabled(appGuid),
|
|
802
968
|
client.fetchAuditEvents({
|
|
803
|
-
appGuid,
|
|
969
|
+
scope: { kind: "app", appGuid },
|
|
804
970
|
types: [...SSH_EVENT_TYPES],
|
|
805
971
|
createdAfter: durationToCreatedAfter(since, this.deps.now()),
|
|
806
972
|
limit: SSH_STATUS_EVENT_LIMIT
|
|
@@ -817,24 +983,28 @@ var CfEventsRuntime = class {
|
|
|
817
983
|
}
|
|
818
984
|
async getCrashes(raw, credentials, options) {
|
|
819
985
|
const selector = await this.deps.resolveSelector(raw);
|
|
820
|
-
return await this.deps.
|
|
821
|
-
const events = await client.fetchAuditEvents({
|
|
822
|
-
|
|
986
|
+
return await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
987
|
+
const events = await session.client.fetchAuditEvents({
|
|
988
|
+
scope: await resolveAuditScope(session),
|
|
823
989
|
types: [...CRASH_EVENT_TYPES],
|
|
824
990
|
createdAfter: this.resolveCreatedAfter(options.since),
|
|
825
991
|
limit: options.limit
|
|
826
992
|
});
|
|
827
|
-
|
|
993
|
+
if (selector.kind === "app") {
|
|
994
|
+
return summarizeCrashes(selector.appName, events);
|
|
995
|
+
}
|
|
996
|
+
return summarizeSpaceCrashes(`${selector.regionKey}/${selector.orgName}/${selector.spaceName}`, events);
|
|
828
997
|
});
|
|
829
998
|
}
|
|
830
999
|
async getStatus(raw, credentials) {
|
|
831
|
-
const selector = await this.deps.resolveSelector(raw);
|
|
832
|
-
return await this.deps.
|
|
1000
|
+
const selector = requireAppSelector(await this.deps.resolveSelector(raw), "status");
|
|
1001
|
+
return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
|
|
1002
|
+
const appGuid = await resolveAppGuid(selector.appName);
|
|
833
1003
|
const [app, instances, sshEnabled, recent] = await Promise.all([
|
|
834
1004
|
client.fetchApp(appGuid),
|
|
835
1005
|
client.fetchWebProcessStats(appGuid),
|
|
836
1006
|
client.fetchSshEnabled(appGuid),
|
|
837
|
-
client.fetchAuditEvents({ appGuid, types: void 0, createdAfter: void 0, limit: 1 })
|
|
1007
|
+
client.fetchAuditEvents({ scope: { kind: "app", appGuid }, types: void 0, createdAfter: void 0, limit: 1 })
|
|
838
1008
|
]);
|
|
839
1009
|
return {
|
|
840
1010
|
appName: selector.appName,
|
|
@@ -848,12 +1018,13 @@ var CfEventsRuntime = class {
|
|
|
848
1018
|
}
|
|
849
1019
|
async watchEvents(raw, credentials, options, onEvent, signal) {
|
|
850
1020
|
const selector = await this.deps.resolveSelector(raw);
|
|
851
|
-
await this.deps.
|
|
1021
|
+
await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
1022
|
+
const scope = await resolveAuditScope(session);
|
|
852
1023
|
const seen = /* @__PURE__ */ new Set();
|
|
853
1024
|
let cursor = durationToCreatedAfter(options.lookback, this.deps.now());
|
|
854
1025
|
while (!signal.aborted) {
|
|
855
|
-
const events = await client.fetchAuditEvents({
|
|
856
|
-
|
|
1026
|
+
const events = await session.client.fetchAuditEvents({
|
|
1027
|
+
scope,
|
|
857
1028
|
types: options.types.length > 0 ? options.types : void 0,
|
|
858
1029
|
createdAfter: cursor,
|
|
859
1030
|
limit: WATCH_FETCH_LIMIT
|
|
@@ -903,6 +1074,8 @@ export {
|
|
|
903
1074
|
formatEventLine,
|
|
904
1075
|
formatEventsReport,
|
|
905
1076
|
formatRelativeTime,
|
|
1077
|
+
formatSpaceEventLine,
|
|
1078
|
+
formatSpaceEventsReport,
|
|
906
1079
|
formatSshStatusReport,
|
|
907
1080
|
formatStatusReport,
|
|
908
1081
|
formatUptime,
|
|
@@ -911,16 +1084,20 @@ export {
|
|
|
911
1084
|
inferSshSessions,
|
|
912
1085
|
isCrashEvent,
|
|
913
1086
|
isSshEvent,
|
|
1087
|
+
mapAuditEvent,
|
|
914
1088
|
parseCfTargetOutput,
|
|
915
1089
|
parseDuration,
|
|
916
1090
|
parseSelector,
|
|
917
1091
|
parseTypeFilter,
|
|
918
1092
|
prepareCfCliSession,
|
|
919
1093
|
readCurrentCfTarget,
|
|
1094
|
+
resolveOrganizationGuid,
|
|
920
1095
|
resolveSelector,
|
|
1096
|
+
resolveSpaceGuid,
|
|
921
1097
|
runCfCommand,
|
|
922
1098
|
sortEventsNewestFirst,
|
|
923
1099
|
summarizeCrashes,
|
|
1100
|
+
summarizeSpaceCrashes,
|
|
924
1101
|
toCrashRecord,
|
|
925
1102
|
withCfSession
|
|
926
1103
|
};
|