@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/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.set("target_guids", input.appGuid);
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 formatCrashReport(summary, now) {
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
- if (regionKey === void 0 || orgName === void 0 || spaceName === void 0 || appName === void 0 || regionKey.length === 0 || orgName.length === 0 || spaceName.length === 0 || appName.length === 0) {
668
- throw new Error(`Invalid selector "${raw}": every region/org/space/app segment must be non-empty.`);
669
- }
670
- return { kind: "explicit", regionKey, orgName, spaceName, appName };
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 defaultWithCfApp(selector, credentials, work) {
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({ appGuid, client });
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
- withCfApp: defaultWithCfApp,
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.withCfApp(selector, credentials, async ({ appGuid, client }) => {
789
- return await client.fetchAuditEvents({
790
- appGuid,
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.withCfApp(selector, credentials, async ({ appGuid, client }) => {
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.withCfApp(selector, credentials, async ({ appGuid, client }) => {
821
- const events = await client.fetchAuditEvents({
822
- appGuid,
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
- return summarizeCrashes(selector.appName, events);
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.withCfApp(selector, credentials, async ({ appGuid, client }) => {
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.withCfApp(selector, credentials, async ({ appGuid, client }) => {
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
- appGuid,
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
  };