UNPKG

@ably/cli

Version:

Ably CLI for Pub/Sub, Chat and Spaces

186 lines (185 loc) 8.39 kB
import { Flags } from "@oclif/core"; import chalk from "chalk"; import { AblyBaseCommand } from "../../../base-command.js"; import { waitUntilInterruptedOrTimeout } from "../../../utils/long-running.js"; export default class LogsAppSubscribe extends AblyBaseCommand { static description = "Subscribe to live app logs"; static examples = [ "$ ably logs app subscribe", "$ ably logs app subscribe --type channel.lifecycle", "$ ably logs app subscribe --json", "$ ably logs app subscribe --pretty-json", "$ ably logs app subscribe --duration 30", ]; static flags = { ...AblyBaseCommand.globalFlags, duration: Flags.integer({ description: "Automatically exit after the given number of seconds (0 = run indefinitely)", char: "D", required: false, }), type: Flags.string({ description: "Filter by log type", options: [ "channel.lifecycle", "channel.occupancy", "channel.presence", "connection.lifecycle", "push.publish", ], }), }; cleanupInProgress = false; client = null; async properlyCloseAblyClient() { if (!this.client || this.client.connection.state === 'closed' || this.client.connection.state === 'failed') { return; } return new Promise((resolve) => { const timeout = setTimeout(() => { resolve(); }, 2000); const onClosedOrFailed = () => { clearTimeout(timeout); resolve(); }; this.client.connection.once('closed', onClosedOrFailed); this.client.connection.once('failed', onClosedOrFailed); this.client.close(); }); } // Override finally to ensure resources are cleaned up async finally(err) { await this.properlyCloseAblyClient(); return super.finally(err); } async run() { const { flags } = await this.parse(LogsAppSubscribe); let channel = null; let subscribedEvents = []; try { this.client = await this.createAblyRealtimeClient(flags); if (!this.client) return; const client = this.client; // Set up connection state logging this.setupConnectionStateLogging(client, flags, { includeUserFriendlyMessages: true }); // Get the logs channel const appConfig = await this.ensureAppAndKey(flags); if (!appConfig) { this.error("Unable to determine app configuration"); return; } const logsChannelName = `[meta]log`; channel = client.channels.get(logsChannelName); // Set up channel state logging this.setupChannelStateLogging(channel, flags, { includeUserFriendlyMessages: true }); // Determine which log types to subscribe to const logTypes = flags.type ? [flags.type] : [ "channel.lifecycle", "channel.occupancy", "channel.presence", "connection.lifecycle", "push.publish", ]; this.logCliEvent(flags, "logs", "subscribing", `Subscribing to log events: ${logTypes.join(", ")}`, { logTypes, channel: logsChannelName }); if (!this.shouldOutputJson(flags)) { this.log(`${chalk.green("Subscribing to app logs:")} ${chalk.cyan(logTypes.join(", "))}`); } // Subscribe to specified log types for (const logType of logTypes) { channel.subscribe(logType, (message) => { const timestamp = message.timestamp ? new Date(message.timestamp).toISOString() : new Date().toISOString(); const event = { type: logType, timestamp, data: message.data, id: message.id, }; this.logCliEvent(flags, "logs", "logReceived", `Log received: ${logType}`, event); if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput(event, flags)); } else { this.log(`${chalk.gray(`[${timestamp}]`)} ${chalk.cyan(`Type: ${logType}`)}`); if (message.data !== null && message.data !== undefined) { this.log(`${chalk.green("Data:")} ${JSON.stringify(message.data, null, 2)}`); } this.log(""); // Empty line for better readability } }); subscribedEvents.push(logType); } this.logCliEvent(flags, "logs", "listening", "Listening for log events. Press Ctrl+C to exit."); if (!this.shouldOutputJson(flags)) { this.log("Listening for log events. Press Ctrl+C to exit."); } // Wait until the user interrupts or the optional duration elapses const effectiveDuration = typeof flags.duration === "number" && flags.duration > 0 ? flags.duration : process.env.ABLY_CLI_DEFAULT_DURATION ? Number(process.env.ABLY_CLI_DEFAULT_DURATION) : undefined; const exitReason = await waitUntilInterruptedOrTimeout(effectiveDuration); this.logCliEvent(flags, "logs", "runComplete", "Exiting wait loop", { exitReason }); this.cleanupInProgress = exitReason === "signal"; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); this.logCliEvent(flags, "logs", "fatalError", `Error during logs subscription: ${errorMsg}`, { error: errorMsg }); if (this.shouldOutputJson(flags)) { this.log(this.formatJsonOutput({ error: errorMsg, success: false }, flags)); } else { this.error(`Error: ${errorMsg}`); } } finally { // Wrap all cleanup in a timeout to prevent hanging await Promise.race([ this.performCleanup(flags || {}, channel, subscribedEvents), new Promise((resolve) => { setTimeout(() => { this.logCliEvent(flags || {}, "logs", "cleanupTimeout", "Cleanup timed out after 5s, forcing completion"); resolve(); }, 5000); }) ]); if (!this.shouldOutputJson(flags || {})) { if (this.cleanupInProgress) { this.log(chalk.green("Graceful shutdown complete (user interrupt).")); } else { this.log(chalk.green("Duration elapsed – command finished cleanly.")); } } } } async performCleanup(flags, channel, subscribedEvents) { // Unsubscribe from log events with timeout if (channel && subscribedEvents.length > 0) { for (const eventType of subscribedEvents) { try { await Promise.race([ Promise.resolve(channel.unsubscribe(eventType)), new Promise((resolve) => setTimeout(resolve, 1000)) ]); this.logCliEvent(flags, "logs", "unsubscribedEvent", `Unsubscribed from ${eventType}`); } catch (error) { this.logCliEvent(flags, "logs", "unsubscribeError", `Error unsubscribing from ${eventType}: ${error instanceof Error ? error.message : String(error)}`); } } } // Close Ably client (already has internal timeout) this.logCliEvent(flags, "connection", "closingClientFinally", "Closing Ably client."); await this.properlyCloseAblyClient(); this.logCliEvent(flags, "connection", "clientClosedFinally", "Ably client close attempt finished."); } }