diff --git a/README.md b/README.md index 0be9238..7150188 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ used below: Will by default use the instance name for service name, which defaults to `pup`. You can override by passing `--name my-custom-name`. -5. To stream the logs from a running instance, use the command `pup monitor`. To show historic logs, use `pup logs`. +5. To view logs from a running instance, use the command `pup logs`, which shows historical logs and then streams new ones in real-time. To show only historical logs, use `pup logs --no-follow`. Will by default use the instance name for service name, which defaults to `pup`. You can override by passing `--name my-custom-name`. diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 15af353..df57741 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -11,6 +11,8 @@ All notable changes to this project will be documented in this section. ## [Unreleased] +- feat(cli): `pup logs` now streams logs by default (shows historical logs then streams new ones), matching behavior of similar tools like pm2. Use `--no-follow` to show only historical logs without + streaming. - feat(core): Add exponential backoff for process restarts via `restartBackoffMs` configuration option to prevent rapid restart loops ## [1.0.4] - 2024-11-19 diff --git a/docs/src/usage/basics.md b/docs/src/usage/basics.md index 25eaad3..5752a86 100644 --- a/docs/src/usage/basics.md +++ b/docs/src/usage/basics.md @@ -43,19 +43,36 @@ pup enable-service --config path/to/config-file ## Viewing Logs -Pup enables you to inspect its internally stored logs through the `logs` command, or live stream the logs using the `monitor` command. Both options supports arguments to help filter the logs and -customize the output: +Pup enables you to inspect its internally stored logs and stream real-time logs using the `logs` command, or use the `monitor` command for live streaming only. Both options support arguments to help +filter the logs and customize the output: -### Arguments to both `logs` and `monitor` +### The `logs` command + +By default, the `logs` command displays historical logs and then streams new logs in real-time (similar to `pm2 logs`). This is useful for monitoring your processes during development or debugging +production issues. + +```bash +# Show historical logs and stream new ones +pup logs + +# Show logs for a specific process +pup logs --id my-process + +# Show only historical logs without streaming +pup logs --no-follow +``` + +### Arguments for `logs` and `monitor` - `--id `: (optional) Allows filtering of logs based on the process ID. - `--severity `: (optional) Enables filtering logs based on the severity level. The acceptable severity levels include error, warning, info, and log. -### ´logs´ only +### Additional arguments for `logs` only -- `-n`: (optional) Defines the number of log entries to display. +- `-n `: (optional) Defines the number of historical log entries to display before streaming. - `--start `: (optional) Allows you to display logs that were generated after a specified timestamp. The timestamp should be in the ISO8601 format. - `--end `: (optional) Lets you display logs generated before a particular timestamp. The timestamp should be in the ISO8601 format. +- `--no-follow`: (optional) Display only historical logs without streaming new ones. > **Note**: The internal logger keeps logs for a default period of 24 hours. You can modify this setting via the global logger configuration. { .note } @@ -70,6 +87,9 @@ pup logs --config path/to/config-file # or pup logs --cwd path/where/config/file/is + +# Show only the last 50 historical logs without streaming +pup logs -n 50 --no-follow ``` ## Controlling running instances diff --git a/lib/cli/args.ts b/lib/cli/args.ts index 4e6cc82..cd63f31 100644 --- a/lib/cli/args.ts +++ b/lib/cli/args.ts @@ -28,8 +28,9 @@ function parseArguments(args: string[]): ArgsParser { "d": "cwd", "upgrade": "update", "e": "env", + "f": "follow", } - const boolean = ["setup", "upgrade", "help", "version", "autostart", "dry-run"] + const boolean = ["setup", "upgrade", "help", "version", "autostart", "dry-run", "follow", "no-follow"] return new ArgsParser(args, { aliases, boolean }) } diff --git a/lib/cli/main.ts b/lib/cli/main.ts index 0c3df73..bb3437c 100644 --- a/lib/cli/main.ts +++ b/lib/cli/main.ts @@ -427,8 +427,16 @@ async function main() { /** * Base argument: logs + * + * By default, shows historical logs and then streams new logs (like pm2 logs). + * Use --no-follow to show only historical logs without streaming. */ if (baseArgument === "logs") { + // Determine if we should follow (stream) logs + // Default to true unless --no-follow is explicitly set + const shouldFollow = !checkedArgs.getBoolean("no-follow") + + // Display historical logs first const logStore = `${await toPersistentPath(configFile as string)}/.main.db` const logger = new Logger(configuration!.logger || {}, logStore) await logger.init() @@ -442,8 +450,10 @@ async function main() { (!checkedArgs.get("severity") || checkedArgs.get("severity") === "") ? undefined : checkedArgs.get("severity")!.toLowerCase(), numberOfRows, ) + + const logWithColors = configuration!.logger?.colors ?? true + if (logs && logs.length > 0) { - const logWithColors = configuration!.logger?.colors ?? true for (const log of logs) { const { processId, severity, category, timeStamp, text } = log const isStdErr = severity === "error" || category === "stderr" @@ -466,9 +476,77 @@ async function main() { logFn(decoratedLogText) } } - } else { + } else if (!shouldFollow) { console.error("No logs found.") } + + // If we should follow, stream new logs + if (shouldFollow) { + if (!client) { + console.error("Could not create API client for log streaming.") + return exit(1) + } + + // Test the client connection + try { + const responseState = await client.getState() + if (!responseState?.data) { + console.error("Could not contact the Pup instance for log streaming.") + exit(1) + } + } catch (_e) { + console.error("Could not contact the Pup instance for log streaming.") + exit(1) + } + + // Set up log handler for streaming + const processFilter = checkedArgs.get("id")?.toLowerCase() + const severityFilter = checkedArgs.get("severity")?.toLowerCase() + const logHandler = (logEntry: ApiLogItem) => { + try { + const { processId, severity, category, timeStamp, text } = logEntry + + // Filter by severity if specified + if (severityFilter && severity.toLowerCase() !== severityFilter) return + // Filter by processId if specified + if (processFilter && processId !== processFilter) return + + const isStdErr = severity === "error" || category === "stderr" + const decoratedLogText = `${new Date(timeStamp).toISOString()} [${severity.toUpperCase()}] [${processId || "core"}:${category}] ${text}` + let color = null + // Apply coloring rules + if (logWithColors) { + if (processId === "core") color = "gray" + if (category === "starting") color = "green" + if (category === "finished") color = "yellow" + if (isStdErr) color = "red" + } + let logFn = console.log + if (severity === "warn") logFn = console.warn + if (severity === "info") logFn = console.info + if (severity === "error") logFn = console.error + if (color !== null) { + logFn(`%c${decoratedLogText}`, `color: ${color}`) + } else { + logFn(decoratedLogText) + } + } catch (_e) { + console.error("Error in log streamer: " + _e) + } + } + + // Output status + console.log(`\nStreaming logs... Abort with CTRL+C.`) + + // Start streaming logs + client.on("log", logHandler as EventHandler) + + // Wait indefinitely + await new Promise((resolve) => setTimeout(resolve, 365 * 24 * 60 * 60 * 1000)) + + exit(0) + } + return exit(0) } diff --git a/test/cli/args.test.ts b/test/cli/args.test.ts index 076c09d..c53ba20 100644 --- a/test/cli/args.test.ts +++ b/test/cli/args.test.ts @@ -142,3 +142,23 @@ test("checkArguments should throw error when both --cmd and -- is specified", () "'--cmd', '--worker' and '--' cannot be used at the same time.", ) }) + +test("Boolean option --no-follow is parsed correctly", () => { + const inputArgs = ["logs", "--no-follow"] + const parsedArgs = parseArguments(inputArgs) + assertEquals(parsedArgs.getBoolean("no-follow"), true) + assertEquals(parsedArgs.getLoose().includes("logs"), true) +}) + +test("Boolean option --follow/-f is parsed correctly", () => { + const inputArgs = ["logs", "-f"] + const parsedArgs = parseArguments(inputArgs) + assertEquals(parsedArgs.getBoolean("follow"), true) + assertEquals(parsedArgs.getLoose().includes("logs"), true) +}) + +test("logs command with --no-follow flag should be valid", () => { + const args = new ArgsParser(["logs", "--no-follow"], { boolean: ["no-follow"] }) + // Should not throw + checkArguments(args) +})