@saptools/cf-events 0.1.3 → 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 +28 -76
- 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/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# ☁️ `@saptools/cf-events`
|
|
4
4
|
|
|
5
|
-
**Inspect SAP BTP Cloud Foundry application audit events and detect active SSH/debug sessions from the command line.**
|
|
5
|
+
**Inspect SAP BTP Cloud Foundry application and space audit events and detect active SSH/debug sessions from the command line.**
|
|
6
6
|
|
|
7
|
-
Point it at a `region/org/space/app`
|
|
7
|
+
Point it at a `region/org/space/app`, `region/org/space`, or a bare app name and instantly answer: "what just happened?", "is anyone SSH'd in right now?", or "why did it crash?".
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/@saptools/cf-events)
|
|
10
10
|
[](./LICENSE)
|
|
@@ -12,7 +12,7 @@ Point it at a `region/org/space/app` (or a bare app name) and instantly answer:
|
|
|
12
12
|
[](https://packagephobia.com/result?p=@saptools/cf-events)
|
|
13
13
|
[](https://www.typescriptlang.org)
|
|
14
14
|
|
|
15
|
-
[Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [
|
|
15
|
+
[Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [FAQ](#-faq)
|
|
16
16
|
|
|
17
17
|
</div>
|
|
18
18
|
|
|
@@ -20,16 +20,15 @@ Point it at a `region/org/space/app` (or a bare app name) and instantly answer:
|
|
|
20
20
|
|
|
21
21
|
## ✨ Features
|
|
22
22
|
|
|
23
|
-
- 📜 **Audit event inspection** — list recent `audit.app.*` events
|
|
23
|
+
- 📜 **Audit event inspection** — list recent `audit.app.*` events for one app or all apps in a space
|
|
24
24
|
- 🔐 **SSH & debug session detection** — surface authorized sessions + denied attempts; infer "likely active" ones from recent `ssh-authorized` events (CF has no live session or close events)
|
|
25
|
-
- 💥 **Crash summaries** — count, last crash time/reason/exit code, and per-instance detail
|
|
25
|
+
- 💥 **Crash summaries** — count, last crash time/reason/exit code, and per-instance detail for an app or grouped by app across a space
|
|
26
26
|
- ❤️ **One-glance status** — requested state, per-instance CPU/mem/uptime, SSH flag, and the most recent audit event
|
|
27
27
|
- 👀 **Live watch** — poll `/v3/audit_events` on an interval and stream new events (Ctrl+C to stop)
|
|
28
|
-
- 🧭 **Smart selectors** — full `region/org/space/app` or bare app
|
|
29
|
-
- 🔄 **Reuses cf-sync** — topology snapshot + current-target helpers from [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync)
|
|
28
|
+
- 🧭 **Smart selectors** — full app selectors (`region/org/space/app`), space selectors (`region/org/space`), or bare app names resolved from the current `cf target`
|
|
30
29
|
- 🧪 **Isolated sessions** — every invocation uses a fresh ephemeral `CF_HOME`; never touches your interactive login
|
|
31
30
|
- 🧩 **CLI + typed library** — full TypeScript exports for `CfEventsRuntime`, parsers, formatters, and types
|
|
32
|
-
- 🪶 **Zero daemon, tiny deps** — commander
|
|
31
|
+
- 🪶 **Zero daemon, tiny deps** — commander only
|
|
33
32
|
|
|
34
33
|
---
|
|
35
34
|
|
|
@@ -59,12 +58,10 @@ npm install @saptools/cf-events
|
|
|
59
58
|
export SAP_EMAIL="you@company.com"
|
|
60
59
|
export SAP_PASSWORD="your-sap-password"
|
|
61
60
|
|
|
62
|
-
# 2.
|
|
63
|
-
cf-sync sync --only ap10 # or cf-sync space ap10 my-org dev
|
|
64
|
-
|
|
65
|
-
# 3. Inspect events
|
|
61
|
+
# 2. Inspect events
|
|
66
62
|
cf-events events orders-srv
|
|
67
63
|
cf-events events ap10/my-org/dev/orders-srv --limit 100 --since 6h
|
|
64
|
+
cf-events events ap10/my-org/dev --limit 200 --since 6h
|
|
68
65
|
|
|
69
66
|
# 4. Check for active SSH/debug sessions
|
|
70
67
|
cf-events ssh-status orders-srv --since 7d
|
|
@@ -79,16 +76,17 @@ cf-events status orders-srv
|
|
|
79
76
|
cf-events watch orders-srv --type crash
|
|
80
77
|
```
|
|
81
78
|
|
|
82
|
-
|
|
79
|
+
Bare app names use the current `cf target` for org/space/API context. Explicit app and space selectors do not require a topology snapshot.
|
|
83
80
|
|
|
84
81
|
---
|
|
85
82
|
|
|
86
83
|
## 🧰 CLI
|
|
87
84
|
|
|
88
|
-
All commands accept a single positional **selector
|
|
85
|
+
All commands accept a single positional **selector**. `events`, `watch`, and `crashes` accept app or space selectors; `status` and `ssh-status` require an app selector.
|
|
89
86
|
|
|
90
|
-
-
|
|
91
|
-
-
|
|
87
|
+
- App path: `ap10/my-org/dev/orders-srv`
|
|
88
|
+
- Space path: `ap10/my-org/dev`
|
|
89
|
+
- Bare app name: `orders-srv` — resolved against the current `cf target`. Bare single-segment selectors are always app names, never spaces.
|
|
92
90
|
|
|
93
91
|
Common credential flags (all commands):
|
|
94
92
|
|
|
@@ -100,13 +98,14 @@ Common credential flags (all commands):
|
|
|
100
98
|
|
|
101
99
|
### `events <selector>`
|
|
102
100
|
|
|
103
|
-
List recent audit events for the app (deployments, restarts, scaling, crashes, SSH, routes, ...).
|
|
101
|
+
List recent audit events for the app or all apps in a space (deployments, restarts, scaling, crashes, SSH, routes, ...).
|
|
104
102
|
|
|
105
103
|
```bash
|
|
106
104
|
cf-events events ap10/my-org/dev/orders-srv
|
|
107
105
|
cf-events events orders-srv --limit 100 --since 6h
|
|
108
106
|
cf-events events orders-srv --type ssh --json
|
|
109
107
|
cf-events events orders-srv --type audit.app.start,audit.app.stop
|
|
108
|
+
cf-events events ap10/my-org/dev --type ssh --json
|
|
110
109
|
```
|
|
111
110
|
|
|
112
111
|
| Flag | Description |
|
|
@@ -133,11 +132,12 @@ cf-events ssh-status orders-srv --since 7d --json
|
|
|
133
132
|
|
|
134
133
|
### `crashes <selector>`
|
|
135
134
|
|
|
136
|
-
Count and detail recent crash events (both `audit.app.crash` and `audit.app.process.crash`).
|
|
135
|
+
Count and detail recent crash events for an app or grouped by target app across a space (both `audit.app.crash` and `audit.app.process.crash`).
|
|
137
136
|
|
|
138
137
|
```bash
|
|
139
138
|
cf-events crashes orders-srv
|
|
140
139
|
cf-events crashes orders-srv --since 24h --json
|
|
140
|
+
cf-events crashes ap10/my-org/dev --since 24h --json
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
| Flag | Description |
|
|
@@ -156,11 +156,12 @@ cf-events status orders-srv --json
|
|
|
156
156
|
|
|
157
157
|
### `watch <selector>`
|
|
158
158
|
|
|
159
|
-
Poll `/v3/audit_events` repeatedly and print (or emit NDJSON) fresh events as they arrive. Press Ctrl+C to stop.
|
|
159
|
+
Poll `/v3/audit_events` repeatedly for an app or space and print (or emit NDJSON) fresh events as they arrive. Press Ctrl+C to stop.
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
162
|
cf-events watch orders-srv
|
|
163
163
|
cf-events watch orders-srv --interval 30000 --type crash --lookback 5m
|
|
164
|
+
cf-events watch ap10/my-org/dev --lookback 5m --type crash --json
|
|
164
165
|
```
|
|
165
166
|
|
|
166
167
|
| Flag | Description |
|
|
@@ -172,70 +173,22 @@ cf-events watch orders-srv --interval 30000 --type crash --lookback 5m
|
|
|
172
173
|
|
|
173
174
|
---
|
|
174
175
|
|
|
175
|
-
##
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
import {
|
|
179
|
-
CfEventsRuntime,
|
|
180
|
-
// also re-exported for advanced use:
|
|
181
|
-
// fetchAuditEvents, fetchApp, fetchSshEnabled, fetchWebProcessStats,
|
|
182
|
-
// parseTypeFilter, durationToCreatedAfter,
|
|
183
|
-
// formatEventsReport, formatSshStatusReport, formatCrashReport, formatStatusReport,
|
|
184
|
-
// resolveSelector, parseSelector,
|
|
185
|
-
// plus all types
|
|
186
|
-
} from "@saptools/cf-events";
|
|
187
|
-
|
|
188
|
-
const runtime = new CfEventsRuntime();
|
|
189
|
-
|
|
190
|
-
const events = await runtime.fetchEvents("orders-srv", {
|
|
191
|
-
email: process.env.SAP_EMAIL ?? "",
|
|
192
|
-
password: process.env.SAP_PASSWORD ?? "",
|
|
193
|
-
}, {
|
|
194
|
-
limit: 50,
|
|
195
|
-
since: "6h",
|
|
196
|
-
types: [], // or ["audit.app.crash"] or use parseTypeFilter("crash")
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
console.log(events.length);
|
|
200
|
-
|
|
201
|
-
// SSH inference + enabled flag
|
|
202
|
-
const ssh = await runtime.getSshStatus("orders-srv", creds, "24h");
|
|
203
|
-
|
|
204
|
-
// Crash summary
|
|
205
|
-
const crashes = await runtime.getCrashes("orders-srv", creds, { limit: 20 });
|
|
206
|
-
|
|
207
|
-
// Health + instances
|
|
208
|
-
const health = await runtime.getStatus("ap10/my-org/dev/orders-srv", creds);
|
|
209
|
-
|
|
210
|
-
// Live polling (call with AbortSignal)
|
|
211
|
-
const ac = new AbortController();
|
|
212
|
-
await runtime.watchEvents("orders-srv", creds, { intervalMs: 15000, lookback: "2m", types: [] }, (ev) => {
|
|
213
|
-
console.log(ev);
|
|
214
|
-
}, ac.signal);
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
The package is a full barrel; everything under `src/` (parsers, formatters, low-level CF session helpers, types) is exported for power users or custom tooling.
|
|
218
|
-
|
|
219
|
-
---
|
|
220
|
-
|
|
221
|
-
## 📁 Prerequisites & Snapshots
|
|
176
|
+
## 📁 Prerequisites & Selector Resolution
|
|
222
177
|
|
|
223
|
-
`cf-events`
|
|
178
|
+
`cf-events` uses direct CF CLI/API calls and an isolated throw-away `CF_HOME`; it does not read or validate a `cf-sync` snapshot.
|
|
224
179
|
|
|
225
|
-
-
|
|
226
|
-
-
|
|
227
|
-
-
|
|
180
|
+
- Explicit app selectors (`region/org/space/app`) and space selectors (`region/org/space`) use the built-in SAP BTP region-to-API map, then authenticate and target that org/space.
|
|
181
|
+
- Bare app names use your current interactive `cf target` only to discover API endpoint, org, and space, then resolve the app GUID inside the isolated session.
|
|
182
|
+
- Space-wide audit event queries use the Cloud Foundry v3 `space_guids` audit-event filter after resolving org and space GUIDs with `/v3/organizations?names=...` and `/v3/spaces?names=...&organization_guids=...`.
|
|
228
183
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
---
|
|
184
|
+
For JSON output, `events --json` returns the same raw audit-event array shape for app and space selectors. Space events include target, space, and organization references from the CF response. `watch --json` emits newline-delimited JSON events.
|
|
232
185
|
|
|
233
186
|
## ❓ FAQ
|
|
234
187
|
|
|
235
188
|
<details>
|
|
236
189
|
<summary><b>Do I need to run cf target first?</b></summary>
|
|
237
190
|
|
|
238
|
-
Not required
|
|
191
|
+
Not required for explicit selectors. Bare app names require a current `cf target` so `cf-events` can discover the API endpoint, org, and space.
|
|
239
192
|
|
|
240
193
|
</details>
|
|
241
194
|
|
|
@@ -263,7 +216,7 @@ Yes: `--type crash` or `--type ssh`. You can also pass comma-separated full type
|
|
|
263
216
|
<details>
|
|
264
217
|
<summary><b>Is the snapshot required even for JSON output?</b></summary>
|
|
265
218
|
|
|
266
|
-
|
|
219
|
+
No. `cf-events` does not read a snapshot. Explicit selectors are resolved with the region map and live CF API/CLI calls; bare app names use the current `cf target`.
|
|
267
220
|
|
|
268
221
|
</details>
|
|
269
222
|
|
|
@@ -289,7 +242,6 @@ E2E tests are fully fake-backed (no real CF or credentials needed).
|
|
|
289
242
|
|
|
290
243
|
## 🌐 Related
|
|
291
244
|
|
|
292
|
-
- 🗺️ [`@saptools/cf-sync`](https://www.npmjs.com/package/@saptools/cf-sync) — the topology snapshot and current-target helpers that power selector resolution
|
|
293
245
|
- 🔐 [`@saptools/cf-xsuaa`](https://www.npmjs.com/package/@saptools/cf-xsuaa)
|
|
294
246
|
- 🐛 [`@saptools/cf-debugger`](https://www.npmjs.com/package/@saptools/cf-debugger)
|
|
295
247
|
- Full toolbox: [saptools monorepo](https://github.com/dongitran/saptools)
|
package/dist/cli.js
CHANGED
|
@@ -88,6 +88,39 @@ function summarizeCrashes(appName, events) {
|
|
|
88
88
|
crashes
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
|
+
function crashAppName(event) {
|
|
92
|
+
if (event.target.name.length > 0) {
|
|
93
|
+
return event.target.name;
|
|
94
|
+
}
|
|
95
|
+
if (event.target.guid.length > 0) {
|
|
96
|
+
return event.target.guid;
|
|
97
|
+
}
|
|
98
|
+
return "(unknown app)";
|
|
99
|
+
}
|
|
100
|
+
function summarizeSpaceCrashes(selector, events) {
|
|
101
|
+
const byApp = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const event of events.filter(isCrashEvent)) {
|
|
103
|
+
const appName = crashAppName(event);
|
|
104
|
+
byApp.set(appName, [...byApp.get(appName) ?? [], event]);
|
|
105
|
+
}
|
|
106
|
+
const apps = [...byApp.entries()].map(([appName, appEvents]) => summarizeCrashes(appName, appEvents)).map(({ appName, crashCount, lastCrashAt, lastCrashReason, crashes }) => ({
|
|
107
|
+
appName,
|
|
108
|
+
crashCount,
|
|
109
|
+
lastCrashAt,
|
|
110
|
+
lastCrashReason,
|
|
111
|
+
crashes
|
|
112
|
+
})).sort((left, right) => {
|
|
113
|
+
const byTime = (right.lastCrashAt ?? "").localeCompare(left.lastCrashAt ?? "");
|
|
114
|
+
return byTime === 0 ? left.appName.localeCompare(right.appName) : byTime;
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
scope: "space",
|
|
118
|
+
selector,
|
|
119
|
+
crashCount: apps.reduce((sum, app) => sum + app.crashCount, 0),
|
|
120
|
+
lastCrashAt: apps[0]?.lastCrashAt,
|
|
121
|
+
apps
|
|
122
|
+
};
|
|
123
|
+
}
|
|
91
124
|
|
|
92
125
|
// src/format.ts
|
|
93
126
|
var EVENT_LABELS = {
|
|
@@ -188,6 +221,34 @@ function formatEventsReport(appName, events, now) {
|
|
|
188
221
|
...events.map((event) => formatEventLine(event, now))
|
|
189
222
|
].join("\n");
|
|
190
223
|
}
|
|
224
|
+
function eventTargetLabel(event) {
|
|
225
|
+
if (event.target.name.length > 0) {
|
|
226
|
+
return event.target.name;
|
|
227
|
+
}
|
|
228
|
+
if (event.target.type.length > 0) {
|
|
229
|
+
return event.target.type;
|
|
230
|
+
}
|
|
231
|
+
return event.target.guid.length > 0 ? event.target.guid : "(unknown target)";
|
|
232
|
+
}
|
|
233
|
+
function formatSpaceEventLine(event, now) {
|
|
234
|
+
const actor = event.actor.name.length > 0 ? event.actor.name : event.actor.type;
|
|
235
|
+
return [
|
|
236
|
+
` ${event.createdAt.padEnd(26)}`,
|
|
237
|
+
formatRelativeTime(event.createdAt, now).padEnd(11),
|
|
238
|
+
describeEventType(event.type).padEnd(24),
|
|
239
|
+
eventTargetLabel(event).padEnd(18),
|
|
240
|
+
actor.length > 0 ? actor : "(unknown actor)"
|
|
241
|
+
].join(" ");
|
|
242
|
+
}
|
|
243
|
+
function formatSpaceEventsReport(selector, events, now) {
|
|
244
|
+
if (events.length === 0) {
|
|
245
|
+
return `No audit events found for space ${selector}.`;
|
|
246
|
+
}
|
|
247
|
+
return [
|
|
248
|
+
`Audit events for space ${selector} (${events.length.toString()}):`,
|
|
249
|
+
...events.map((event) => formatSpaceEventLine(event, now))
|
|
250
|
+
].join("\n");
|
|
251
|
+
}
|
|
191
252
|
function formatSshStatusReport(status, now) {
|
|
192
253
|
const sessionLines = status.sessions.map((session) => {
|
|
193
254
|
const tag = session.likelyActive ? "[active]" : "[past] ";
|
|
@@ -212,7 +273,7 @@ function formatSshStatusReport(status, now) {
|
|
|
212
273
|
" are inferred from recent ssh-authorized audit events."
|
|
213
274
|
].join("\n");
|
|
214
275
|
}
|
|
215
|
-
function
|
|
276
|
+
function formatAppCrashReport(summary, now) {
|
|
216
277
|
if (summary.crashCount === 0) {
|
|
217
278
|
return `No crashes found for ${summary.appName}.`;
|
|
218
279
|
}
|
|
@@ -233,6 +294,35 @@ function formatCrashReport(summary, now) {
|
|
|
233
294
|
...crashLines
|
|
234
295
|
].join("\n");
|
|
235
296
|
}
|
|
297
|
+
function formatSpaceCrashReport(summary, now) {
|
|
298
|
+
if (summary.crashCount === 0) {
|
|
299
|
+
return `No crashes found for space ${summary.selector}.`;
|
|
300
|
+
}
|
|
301
|
+
const appLines = summary.apps.flatMap((app) => [
|
|
302
|
+
` ${app.appName} (${app.crashCount.toString()})`,
|
|
303
|
+
...app.crashes.map((crash) => {
|
|
304
|
+
const index = crash.index === void 0 ? "-" : `#${crash.index.toString()}`;
|
|
305
|
+
const reason = crash.reason ?? "unknown";
|
|
306
|
+
const exit = crash.exitStatus === void 0 ? "" : ` exit ${crash.exitStatus.toString()}`;
|
|
307
|
+
return ` ${crash.at} instance ${index} ${reason}${exit}`;
|
|
308
|
+
})
|
|
309
|
+
]);
|
|
310
|
+
return [
|
|
311
|
+
`Crash report for space ${summary.selector}`,
|
|
312
|
+
"",
|
|
313
|
+
` Crashes: ${summary.crashCount.toString()}`,
|
|
314
|
+
` Apps affected: ${summary.apps.length.toString()}`,
|
|
315
|
+
...summary.lastCrashAt === void 0 ? [] : [` Last crash: ${summary.lastCrashAt} (${formatRelativeTime(summary.lastCrashAt, now)})`],
|
|
316
|
+
"",
|
|
317
|
+
...appLines
|
|
318
|
+
].join("\n");
|
|
319
|
+
}
|
|
320
|
+
function formatCrashReport(summary, now) {
|
|
321
|
+
if ("scope" in summary) {
|
|
322
|
+
return formatSpaceCrashReport(summary, now);
|
|
323
|
+
}
|
|
324
|
+
return formatAppCrashReport(summary, now);
|
|
325
|
+
}
|
|
236
326
|
function formatInstanceLine(instance) {
|
|
237
327
|
return [
|
|
238
328
|
` #${instance.index.toString()}`.padEnd(6),
|
|
@@ -327,7 +417,7 @@ function mapProcessStat(value) {
|
|
|
327
417
|
}
|
|
328
418
|
function buildAuditEventsPath(input, perPage) {
|
|
329
419
|
const params = new URLSearchParams();
|
|
330
|
-
params
|
|
420
|
+
applyAuditEventScope(params, input.scope);
|
|
331
421
|
params.set("order_by", "-created_at");
|
|
332
422
|
params.set("per_page", perPage.toString());
|
|
333
423
|
if (input.types !== void 0 && input.types.length > 0) {
|
|
@@ -338,6 +428,36 @@ function buildAuditEventsPath(input, perPage) {
|
|
|
338
428
|
}
|
|
339
429
|
return `/v3/audit_events?${params.toString()}`;
|
|
340
430
|
}
|
|
431
|
+
function applyAuditEventScope(params, scope) {
|
|
432
|
+
if (scope.kind === "app") {
|
|
433
|
+
params.set("target_guids", scope.appGuid);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
params.set("space_guids", scope.spaceGuid);
|
|
437
|
+
}
|
|
438
|
+
async function resolveOrganizationGuid(orgName, curl) {
|
|
439
|
+
const params = new URLSearchParams();
|
|
440
|
+
params.set("names", orgName);
|
|
441
|
+
const body = asRecord(parseJson(await curl(`/v3/organizations?${params.toString()}`)));
|
|
442
|
+
const first = asRecord(asArray(body["resources"])[0]);
|
|
443
|
+
const guid = asString(first["guid"]);
|
|
444
|
+
if (guid.length === 0) {
|
|
445
|
+
throw new Error(`Could not resolve the GUID for organization "${orgName}".`);
|
|
446
|
+
}
|
|
447
|
+
return guid;
|
|
448
|
+
}
|
|
449
|
+
async function resolveSpaceGuid(spaceName, orgGuid, curl) {
|
|
450
|
+
const params = new URLSearchParams();
|
|
451
|
+
params.set("names", spaceName);
|
|
452
|
+
params.set("organization_guids", orgGuid);
|
|
453
|
+
const body = asRecord(parseJson(await curl(`/v3/spaces?${params.toString()}`)));
|
|
454
|
+
const first = asRecord(asArray(body["resources"])[0]);
|
|
455
|
+
const guid = asString(first["guid"]);
|
|
456
|
+
if (guid.length === 0) {
|
|
457
|
+
throw new Error(`Could not resolve the GUID for space "${spaceName}".`);
|
|
458
|
+
}
|
|
459
|
+
return guid;
|
|
460
|
+
}
|
|
341
461
|
function nextPagePath(body) {
|
|
342
462
|
const next = asRecord(asRecord(asRecord(body)["pagination"])["next"]);
|
|
343
463
|
const href = next["href"];
|
|
@@ -626,7 +746,12 @@ function extractSafeCliDetail(error) {
|
|
|
626
746
|
}
|
|
627
747
|
|
|
628
748
|
// src/selector.ts
|
|
629
|
-
var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
|
|
749
|
+
var SELECTOR_USAGE = 'use "region/org/space", "region/org/space/app", or a bare app name';
|
|
750
|
+
function assertNonEmpty(raw, parts, shape) {
|
|
751
|
+
if (parts.some((part) => part.length === 0)) {
|
|
752
|
+
throw new Error(`Invalid selector "${raw}": every ${shape} segment must be non-empty.`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
630
755
|
function parseSelector(raw) {
|
|
631
756
|
const trimmed = raw.trim();
|
|
632
757
|
if (trimmed.length === 0) {
|
|
@@ -640,12 +765,21 @@ function parseSelector(raw) {
|
|
|
640
765
|
}
|
|
641
766
|
return { kind: "appName", appName };
|
|
642
767
|
}
|
|
768
|
+
if (parts.length === 3) {
|
|
769
|
+
assertNonEmpty(raw, parts, "region/org/space");
|
|
770
|
+
const [regionKey, orgName, spaceName] = parts;
|
|
771
|
+
return { kind: "spacePath", regionKey: regionKey ?? "", orgName: orgName ?? "", spaceName: spaceName ?? "" };
|
|
772
|
+
}
|
|
643
773
|
if (parts.length === 4) {
|
|
774
|
+
assertNonEmpty(raw, parts, "region/org/space/app");
|
|
644
775
|
const [regionKey, orgName, spaceName, appName] = parts;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
776
|
+
return {
|
|
777
|
+
kind: "appPath",
|
|
778
|
+
regionKey: regionKey ?? "",
|
|
779
|
+
orgName: orgName ?? "",
|
|
780
|
+
spaceName: spaceName ?? "",
|
|
781
|
+
appName: appName ?? ""
|
|
782
|
+
};
|
|
649
783
|
}
|
|
650
784
|
throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
|
|
651
785
|
}
|
|
@@ -659,6 +793,7 @@ async function resolveSelector(raw) {
|
|
|
659
793
|
);
|
|
660
794
|
}
|
|
661
795
|
return {
|
|
796
|
+
kind: "app",
|
|
662
797
|
raw,
|
|
663
798
|
regionKey: current.regionKey ?? "",
|
|
664
799
|
apiEndpoint: current.apiEndpoint,
|
|
@@ -673,7 +808,18 @@ async function resolveSelector(raw) {
|
|
|
673
808
|
`Unknown region "${parsed.regionKey}". Use a bare app name (requires current CF target) or a known region key.`
|
|
674
809
|
);
|
|
675
810
|
}
|
|
811
|
+
if (parsed.kind === "spacePath") {
|
|
812
|
+
return {
|
|
813
|
+
kind: "space",
|
|
814
|
+
raw,
|
|
815
|
+
regionKey: parsed.regionKey,
|
|
816
|
+
apiEndpoint,
|
|
817
|
+
orgName: parsed.orgName,
|
|
818
|
+
spaceName: parsed.spaceName
|
|
819
|
+
};
|
|
820
|
+
}
|
|
676
821
|
return {
|
|
822
|
+
kind: "app",
|
|
677
823
|
raw,
|
|
678
824
|
regionKey: parsed.regionKey,
|
|
679
825
|
apiEndpoint,
|
|
@@ -724,10 +870,12 @@ function createCfClient(curl) {
|
|
|
724
870
|
fetchAuditEvents: (input) => fetchAuditEvents(input, curl),
|
|
725
871
|
fetchApp: (appGuid) => fetchApp(appGuid, curl),
|
|
726
872
|
fetchSshEnabled: (appGuid) => fetchSshEnabled(appGuid, curl),
|
|
727
|
-
fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl)
|
|
873
|
+
fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl),
|
|
874
|
+
resolveOrganizationGuid: (orgName) => resolveOrganizationGuid(orgName, curl),
|
|
875
|
+
resolveSpaceGuid: (spaceName, orgGuid) => resolveSpaceGuid(spaceName, orgGuid, curl)
|
|
728
876
|
};
|
|
729
877
|
}
|
|
730
|
-
async function
|
|
878
|
+
async function defaultWithCfTarget(selector, credentials, work) {
|
|
731
879
|
return await withCfSession(async (ctx) => {
|
|
732
880
|
await prepareCfCliSession(
|
|
733
881
|
{
|
|
@@ -738,15 +886,14 @@ async function defaultWithCfApp(selector, credentials, work) {
|
|
|
738
886
|
},
|
|
739
887
|
ctx
|
|
740
888
|
);
|
|
741
|
-
const appGuid = await cfAppGuid(selector.appName, ctx);
|
|
742
889
|
const client = createCfClient((path) => cfCurl(path, ctx));
|
|
743
|
-
return await work({
|
|
890
|
+
return await work({ selector, client, resolveAppGuid: (appName) => cfAppGuid(appName, ctx) });
|
|
744
891
|
});
|
|
745
892
|
}
|
|
746
893
|
function createDefaultDependencies() {
|
|
747
894
|
return {
|
|
748
895
|
resolveSelector,
|
|
749
|
-
|
|
896
|
+
withCfTarget: defaultWithCfTarget,
|
|
750
897
|
now: () => /* @__PURE__ */ new Date()
|
|
751
898
|
};
|
|
752
899
|
}
|
|
@@ -756,6 +903,24 @@ async function delay(ms, signal) {
|
|
|
756
903
|
} catch {
|
|
757
904
|
}
|
|
758
905
|
}
|
|
906
|
+
function requireAppSelector(selector, command) {
|
|
907
|
+
if (selector.kind === "app") {
|
|
908
|
+
return selector;
|
|
909
|
+
}
|
|
910
|
+
throw new Error(
|
|
911
|
+
`The ${command} command requires an app selector. Use region/org/space/app or a bare app name.`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
async function resolveAuditScope(session) {
|
|
915
|
+
if (session.selector.kind === "app") {
|
|
916
|
+
return { kind: "app", appGuid: await session.resolveAppGuid(session.selector.appName) };
|
|
917
|
+
}
|
|
918
|
+
const orgGuid = await session.client.resolveOrganizationGuid(session.selector.orgName);
|
|
919
|
+
return {
|
|
920
|
+
kind: "space",
|
|
921
|
+
spaceGuid: await session.client.resolveSpaceGuid(session.selector.spaceName, orgGuid)
|
|
922
|
+
};
|
|
923
|
+
}
|
|
759
924
|
var CfEventsRuntime = class {
|
|
760
925
|
deps;
|
|
761
926
|
constructor(deps = createDefaultDependencies()) {
|
|
@@ -763,9 +928,9 @@ var CfEventsRuntime = class {
|
|
|
763
928
|
}
|
|
764
929
|
async fetchEvents(raw, credentials, options) {
|
|
765
930
|
const selector = await this.deps.resolveSelector(raw);
|
|
766
|
-
return await this.deps.
|
|
767
|
-
return await client.fetchAuditEvents({
|
|
768
|
-
|
|
931
|
+
return await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
932
|
+
return await session.client.fetchAuditEvents({
|
|
933
|
+
scope: await resolveAuditScope(session),
|
|
769
934
|
types: options.types.length > 0 ? options.types : void 0,
|
|
770
935
|
createdAfter: this.resolveCreatedAfter(options.since),
|
|
771
936
|
limit: options.limit
|
|
@@ -773,12 +938,13 @@ var CfEventsRuntime = class {
|
|
|
773
938
|
});
|
|
774
939
|
}
|
|
775
940
|
async getSshStatus(raw, credentials, since) {
|
|
776
|
-
const selector = await this.deps.resolveSelector(raw);
|
|
777
|
-
return await this.deps.
|
|
941
|
+
const selector = requireAppSelector(await this.deps.resolveSelector(raw), "ssh-status");
|
|
942
|
+
return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
|
|
943
|
+
const appGuid = await resolveAppGuid(selector.appName);
|
|
778
944
|
const [sshEnabled, events] = await Promise.all([
|
|
779
945
|
client.fetchSshEnabled(appGuid),
|
|
780
946
|
client.fetchAuditEvents({
|
|
781
|
-
appGuid,
|
|
947
|
+
scope: { kind: "app", appGuid },
|
|
782
948
|
types: [...SSH_EVENT_TYPES],
|
|
783
949
|
createdAfter: durationToCreatedAfter(since, this.deps.now()),
|
|
784
950
|
limit: SSH_STATUS_EVENT_LIMIT
|
|
@@ -795,24 +961,28 @@ var CfEventsRuntime = class {
|
|
|
795
961
|
}
|
|
796
962
|
async getCrashes(raw, credentials, options) {
|
|
797
963
|
const selector = await this.deps.resolveSelector(raw);
|
|
798
|
-
return await this.deps.
|
|
799
|
-
const events = await client.fetchAuditEvents({
|
|
800
|
-
|
|
964
|
+
return await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
965
|
+
const events = await session.client.fetchAuditEvents({
|
|
966
|
+
scope: await resolveAuditScope(session),
|
|
801
967
|
types: [...CRASH_EVENT_TYPES],
|
|
802
968
|
createdAfter: this.resolveCreatedAfter(options.since),
|
|
803
969
|
limit: options.limit
|
|
804
970
|
});
|
|
805
|
-
|
|
971
|
+
if (selector.kind === "app") {
|
|
972
|
+
return summarizeCrashes(selector.appName, events);
|
|
973
|
+
}
|
|
974
|
+
return summarizeSpaceCrashes(`${selector.regionKey}/${selector.orgName}/${selector.spaceName}`, events);
|
|
806
975
|
});
|
|
807
976
|
}
|
|
808
977
|
async getStatus(raw, credentials) {
|
|
809
|
-
const selector = await this.deps.resolveSelector(raw);
|
|
810
|
-
return await this.deps.
|
|
978
|
+
const selector = requireAppSelector(await this.deps.resolveSelector(raw), "status");
|
|
979
|
+
return await this.deps.withCfTarget(selector, credentials, async ({ client, resolveAppGuid }) => {
|
|
980
|
+
const appGuid = await resolveAppGuid(selector.appName);
|
|
811
981
|
const [app, instances, sshEnabled, recent] = await Promise.all([
|
|
812
982
|
client.fetchApp(appGuid),
|
|
813
983
|
client.fetchWebProcessStats(appGuid),
|
|
814
984
|
client.fetchSshEnabled(appGuid),
|
|
815
|
-
client.fetchAuditEvents({ appGuid, types: void 0, createdAfter: void 0, limit: 1 })
|
|
985
|
+
client.fetchAuditEvents({ scope: { kind: "app", appGuid }, types: void 0, createdAfter: void 0, limit: 1 })
|
|
816
986
|
]);
|
|
817
987
|
return {
|
|
818
988
|
appName: selector.appName,
|
|
@@ -826,12 +996,13 @@ var CfEventsRuntime = class {
|
|
|
826
996
|
}
|
|
827
997
|
async watchEvents(raw, credentials, options, onEvent, signal) {
|
|
828
998
|
const selector = await this.deps.resolveSelector(raw);
|
|
829
|
-
await this.deps.
|
|
999
|
+
await this.deps.withCfTarget(selector, credentials, async (session) => {
|
|
1000
|
+
const scope = await resolveAuditScope(session);
|
|
830
1001
|
const seen = /* @__PURE__ */ new Set();
|
|
831
1002
|
let cursor = durationToCreatedAfter(options.lookback, this.deps.now());
|
|
832
1003
|
while (!signal.aborted) {
|
|
833
|
-
const events = await client.fetchAuditEvents({
|
|
834
|
-
|
|
1004
|
+
const events = await session.client.fetchAuditEvents({
|
|
1005
|
+
scope,
|
|
835
1006
|
types: options.types.length > 0 ? options.types : void 0,
|
|
836
1007
|
createdAfter: cursor,
|
|
837
1008
|
limit: WATCH_FETCH_LIMIT
|
|
@@ -893,6 +1064,13 @@ function parsePositiveInteger(value, optionName) {
|
|
|
893
1064
|
function appLabel(selector) {
|
|
894
1065
|
return (selector.split("/").at(-1) ?? selector).trim();
|
|
895
1066
|
}
|
|
1067
|
+
function isSpaceSelector(selector) {
|
|
1068
|
+
return parseSelector(selector).kind === "spacePath";
|
|
1069
|
+
}
|
|
1070
|
+
function spaceLabel(selector) {
|
|
1071
|
+
const parsed = parseSelector(selector);
|
|
1072
|
+
return parsed.kind === "spacePath" ? `${parsed.regionKey}/${parsed.orgName}/${parsed.spaceName}` : selector;
|
|
1073
|
+
}
|
|
896
1074
|
function resolveSelectorArgument(selector) {
|
|
897
1075
|
return selector;
|
|
898
1076
|
}
|
|
@@ -920,7 +1098,7 @@ async function runEvents(selector, flags) {
|
|
|
920
1098
|
writeJson(events);
|
|
921
1099
|
return;
|
|
922
1100
|
}
|
|
923
|
-
writeOut(formatEventsReport(appLabel(resolvedSelector), events, /* @__PURE__ */ new Date()));
|
|
1101
|
+
writeOut(isSpaceSelector(resolvedSelector) ? formatSpaceEventsReport(spaceLabel(resolvedSelector), events, /* @__PURE__ */ new Date()) : formatEventsReport(appLabel(resolvedSelector), events, /* @__PURE__ */ new Date()));
|
|
924
1102
|
}
|
|
925
1103
|
async function runSshStatus(selector, flags) {
|
|
926
1104
|
const resolvedSelector = resolveSelectorArgument(selector);
|
|
@@ -959,12 +1137,12 @@ async function runStatus(selector, flags) {
|
|
|
959
1137
|
}
|
|
960
1138
|
writeOut(formatStatusReport(health, /* @__PURE__ */ new Date()));
|
|
961
1139
|
}
|
|
962
|
-
function emitWatchEvent(event, asJson) {
|
|
1140
|
+
function emitWatchEvent(event, asJson, spaceScope) {
|
|
963
1141
|
if (asJson) {
|
|
964
1142
|
writeJsonLine(event);
|
|
965
1143
|
return;
|
|
966
1144
|
}
|
|
967
|
-
writeOut(formatEventLine(event, /* @__PURE__ */ new Date()));
|
|
1145
|
+
writeOut(spaceScope ? formatSpaceEventLine(event, /* @__PURE__ */ new Date()) : formatEventLine(event, /* @__PURE__ */ new Date()));
|
|
968
1146
|
}
|
|
969
1147
|
async function runWatch(selector, flags) {
|
|
970
1148
|
const resolvedSelector = resolveSelectorArgument(selector);
|
|
@@ -978,6 +1156,7 @@ async function runWatch(selector, flags) {
|
|
|
978
1156
|
controller.abort();
|
|
979
1157
|
});
|
|
980
1158
|
const asJson = flags.json === true;
|
|
1159
|
+
const spaceScope = isSpaceSelector(resolvedSelector);
|
|
981
1160
|
try {
|
|
982
1161
|
process2.stderr.write(`Watching ${resolvedSelector} for new audit events. Press Ctrl+C to stop.
|
|
983
1162
|
`);
|
|
@@ -990,7 +1169,7 @@ async function runWatch(selector, flags) {
|
|
|
990
1169
|
types: parseTypeFilter(flags.type)
|
|
991
1170
|
},
|
|
992
1171
|
(event) => {
|
|
993
|
-
emitWatchEvent(event, asJson);
|
|
1172
|
+
emitWatchEvent(event, asJson, spaceScope);
|
|
994
1173
|
},
|
|
995
1174
|
controller.signal
|
|
996
1175
|
);
|
|
@@ -1036,7 +1215,7 @@ function buildProgram() {
|
|
|
1036
1215
|
const program = new Command();
|
|
1037
1216
|
program.name("cf-events").description("Inspect SAP BTP Cloud Foundry application audit events and SSH/debug sessions").version(readPackageVersion(), "-V, --version", "Print the cf-events package version");
|
|
1038
1217
|
addCommonOptions(
|
|
1039
|
-
program.command("events").description("List recent audit events for an app").argument("<selector>", "region/org/space/app or a bare app name")
|
|
1218
|
+
program.command("events").description("List recent audit events for an app or all apps in a space").argument("<selector>", "region/org/space, region/org/space/app, or a bare app name")
|
|
1040
1219
|
).option(
|
|
1041
1220
|
"--limit <count>",
|
|
1042
1221
|
"Maximum number of events to return",
|
|
@@ -1050,7 +1229,7 @@ function buildProgram() {
|
|
|
1050
1229
|
await runSshStatus(selector, flags);
|
|
1051
1230
|
});
|
|
1052
1231
|
addCommonOptions(
|
|
1053
|
-
program.command("crashes").description("Summarize recent crash events for an app").argument("<selector>", "region/org/space/app or a bare app name")
|
|
1232
|
+
program.command("crashes").description("Summarize recent crash events for an app or all apps in a space").argument("<selector>", "region/org/space, region/org/space/app, or a bare app name")
|
|
1054
1233
|
).option(
|
|
1055
1234
|
"--limit <count>",
|
|
1056
1235
|
"Maximum number of crash events to inspect",
|
|
@@ -1064,7 +1243,7 @@ function buildProgram() {
|
|
|
1064
1243
|
await runStatus(selector, flags);
|
|
1065
1244
|
});
|
|
1066
1245
|
addCommonOptions(
|
|
1067
|
-
program.command("watch").description("Poll for new audit events
|
|
1246
|
+
program.command("watch").description("Poll for new audit events for an app or all apps in a space").argument("<selector>", "region/org/space, region/org/space/app, or a bare app name")
|
|
1068
1247
|
).option(
|
|
1069
1248
|
"--interval <ms>",
|
|
1070
1249
|
"Polling interval in milliseconds",
|