diff --git a/docs/product/command-principles.md b/docs/product/command-principles.md
index f59a8821..912c8b18 100644
--- a/docs/product/command-principles.md
+++ b/docs/product/command-principles.md
@@ -107,6 +107,8 @@ The scaffolded config imports `definePrismaConfig` from `prisma/config` and spel
Resolve a service version and show or stream its logs.
+`dev logs` is the local counterpart: it reads the services of the app `prisma dev` runs from the current directory, so it sits under `dev` beside `dev url` and `dev stop` and `service logs` keeps meaning a deployed service. It takes the same `--tail` (default 100) and `--follow` (off by default) as `service logs`, so the two read alike.
+
### `delete` and `remove`
`delete` destroys a resource; `remove` detaches one thing from another without destroying it. A command that permanently destroys what it targets is spelled `delete` (`project delete`, `service delete`, `postgres delete`); `remove` is reserved for detachment.
diff --git a/docs/product/output-conventions.md b/docs/product/output-conventions.md
index 5ead36eb..563fd9a4 100644
--- a/docs/product/output-conventions.md
+++ b/docs/product/output-conventions.md
@@ -690,6 +690,11 @@ In human mode, `app logs` is a special case: raw app log lines are the primary
streamed payload and go to stdout so they can be piped or redirected. CLI
context, status, decoration, and errors stay on stderr.
+`dev logs` follows the same rule for a local app. Without an address it
+prefixes each line with `[address] ` so lines from several services stay
+attributable in a pipe; with one, lines are bare. In `--json` mode each line is
+an `output` frame whose `data` is `{ service, line }`.
+
## Data Conventions
- ids are opaque and stable
diff --git a/docs/reference/error-reference.md b/docs/reference/error-reference.md
index ed9fa939..1df274ba 100644
--- a/docs/reference/error-reference.md
+++ b/docs/reference/error-reference.md
@@ -18,7 +18,7 @@ Namespaces:
| `BRANCH` | Branch listing (`prisma branch`) |
| `BUCKET` | Bucket and bucket-key management (`prisma bucket`) |
| `CLI` | Engine-level invocation: arguments, config loading, prompts, consent, credentials, child processes |
-| `DEV` | Running an app on this machine with Composer (`prisma dev`, `prisma dev url`, `prisma dev stop`, `prisma emulator list`) |
+| `DEV` | Running an app on this machine with Composer (`prisma dev`, `prisma dev url`, `prisma dev logs`, `prisma dev stop`, `prisma emulator list`) |
| `FEEDBACK` | Sending product feedback (`prisma feedback`) |
| `GIT` | Git repository connections (`prisma git`) |
| `INIT` | Project initialization diagnostics (`prisma init`) |
@@ -223,23 +223,31 @@ The typed path routed to no command (stricli's route failure, mapped at the adap
## DEV
-These codes come from Composer, the command family behind `prisma dev`, `prisma dev url`, `prisma dev stop` and `prisma emulator list`. A node is one service, database or bucket in your app; its address is its name in the app, with dots for a nested node (`catalog.database`). Each node runs on a local emulator: a program on this machine that stands in for Prisma Cloud.
+These codes come from Composer, the command family behind `prisma dev`, `prisma dev url`, `prisma dev logs`, `prisma dev stop` and `prisma emulator list`. A node is one service, database or bucket in your app; its address is its name in the app, with dots for a nested node (`catalog.database`). Each node runs on a local emulator: a program on this machine that stands in for Prisma Cloud.
### DEV.ADDRESS_UNKNOWN
-`prisma dev url
` named an address that the app `prisma dev` last ran from this directory does not have. The `why` lists the addresses that are recorded for it. The fix is to pass one of those; a nested node has a dotted address, such as `catalog.database`. Meta: `address`, `known` (the recorded addresses).
+`prisma dev url ` or `prisma dev logs ` named an address that the app `prisma dev` last ran from this directory does not have. The `why` lists the addresses that are recorded for it. The fix is to pass one of those; a nested node has a dotted address, such as `catalog.database`. Meta: `address`, `known` (the recorded addresses).
### DEV.EMULATOR_FAILED
-A local emulator answered a request with an error. `prisma dev url`, `prisma dev stop` and `prisma dev` raise it when the emulator that runs a node fails a request. In `prisma emulator list` it is not a failure of the command: the emulator is listed with this code and the emulator's message instead of its instances, and the command still exits `0`. Meta: none.
+A local emulator answered a request with an error. `prisma dev url`, `prisma dev logs`, `prisma dev stop` and `prisma dev` raise it when the emulator that runs a node fails a request. In `prisma emulator list` it is not a failure of the command: the emulator is listed with this code and the emulator's message instead of its instances, and the command still exits `0`. Meta: none.
### DEV.EMULATOR_UNREACHABLE
-A local emulator could not be reached. `prisma dev url` raises it when the emulator that runs the node is not running; the fix is to run `prisma dev ` in this directory, which starts the emulators the app needs. It is also raised when an emulator is registered and its process is alive but it did not accept the connection; the fix then is to run the command again, and if it keeps failing, to end the emulator's process so the next `prisma dev` starts a new one. Meta: `address` and `kind` (the node's address and kind) when the emulator is not running; `url` (the emulator's address) when it did not accept the connection.
+A local emulator could not be reached. `prisma dev url` raises it when the emulator that runs the node is not running, and `prisma dev logs` when the emulator that runs the services is not running; the fix is to run `prisma dev ` in this directory, which starts the emulators the app needs. It is also raised when an emulator is registered and its process is alive but it did not accept the connection; the fix then is to run the command again, and if it keeps failing, to end the emulator's process so the next `prisma dev` starts a new one. Meta: `address` and `kind` (the node's address and kind) when the emulator is not running; `url` (the emulator's address) when it did not accept the connection.
### DEV.EMULATORS_DIR_INVALID
-Any command that reaches the local emulators (`prisma dev`, `prisma dev url`, `prisma dev stop`, `prisma emulator list`) found the `PRISMA_COMPOSER_EMULATORS_DIR` environment variable set to a relative path. Every process that starts or reaches the local emulators resolves this directory, and a relative path would name a different directory in each working directory. The fix is to set it to an absolute path (`~` is not expanded) or to unset it, which uses `~/.prisma-composer/emulators`. Meta: `variable`, `value`.
+Any command that reaches the local emulators (`prisma dev`, `prisma dev url`, `prisma dev logs`, `prisma dev stop`, `prisma emulator list`) found the `PRISMA_COMPOSER_EMULATORS_DIR` environment variable set to a relative path. Every process that starts or reaches the local emulators resolves this directory, and a relative path would name a different directory in each working directory. The fix is to set it to an absolute path (`~` is not expanded) or to unset it, which uses `~/.prisma-composer/emulators`. Meta: `variable`, `value`.
+
+### DEV.LOGS_UNSUPPORTED
+
+`prisma dev logs` without `--follow` ran against an app whose Composer packages are older than the Composer in this `prisma` CLI, and they cannot read a service's log without following it. The fix is to upgrade the app's Composer packages (`@prisma/composer`, `@prisma/composer-prisma-cloud`) to the version this CLI uses, or to pass `--follow`. Meta: none.
+
+### DEV.NO_LOGS
+
+`prisma dev logs ` named a database or a bucket. Only services have logs: what a service's process writes to stdout and stderr. The fix is to pass a service's address, or no address for every service. Meta: `address`, `kind`.
### DEV.NOT_RUNNING
@@ -247,7 +255,7 @@ Any command that reaches the local emulators (`prisma dev`, `prisma dev url`, `p
### DEV.NOT_STARTED_HERE
-`prisma dev url` or `prisma dev stop` ran in a directory where `prisma dev` has not run. An app's local services, databases and buckets are recorded in the directory `prisma dev` runs from, and there is no such record here. The fix is to run `prisma dev ` in this directory, or to run the command from the directory where `prisma dev` ran. Meta: `cwd`.
+`prisma dev url`, `prisma dev logs` or `prisma dev stop` ran in a directory where `prisma dev` has not run. An app's local services, databases and buckets are recorded in the directory `prisma dev` runs from, and there is no such record here. The fix is to run `prisma dev ` in this directory, or to run the command from the directory where `prisma dev` ran. Meta: `cwd`.
## FEEDBACK
diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts
index b66688a3..9c5caa51 100644
--- a/packages/cli/src/cli.ts
+++ b/packages/cli/src/cli.ts
@@ -348,7 +348,7 @@ export const cliGroups: Readonly<
orm: { brief: "Initialize a Prisma ORM project" },
dev: {
brief:
- "Run your app on this machine, with no Prisma account. Start it, print a node's local URL, stop it",
+ "Run your app on this machine, with no Prisma account. Start it, print a node's local URL, read its logs, stop it",
},
emulator: {
brief:
@@ -426,6 +426,7 @@ export const mountedCommands: Readonly> = {
deploy: composerCommandFamily.commands.deploy,
dev: composerCommandFamily.commands.dev,
"dev url": composerCommandFamily.commands["dev url"],
+ "dev logs": composerCommandFamily.commands["dev logs"],
"dev stop": composerCommandFamily.commands["dev stop"],
"emulator list": composerCommandFamily.commands["emulator list"],
// The ORM family. Written out per path: the shell owns the tree
diff --git a/packages/cli/tests/composer-mount.test.ts b/packages/cli/tests/composer-mount.test.ts
index d95a6cbc..1b5c4950 100644
--- a/packages/cli/tests/composer-mount.test.ts
+++ b/packages/cli/tests/composer-mount.test.ts
@@ -70,11 +70,22 @@ const ONE_INSTANCE = listing([
{ kind: "database", running: false, url: null, instances: [], error: null },
]);
-const COMPOSER_PATHS = ["dev", "dev url", "dev stop", "emulator list"];
+const COMPOSER_PATHS = [
+ "dev",
+ "dev url",
+ "dev logs",
+ "dev stop",
+ "emulator list",
+];
function shell(emulators: Listing = listing([])) {
const family = createComposerFamily({
- operations: createOperationsDouble().operations,
+ operations: createOperationsDouble({
+ devLogLines: [
+ { service: "catalog", line: "catalog ready" },
+ { service: "gateway", line: "GET / 200" },
+ ],
+ }).operations,
listEmulators: async () => emulators,
});
const commands = Object.fromEntries(
@@ -105,6 +116,16 @@ describe("Composer's commands in the shell", () => {
expect(result.stdout).toBe("http://localhost:3000\n");
});
+ it("pipes the log lines from dev logs, labelled by service", async () => {
+ const all = await shell().run(["dev", "logs"]);
+ const one = await shell().run(["dev", "logs", "gateway", "--tail", "20"]);
+
+ expect(all.exitCode).toBe(0);
+ expect(all.stdout).toBe("[catalog] catalog ready\n[gateway] GET / 200\n");
+ expect(one.exitCode).toBe(0);
+ expect(one.stdout).toBe("GET / 200\n");
+ });
+
it("pipes nothing from dev stop", async () => {
const result = await shell().run(["dev", "stop"]);
@@ -133,6 +154,7 @@ describe("Composer's commands in the shell", () => {
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("prisma-test dev");
expect(result.stdout).toContain("url ");
+ expect(result.stdout).toContain("logs");
expect(result.stdout).toContain("stop");
});
});
diff --git a/packages/cli/tests/e2e-coverage.test.ts b/packages/cli/tests/e2e-coverage.test.ts
index 46593825..a0b7aaed 100644
--- a/packages/cli/tests/e2e-coverage.test.ts
+++ b/packages/cli/tests/e2e-coverage.test.ts
@@ -107,6 +107,7 @@ const EXCLUSIONS: Readonly> = {
"Provisions real cloud infrastructure for an app entry point, through a child `alchemy deploy`. Standing that up per CI run is neither cheap nor unattended-safe.",
dev: "A session command: it runs until SIGINT or SIGTERM, redeploying on file change, so it has no happy path that terminates on its own.",
"dev url": LOCAL_EMULATOR_REASON,
+ "dev logs": LOCAL_EMULATOR_REASON,
"dev stop": LOCAL_EMULATOR_REASON,
"emulator list": LOCAL_EMULATOR_REASON,
};
diff --git a/packages/cli/tests/mount-coverage.test.ts b/packages/cli/tests/mount-coverage.test.ts
index 5d86dc13..1a7eddeb 100644
--- a/packages/cli/tests/mount-coverage.test.ts
+++ b/packages/cli/tests/mount-coverage.test.ts
@@ -100,6 +100,7 @@ const EXPECTED_MOUNT_PATHS: readonly string[] = [
"db verify",
"deploy",
"dev",
+ "dev logs",
"dev stop",
"dev url",
"emulator list",