@ably/cli
Version:
Ably CLI for Pub/Sub, Chat and Spaces
269 lines (268 loc) • 12 kB
JavaScript
import { Flags } from "@oclif/core";
import chalk from "chalk";
import { AblyBaseCommand } from "../../base-command.js";
import { StatsDisplay } from "../../services/stats-display.js";
export default class ConnectionsStats extends AblyBaseCommand {
static description = "View connection statistics for an Ably app";
static examples = [
"$ ably connections stats",
"$ ably connections stats --unit hour",
"$ ably connections stats --start 1618005600000 --end 1618091999999",
"$ ably connections stats --limit 10",
"$ ably connections stats --json",
"$ ably connections stats --pretty-json",
"$ ably connections stats --live",
];
static flags = {
...AblyBaseCommand.globalFlags,
debug: Flags.boolean({
default: false,
description: "Show debug information for live stats polling",
}),
end: Flags.integer({
description: "End time in milliseconds since epoch",
}),
interval: Flags.integer({
default: 6,
description: "Polling interval in seconds (only used with --live)",
}),
limit: Flags.integer({
default: 10,
description: "Maximum number of stats records to return",
}),
live: Flags.boolean({
default: false,
description: "Subscribe to live stats updates (uses minute interval)",
}),
start: Flags.integer({
description: "Start time in milliseconds since epoch",
}),
unit: Flags.string({
default: "minute",
description: "Time unit for stats",
options: ["minute", "hour", "day", "month"],
}),
};
client = null;
isPolling = false;
pollInterval = undefined; // Use NodeJS.Timeout
statsDisplay = null; // Track when we're already fetching stats
// Override finally to ensure resources are cleaned up
async finally(err) {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = undefined;
}
// No need to close REST client explicitly
return super.finally(err);
}
async run() {
const { flags } = await this.parse(ConnectionsStats);
// For live stats, enforce minute interval
if (flags.live && flags.unit !== "minute") {
this.logCliEvent(flags, "stats", "liveIntervalOverride", "Live stats only support minute intervals. Using minute interval.");
this.warn("Live stats only support minute intervals. Using minute interval.");
flags.unit = "minute";
}
// Get API key from flags or config
const apiKey = flags["api-key"] || (await this.configManager.getApiKey());
if (!apiKey) {
this.error('No API key found. Please set an API key using "ably keys add" or set the ABLY_API_KEY environment variable.');
return;
}
// Create stats display
this.statsDisplay = new StatsDisplay({
intervalSeconds: flags.interval,
isConnectionStats: true,
json: this.shouldOutputJson(flags),
live: flags.live,
startTime: flags.live ? new Date() : undefined,
unit: flags.unit,
});
await (flags.live ? this.runLiveStats(flags) : this.runOneTimeStats(flags));
}
async runLiveStats(flags) {
try {
const client = await this.createAblyRestClient(flags);
if (!client) {
return;
}
this.logCliEvent(flags, "stats", "liveSubscribeStarting", "Subscribing to live connection stats...");
if (!this.shouldOutputJson(flags)) {
this.log("Subscribing to live connection stats...");
}
// Setup graceful shutdown
const cleanup = () => {
this.logCliEvent(flags, "stats", "liveCleanupInitiated", "Cleanup initiated for live stats");
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = undefined;
}
if (!this.shouldOutputJson(flags)) {
this.log("\nUnsubscribed from live stats");
}
};
process.on("SIGINT", cleanup);
process.on("SIGTERM", cleanup);
// Show stats immediately before starting polling
await this.fetchAndDisplayStats(flags, client);
// Poll for stats at the specified interval
this.pollInterval = setInterval(() => {
if (!this.isPolling) {
this.pollStats(flags, client);
}
else if (flags.debug) {
this.logCliEvent(flags, "stats", "pollSkipped", "Skipping poll - previous request still in progress");
// Only show this message if debug flag is enabled
console.log(chalk.yellow("Skipping poll - previous request still in progress"));
}
}, (flags.interval || 6) * 1000);
this.logCliEvent(flags, "stats", "liveListening", "Now listening for live stats updates");
// Keep the process running
await new Promise(() => {
// This promise is intentionally never resolved
// The process will exit via the SIGINT/SIGTERM handlers
});
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "stats", "liveSetupError", `Error setting up live stats: ${errorMsg}`, { error: errorMsg });
this.error(`Error setting up live stats: ${errorMsg}`);
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
}
}
async runOneTimeStats(flags) {
// Calculate time range based on the unit
const now = new Date();
let startTime = new Date();
switch (flags.unit) {
case "minute": {
startTime = new Date(now.getTime() - 60 * 60 * 1000); // 1 hour ago for minutes
break;
}
case "hour": {
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago for hours
break;
}
case "day": {
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago for days
break;
}
default: {
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago for months
}
}
// Prepare query parameters
const params = {
direction: "backwards",
end: flags.end ?? now.getTime(),
limit: flags.limit,
start: flags.start ?? startTime.getTime(),
unit: flags.unit,
};
this.logCliEvent(flags, "stats", "oneTimeFetchRequest", "Fetching one-time stats with parameters", { params });
try {
const client = await this.createAblyRestClient(flags);
if (!client) {
return;
}
// Get stats
const statsPage = await client.stats(params);
const stats = statsPage.items;
this.logCliEvent(flags, "stats", "oneTimeFetchResponse", `Received ${stats.length} stats records`, { count: stats.length, stats });
if (stats.length === 0) {
this.logCliEvent(flags, "stats", "noStatsAvailable", "No connection stats available for the requested period");
if (!this.shouldOutputJson(flags)) {
this.log("No connection stats available.");
}
return;
}
// Display stats using the StatsDisplay class
this.statsDisplay.display(stats[0]); // Display only the latest/first record for simplicity
// If you need to display all records for one-time stats, you'll need to adjust StatsDisplay or loop here.
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "stats", "oneTimeFetchError", `Failed to fetch one-time stats: ${errorMsg}`, { error: errorMsg });
this.error(`Failed to fetch stats: ${errorMsg}`);
}
}
async fetchAndDisplayStats(flags, client) {
try {
// Calculate time range based on the unit
const now = new Date();
let startTime = new Date();
switch (flags.unit) {
case "minute": {
startTime = new Date(now.getTime() - 60 * 60 * 1000); // 1 hour ago for minutes
break;
}
case "hour": {
startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago for hours
break;
}
case "day": {
startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7 days ago for days
break;
}
default: {
startTime = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 days ago for months
}
}
// Prepare query parameters
const params = {
direction: "backwards",
end: now.getTime(),
limit: flags.live ? 1 : flags.limit,
start: startTime.getTime(),
unit: flags.unit,
};
this.logCliEvent(flags, "stats", "fetchRequest", "Fetching stats with parameters", { params });
// Get stats
const statsPage = await client.stats(params);
const stats = statsPage.items;
this.logCliEvent(flags, "stats", "fetchResponse", `Received ${stats.length} stats records`, { count: stats.length, stats });
if (stats.length === 0) {
this.logCliEvent(flags, "stats", "noStatsAvailable", "No connection stats available for the requested period");
if (!flags.live && !this.shouldOutputJson(flags)) {
this.log("No connection stats available.");
}
return;
}
// Display stats using the StatsDisplay class
this.statsDisplay.display(stats[0]);
// Display each stat interval
for (const stat of stats) {
this.statsDisplay.display(stat);
}
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "stats", "fetchError", `Failed to fetch stats: ${errorMsg}`, { error: errorMsg });
this.error(`Failed to fetch stats: ${errorMsg}`);
}
}
async pollStats(flags, client) {
try {
this.isPolling = true;
this.logCliEvent(flags, "stats", "pollStarting", "Polling for new stats...");
if (flags.debug) {
console.log(chalk.dim(`[${new Date().toISOString()}] Polling for new stats...`));
}
await this.fetchAndDisplayStats(flags, client);
this.logCliEvent(flags, "stats", "pollSuccess", "Successfully polled and displayed stats");
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
this.logCliEvent(flags, "stats", "pollError", `Error during stats polling: ${errorMsg}`, { error: errorMsg });
if (flags.debug) {
console.error(chalk.red(`Error during stats polling: ${errorMsg}`));
}
}
finally {
this.isPolling = false;
}
}
}