axusage 3.5.0 → 3.6.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/commands/serve-command.js +5 -2
- package/dist/commands/usage-command.js +4 -3
- package/dist/server/routes.js +6 -2
- package/dist/utils/calculate-usage-rate.d.ts +1 -1
- package/dist/utils/calculate-usage-rate.js +1 -2
- package/dist/utils/format-prometheus-metrics.d.ts +2 -2
- package/dist/utils/format-prometheus-metrics.js +16 -4
- package/dist/utils/format-service-usage.d.ts +1 -1
- package/dist/utils/format-service-usage.js +22 -16
- package/package.json +1 -1
|
@@ -97,12 +97,15 @@ export async function serveCommand(options) {
|
|
|
97
97
|
process.exit(1);
|
|
98
98
|
}, 5000);
|
|
99
99
|
forceExit.unref();
|
|
100
|
-
server
|
|
100
|
+
server
|
|
101
|
+
.stop()
|
|
102
|
+
.finally(() => {
|
|
101
103
|
clearTimeout(forceExit);
|
|
104
|
+
})
|
|
105
|
+
.then(() => {
|
|
102
106
|
// eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
|
|
103
107
|
process.exit(0);
|
|
104
108
|
}, (error) => {
|
|
105
|
-
clearTimeout(forceExit);
|
|
106
109
|
console.error("Error during shutdown:", error);
|
|
107
110
|
// eslint-disable-next-line unicorn/no-process-exit -- CLI graceful shutdown
|
|
108
111
|
process.exit(1);
|
|
@@ -74,9 +74,10 @@ export async function usageCommand(options) {
|
|
|
74
74
|
console.log(formatServiceUsageDataAsJson(singleSuccess));
|
|
75
75
|
}
|
|
76
76
|
else {
|
|
77
|
+
const now = Date.now();
|
|
77
78
|
const payload = successes.length === 1 && singleSuccess
|
|
78
|
-
? toJsonObject(singleSuccess)
|
|
79
|
-
: successes.map((data) => toJsonObject(data));
|
|
79
|
+
? toJsonObject(singleSuccess, now)
|
|
80
|
+
: successes.map((data) => toJsonObject(data, now));
|
|
80
81
|
const output = hasPartialFailures
|
|
81
82
|
? {
|
|
82
83
|
results: payload,
|
|
@@ -94,7 +95,7 @@ export async function usageCommand(options) {
|
|
|
94
95
|
}
|
|
95
96
|
case "prometheus": {
|
|
96
97
|
// Emit Prometheus text metrics using prom-client
|
|
97
|
-
const output = await formatPrometheusMetrics(successes);
|
|
98
|
+
const output = await formatPrometheusMetrics(successes, Date.now());
|
|
98
99
|
process.stdout.write(output);
|
|
99
100
|
break;
|
|
100
101
|
}
|
package/dist/server/routes.js
CHANGED
|
@@ -27,6 +27,8 @@ export function createMetricsRouter(getState) {
|
|
|
27
27
|
// Memoize the rendered Prometheus text by the state snapshot's refreshedAt
|
|
28
28
|
// timestamp. Scrapes within the same cache window reuse the same Promise,
|
|
29
29
|
// avoiding recreating prom-client Registry/Gauge objects on each request.
|
|
30
|
+
// Rate is computed at refreshedAt so output is deterministic per snapshot,
|
|
31
|
+
// keeping the gauge coherent with the usage data it describes.
|
|
30
32
|
// Assignments happen synchronously (before any await) so require-atomic-updates
|
|
31
33
|
// is satisfied and concurrent scrapes naturally coalesce onto one render.
|
|
32
34
|
let memoFor;
|
|
@@ -40,7 +42,7 @@ export function createMetricsRouter(getState) {
|
|
|
40
42
|
}
|
|
41
43
|
if (memoFor !== state.refreshedAt) {
|
|
42
44
|
memoFor = state.refreshedAt;
|
|
43
|
-
memoPromise = formatPrometheusMetrics(usage);
|
|
45
|
+
memoPromise = formatPrometheusMetrics(usage, state.refreshedAt.getTime());
|
|
44
46
|
}
|
|
45
47
|
const text = await memoPromise;
|
|
46
48
|
response
|
|
@@ -60,7 +62,9 @@ export function createUsageRouter(getFreshState) {
|
|
|
60
62
|
response.status(503).json({ error: "No data yet" });
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
63
|
-
response
|
|
65
|
+
response
|
|
66
|
+
.status(200)
|
|
67
|
+
.json(usage.map((entry) => toJsonObject(entry, state.refreshedAt.getTime())));
|
|
64
68
|
});
|
|
65
69
|
return router;
|
|
66
70
|
}
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
* Rate is shown after either {@link MIN_ELAPSED_FRACTION} of the period OR
|
|
7
7
|
* {@link MIN_ELAPSED_TIME_MS} has passed (whichever comes first).
|
|
8
8
|
*/
|
|
9
|
-
export declare function calculateUsageRate(utilization: number, resetsAt: Date | undefined, periodDurationMs: number): number | undefined;
|
|
9
|
+
export declare function calculateUsageRate(utilization: number, resetsAt: Date | undefined, periodDurationMs: number, now: number): number | undefined;
|
|
@@ -10,12 +10,11 @@ const MIN_ELAPSED_TIME_MS = 2 * 60 * 60 * 1000;
|
|
|
10
10
|
* Rate is shown after either {@link MIN_ELAPSED_FRACTION} of the period OR
|
|
11
11
|
* {@link MIN_ELAPSED_TIME_MS} has passed (whichever comes first).
|
|
12
12
|
*/
|
|
13
|
-
export function calculateUsageRate(utilization, resetsAt, periodDurationMs) {
|
|
13
|
+
export function calculateUsageRate(utilization, resetsAt, periodDurationMs, now) {
|
|
14
14
|
if (!resetsAt)
|
|
15
15
|
return undefined;
|
|
16
16
|
if (periodDurationMs <= 0)
|
|
17
17
|
return 0;
|
|
18
|
-
const now = Date.now();
|
|
19
18
|
const resetTime = resetsAt.getTime();
|
|
20
19
|
const periodStart = resetTime - periodDurationMs;
|
|
21
20
|
const elapsedTime = now - periodStart;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ServiceUsageData } from "../types/domain.js";
|
|
2
2
|
/**
|
|
3
3
|
* Formats service usage data as Prometheus text exposition using prom-client.
|
|
4
|
-
* Emits
|
|
4
|
+
* Emits gauges `axusage_utilization_percent` and `axusage_usage_rate` per window.
|
|
5
5
|
*/
|
|
6
|
-
export declare function formatPrometheusMetrics(data: readonly ServiceUsageData[]): Promise<string>;
|
|
6
|
+
export declare function formatPrometheusMetrics(data: readonly ServiceUsageData[], now: number): Promise<string>;
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { Gauge, Registry } from "prom-client";
|
|
2
|
+
import { calculateUsageRate } from "./calculate-usage-rate.js";
|
|
2
3
|
/**
|
|
3
4
|
* Formats service usage data as Prometheus text exposition using prom-client.
|
|
4
|
-
* Emits
|
|
5
|
+
* Emits gauges `axusage_utilization_percent` and `axusage_usage_rate` per window.
|
|
5
6
|
*/
|
|
6
|
-
export async function formatPrometheusMetrics(data) {
|
|
7
|
+
export async function formatPrometheusMetrics(data, now) {
|
|
7
8
|
const registry = new Registry();
|
|
8
|
-
const
|
|
9
|
+
const utilizationGauge = new Gauge({
|
|
9
10
|
name: "axusage_utilization_percent",
|
|
10
11
|
help: "Current utilization percentage by service/window",
|
|
11
12
|
labelNames: ["service", "window"],
|
|
12
13
|
registers: [registry],
|
|
13
14
|
});
|
|
15
|
+
const rateGauge = new Gauge({
|
|
16
|
+
name: "axusage_usage_rate",
|
|
17
|
+
help: "Usage rate (utilization / elapsed fraction of period); >1 means over budget",
|
|
18
|
+
labelNames: ["service", "window"],
|
|
19
|
+
registers: [registry],
|
|
20
|
+
});
|
|
14
21
|
for (const entry of data) {
|
|
15
22
|
for (const w of entry.windows) {
|
|
16
|
-
|
|
23
|
+
const labels = { service: entry.service, window: w.name };
|
|
24
|
+
utilizationGauge.set(labels, w.utilization);
|
|
25
|
+
const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs, now);
|
|
26
|
+
if (rate !== undefined) {
|
|
27
|
+
rateGauge.set(labels, rate);
|
|
28
|
+
}
|
|
17
29
|
}
|
|
18
30
|
}
|
|
19
31
|
return registry.metrics();
|
|
@@ -6,7 +6,7 @@ export declare function formatServiceUsageData(data: ServiceUsageData): string;
|
|
|
6
6
|
/**
|
|
7
7
|
* Converts service usage data to a plain JSON-serializable object
|
|
8
8
|
*/
|
|
9
|
-
export declare function toJsonObject(data: ServiceUsageData): unknown;
|
|
9
|
+
export declare function toJsonObject(data: ServiceUsageData, now: number): unknown;
|
|
10
10
|
/**
|
|
11
11
|
* Formats service usage data as JSON string
|
|
12
12
|
*/
|
|
@@ -34,10 +34,9 @@ function getUtilizationColor(rate) {
|
|
|
34
34
|
*/
|
|
35
35
|
function formatUsageWindow(window) {
|
|
36
36
|
const utilizationString = formatUtilization(window.utilization);
|
|
37
|
-
const rate = calculateUsageRate(window.utilization, window.resetsAt, window.periodDurationMs);
|
|
37
|
+
const rate = calculateUsageRate(window.utilization, window.resetsAt, window.periodDurationMs, Date.now());
|
|
38
38
|
const coloredUtilization = getUtilizationColor(rate)(utilizationString);
|
|
39
39
|
const resetTime = formatResetTime(window.resetsAt);
|
|
40
|
-
// Build full display string for rate to keep formatting consistent
|
|
41
40
|
const rateDisplay = rate === undefined ? "Not available" : `${rate.toFixed(2)}x rate`;
|
|
42
41
|
return `${chalk.bold(window.name)}:
|
|
43
42
|
Utilization: ${coloredUtilization} (${rateDisplay})
|
|
@@ -47,14 +46,17 @@ function formatUsageWindow(window) {
|
|
|
47
46
|
* Formats complete service usage data for human-readable display
|
|
48
47
|
*/
|
|
49
48
|
export function formatServiceUsageData(data) {
|
|
49
|
+
let statusWarning;
|
|
50
|
+
if (data.metadata?.limitReached === true) {
|
|
51
|
+
statusWarning = chalk.red("⚠ Rate limit reached");
|
|
52
|
+
}
|
|
53
|
+
else if (data.metadata?.allowed === false) {
|
|
54
|
+
statusWarning = chalk.red("⚠ Usage not allowed");
|
|
55
|
+
}
|
|
50
56
|
const header = [
|
|
51
57
|
chalk.cyan.bold(`=== ${data.service} Usage ===`),
|
|
52
58
|
data.planType ? chalk.gray(`Plan: ${data.planType}`) : undefined,
|
|
53
|
-
|
|
54
|
-
? chalk.red("⚠ Rate limit reached")
|
|
55
|
-
: data.metadata?.allowed === false
|
|
56
|
-
? chalk.red("⚠ Usage not allowed")
|
|
57
|
-
: undefined,
|
|
59
|
+
statusWarning,
|
|
58
60
|
]
|
|
59
61
|
.filter(Boolean)
|
|
60
62
|
.join("\n");
|
|
@@ -66,16 +68,20 @@ export function formatServiceUsageData(data) {
|
|
|
66
68
|
/**
|
|
67
69
|
* Converts service usage data to a plain JSON-serializable object
|
|
68
70
|
*/
|
|
69
|
-
export function toJsonObject(data) {
|
|
71
|
+
export function toJsonObject(data, now) {
|
|
70
72
|
return {
|
|
71
73
|
service: data.service,
|
|
72
74
|
planType: data.planType,
|
|
73
|
-
windows: data.windows.map((w) =>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
windows: data.windows.map((w) => {
|
|
76
|
+
const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs, now);
|
|
77
|
+
return {
|
|
78
|
+
name: w.name,
|
|
79
|
+
utilization: w.utilization,
|
|
80
|
+
rate,
|
|
81
|
+
resetsAt: w.resetsAt?.toISOString(),
|
|
82
|
+
periodDurationMs: w.periodDurationMs,
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
79
85
|
metadata: data.metadata,
|
|
80
86
|
};
|
|
81
87
|
}
|
|
@@ -84,7 +90,7 @@ export function toJsonObject(data) {
|
|
|
84
90
|
*/
|
|
85
91
|
export function formatServiceUsageDataAsJson(data) {
|
|
86
92
|
// eslint-disable-next-line unicorn/no-null -- JSON.stringify requires null for no replacer
|
|
87
|
-
return JSON.stringify(toJsonObject(data), null, 2);
|
|
93
|
+
return JSON.stringify(toJsonObject(data, Date.now()), null, 2);
|
|
88
94
|
}
|
|
89
95
|
const TSV_HEADER = "SERVICE\tPLAN\tWINDOW\tUTILIZATION\tRATE\tRESETS_AT";
|
|
90
96
|
/**
|
|
@@ -99,7 +105,7 @@ function sanitizeForTsv(value) {
|
|
|
99
105
|
*/
|
|
100
106
|
function formatServiceUsageRowsAsTsv(data) {
|
|
101
107
|
return data.windows.map((w) => {
|
|
102
|
-
const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs);
|
|
108
|
+
const rate = calculateUsageRate(w.utilization, w.resetsAt, w.periodDurationMs, Date.now());
|
|
103
109
|
return [
|
|
104
110
|
sanitizeForTsv(data.service),
|
|
105
111
|
sanitizeForTsv(data.planType ?? "-"),
|
package/package.json
CHANGED