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.
@@ -97,12 +97,15 @@ export async function serveCommand(options) {
97
97
  process.exit(1);
98
98
  }, 5000);
99
99
  forceExit.unref();
100
- server.stop().then(() => {
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
  }
@@ -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.status(200).json(usage.map((entry) => toJsonObject(entry)));
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 a gauge `axusage_utilization_percent{service,window}` per window.
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 a gauge `axusage_utilization_percent{service,window}` per window.
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 gauge = new Gauge({
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
- gauge.set({ service: entry.service, window: w.name }, w.utilization);
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
- data.metadata?.limitReached === true
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
- name: w.name,
75
- utilization: w.utilization,
76
- resetsAt: w.resetsAt?.toISOString(),
77
- periodDurationMs: w.periodDurationMs,
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
@@ -2,7 +2,7 @@
2
2
  "name": "axusage",
3
3
  "author": "Łukasz Jerciński",
4
4
  "license": "MIT",
5
- "version": "3.5.0",
5
+ "version": "3.6.0",
6
6
  "description": "Monitor API usage across Claude, ChatGPT, GitHub Copilot, and Gemini from a single CLI",
7
7
  "repository": {
8
8
  "type": "git",