Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
2 changes: 2 additions & 0 deletions docs/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 25 additions & 5 deletions docs/src/usage/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <process-id>`: (optional) Allows filtering of logs based on the process ID.
- `--severity <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 <number>`: (optional) Defines the number of historical log entries to display before streaming.
- `--start <iso860-timestamp>`: (optional) Allows you to display logs that were generated after a specified timestamp. The timestamp should be in the ISO8601 format.
- `--end <iso860-timestamp>`: (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 }

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}

Expand Down
82 changes: 80 additions & 2 deletions lib/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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"
Expand All @@ -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<unknown>)

// Wait indefinitely
await new Promise((resolve) => setTimeout(resolve, 365 * 24 * 60 * 60 * 1000))

exit(0)
}

return exit(0)
}

Expand Down
20 changes: 20 additions & 0 deletions test/cli/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})