diff --git a/docs/commands/agents.md b/docs/commands/agents.md index 1f6cafdee62..215b3c64aee 100644 --- a/docs/commands/agents.md +++ b/docs/commands/agents.md @@ -21,9 +21,9 @@ netlify agents **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in | Subcommand | description | |:--------------------------- |:-----| @@ -91,12 +91,12 @@ netlify agents:list **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - output result as JSON - `project` (*string*) - project ID or name (if not in a linked directory) - `status` (*string*) - filter by status (new, running, done, error, cancelled) -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** @@ -123,11 +123,11 @@ netlify agents:show **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - output result as JSON - `project` (*string*) - project ID or name (if not in a linked directory) -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** @@ -153,11 +153,11 @@ netlify agents:stop **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - output result as JSON - `project` (*string*) - project ID or name (if not in a linked directory) -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** diff --git a/docs/commands/api.md b/docs/commands/api.md index a5561037f92..f5b74c676f8 100644 --- a/docs/commands/api.md +++ b/docs/commands/api.md @@ -24,10 +24,10 @@ netlify api **Flags** -- `data` (*string*) - Data to use -- `list` (*boolean*) - List out available API methods - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `data` (*string*) - Data to use +- `list` (*boolean*) - List out available API methods **Examples** diff --git a/docs/commands/blobs.md b/docs/commands/blobs.md index a0a6fb52b4f..a9c6b3d82cb 100644 --- a/docs/commands/blobs.md +++ b/docs/commands/blobs.md @@ -18,9 +18,9 @@ netlify blobs **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in | Subcommand | description | |:--------------------------- |:-----| @@ -59,10 +59,10 @@ netlify blobs:delete **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Bypasses prompts & Force the command to run. - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Bypasses prompts & Force the command to run. --- ## `blobs:get` @@ -82,10 +82,10 @@ netlify blobs:get **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `output` (*string*) - Defines the filesystem path where the blob data should be persisted - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `output` (*string*) - Defines the filesystem path where the blob data should be persisted --- ## `blobs:list` @@ -104,12 +104,12 @@ netlify blobs:list **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `directories` (*boolean*) - Indicates that keys with the '/' character should be treated as directories, returning a list of sub-directories at a given level rather than all the keys inside them - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output list contents as JSON - `prefix` (*string*) - A string for filtering down the entries; when specified, only the entries whose key starts with that prefix are returned -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in --- ## `blobs:set` @@ -130,11 +130,11 @@ netlify blobs:set **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Bypasses prompts & Force the command to run. - `input` (*string*) - Defines the filesystem path where the blob data should be read from -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in --- diff --git a/docs/commands/build.md b/docs/commands/build.md index 2b345292b36..1da787629f2 100644 --- a/docs/commands/build.md +++ b/docs/commands/build.md @@ -17,11 +17,11 @@ netlify build **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `context` (*string*) - Specify a deploy context for environment variables read during the build (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch (default: value of CONTEXT or ”production”) - `dry` (*boolean*) - Dry run: show instructions without running them - `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `offline` (*boolean*) - Disables any features that require network access **Examples** diff --git a/docs/commands/clone.md b/docs/commands/clone.md index d67d38b62b5..c82806c8769 100644 --- a/docs/commands/clone.md +++ b/docs/commands/clone.md @@ -28,11 +28,11 @@ netlify clone **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `id` (*string*) - ID of existing Netlify project to link to - `name` (*string*) - Name of existing Netlify project to link to -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** diff --git a/docs/commands/completion.md b/docs/commands/completion.md index 71e9ee5d967..c460b785bfb 100644 --- a/docs/commands/completion.md +++ b/docs/commands/completion.md @@ -46,9 +46,9 @@ netlify completion:install **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in --- diff --git a/docs/commands/db.md b/docs/commands/db.md index de8ca48e83c..dbf21b4d75f 100644 --- a/docs/commands/db.md +++ b/docs/commands/db.md @@ -19,9 +19,9 @@ netlify db **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in | Subcommand | description | |:--------------------------- |:-----| @@ -51,12 +51,12 @@ netlify init **Flags** - `assume-no` (*boolean*) - Non-interactive setup. Does not initialize any third-party tools/boilerplate. Ideal for CI environments or AI tools. -- `boilerplate` (*drizzle*) - Type of boilerplate to add to your project. -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `overwrite` (*boolean*) - Overwrites existing files that would be created when setting up boilerplate - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `boilerplate` (*drizzle*) - Type of boilerplate to add to your project. +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `no-boilerplate` (*boolean*) - Don't add any boilerplate to your project. +- `overwrite` (*boolean*) - Overwrites existing files that would be created when setting up boilerplate **Examples** diff --git a/docs/commands/dev.md b/docs/commands/dev.md index cba068156ce..a873f732a89 100644 --- a/docs/commands/dev.md +++ b/docs/commands/dev.md @@ -78,10 +78,10 @@ netlify dev:exec **Flags** -- `context` (*string*) - Specify a deploy context for environment variables (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch (default: dev) -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `context` (*string*) - Specify a deploy context for environment variables (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch (default: dev) +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in **Examples** diff --git a/docs/commands/env.md b/docs/commands/env.md index f21e118241a..3aa7b84789b 100644 --- a/docs/commands/env.md +++ b/docs/commands/env.md @@ -18,9 +18,9 @@ netlify env **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in | Subcommand | description | |:--------------------------- |:-----| @@ -56,12 +56,12 @@ netlify env:clone **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Bypasses prompts & Force the command to run. - `from` (*string*) - Project ID (From) - `to` (*string*) - Project ID (To) -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** @@ -87,11 +87,11 @@ netlify env:get **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `context` (*string*) - Specify a deploy context for environment variables (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output environment variables as JSON -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `scope` (*builds | functions | post-processing | runtime | any*) - Specify a scope **Examples** @@ -120,11 +120,11 @@ netlify env:import **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output environment variables as JSON - `replace-existing` (*boolean*) - Replace all existing variables instead of merging them with the current ones -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in --- ## `env:list` @@ -139,13 +139,13 @@ netlify env:list **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `context` (*string*) - Specify a deploy context for environment variables (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `json` (*boolean*) - Output environment variables as JSON -- `scope` (*builds | functions | post-processing | runtime | any*) - Specify a scope -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `plain` (*boolean*) - Output environment variables as plaintext +- `scope` (*builds | functions | post-processing | runtime | any*) - Specify a scope **Examples** @@ -214,12 +214,12 @@ netlify env:unset **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `context` (*string*) - Specify a deploy context for environment variables (”production”, ”deploy-preview”, ”branch-deploy”, ”dev”) or `branch:your-branch` where `your-branch` is the name of a branch (default: all contexts) - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Bypasses prompts & Force the command to run. - `json` (*boolean*) - Output environment variables as JSON -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** diff --git a/docs/commands/functions.md b/docs/commands/functions.md index 11f5da7b1ba..fc30702f885 100644 --- a/docs/commands/functions.md +++ b/docs/commands/functions.md @@ -19,9 +19,9 @@ netlify functions **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in | Subcommand | description | |:--------------------------- |:-----| @@ -52,11 +52,11 @@ netlify functions:build **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `functions` (*string*) - Specify a functions directory to build to - `src` (*string*) - Specify the source directory for the functions -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in --- ## `functions:create` @@ -75,13 +75,13 @@ netlify functions:create **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `language` (*string*) - function language - `name` (*string*) - function name - `offline` (*boolean*) - Disables any features that require network access - `url` (*string*) - pull template from URL -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** @@ -149,11 +149,11 @@ netlify functions:list **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `functions` (*string*) - Specify a functions directory to list - `json` (*boolean*) - Output function data as JSON -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in --- ## `functions:serve` @@ -168,12 +168,12 @@ netlify functions:serve **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `functions` (*string*) - Specify a functions directory to serve - `offline` (*boolean*) - Disables any features that require network access - `port` (*string*) - Specify a port for the functions server -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in --- diff --git a/docs/commands/init.md b/docs/commands/init.md index 9aa21e7e4fe..7521d8b24c0 100644 --- a/docs/commands/init.md +++ b/docs/commands/init.md @@ -18,12 +18,12 @@ netlify init **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `force` (*boolean*) - Reinitialize CI hooks if the linked project is already configured to use CI - `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin" - `manual` (*boolean*) - Manually configure a git remote for CI -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in diff --git a/docs/commands/link.md b/docs/commands/link.md index 5d1b233e7b6..f7ad7656510 100644 --- a/docs/commands/link.md +++ b/docs/commands/link.md @@ -18,13 +18,13 @@ netlify link **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `git-remote-name` (*string*) - Name of Git remote to use. e.g. "origin" - `git-remote-url` (*string*) - URL of the repository (or Github `owner/repo`) to link to - `id` (*string*) - ID of project to link to - `name` (*string*) - Name of project to link to -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in **Examples** diff --git a/docs/commands/login.md b/docs/commands/login.md index 2fdd7b4865b..4059fd1cfc1 100644 --- a/docs/commands/login.md +++ b/docs/commands/login.md @@ -19,9 +19,9 @@ netlify login **Flags** -- `new` (*boolean*) - Login to new Netlify account - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `new` (*boolean*) - Login to new Netlify account diff --git a/docs/commands/logs.md b/docs/commands/logs.md index 8c761e85b25..3b3426c2afb 100644 --- a/docs/commands/logs.md +++ b/docs/commands/logs.md @@ -17,13 +17,14 @@ netlify logs **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in | Subcommand | description | |:--------------------------- |:-----| | [`logs:deploy`](/commands/logs#logsdeploy) | Stream the logs of deploys currently being built to the console | +| [`logs:edge-functions`](/commands/logs#logsedge-functions) | Stream netlify edge function logs to the console | | [`logs:function`](/commands/logs#logsfunction) | Stream netlify function logs to the console | @@ -33,6 +34,8 @@ netlify logs netlify logs:deploy netlify logs:function netlify logs:function my-function +netlify logs:edge-functions +netlify logs:edge-functions --deploy-id ``` --- @@ -48,9 +51,40 @@ netlify logs:deploy **Flags** +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in + +--- +## `logs:edge-functions` + +Stream netlify edge function logs to the console + +**Usage** + +```bash +netlify logs:edge-functions +``` + +**Flags** + - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `deploy-id` (*string*) - Deploy ID to stream edge function logs for +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `from` (*string*) - Start date for historical logs (ISO 8601 format) +- `level` (*string*) - Log levels to stream. Choices are: trace, debug, info, warn, error, fatal +- `to` (*string*) - End date for historical logs (ISO 8601 format, defaults to now) + +**Examples** + +```bash +netlify logs:edge-functions +netlify logs:edge-functions --deploy-id +netlify logs:edge-functions --from 2026-01-01T00:00:00Z +netlify logs:edge-functions --from 2026-01-01T00:00:00Z --to 2026-01-02T00:00:00Z +netlify logs:edge-functions -l info warn +``` --- ## `logs:function` @@ -65,21 +99,27 @@ netlify logs:function **Arguments** -- functionName - Name of the function to stream logs for +- functionName - Name or ID of the function to stream logs for **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `level` (*string*) - Log levels to stream. Choices are: trace, debug, info, warn, error, fatal - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `deploy-id` (*string*) - Deploy ID to look up the function from +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `from` (*string*) - Start date for historical logs (ISO 8601 format) +- `level` (*string*) - Log levels to stream. Choices are: trace, debug, info, warn, error, fatal +- `to` (*string*) - End date for historical logs (ISO 8601 format, defaults to now) **Examples** ```bash netlify logs:function netlify logs:function my-function +netlify logs:function my-function --deploy-id netlify logs:function my-function -l info warn +netlify logs:function my-function --from 2026-01-01T00:00:00Z +netlify logs:function my-function --from 2026-01-01T00:00:00Z --to 2026-01-02T00:00:00Z ``` --- diff --git a/docs/commands/open.md b/docs/commands/open.md index fc15acdd7a9..1accfa6af95 100644 --- a/docs/commands/open.md +++ b/docs/commands/open.md @@ -18,10 +18,10 @@ netlify open **Flags** - `admin` (*boolean*) - Open Netlify project -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `site` (*boolean*) - Open project - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `site` (*boolean*) - Open project | Subcommand | description | |:--------------------------- |:-----| @@ -51,9 +51,9 @@ netlify open:admin **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in **Examples** @@ -74,9 +74,9 @@ netlify open:site **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in **Examples** diff --git a/docs/commands/recipes.md b/docs/commands/recipes.md index dcd1ed9ced1..29f5ddbd8a7 100644 --- a/docs/commands/recipes.md +++ b/docs/commands/recipes.md @@ -21,9 +21,9 @@ netlify recipes **Flags** -- `name` (*string*) - recipe name to use - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `name` (*string*) - recipe name to use | Subcommand | description | |:--------------------------- |:-----| @@ -50,9 +50,9 @@ netlify recipes:list **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in **Examples** diff --git a/docs/commands/sites.md b/docs/commands/sites.md index d11c84a135c..62e2e4e53b3 100644 --- a/docs/commands/sites.md +++ b/docs/commands/sites.md @@ -19,9 +19,9 @@ netlify sites **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in | Subcommand | description | |:--------------------------- |:-----| @@ -80,11 +80,11 @@ netlify sites:create-template **Flags** - `account-slug` (*string*) - account slug to create the project under +- `debug` (*boolean*) - Print debugging information +- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `name` (*string*) - name of project - `url` (*string*) - template url -- `debug` (*boolean*) - Print debugging information -- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in - `with-ci` (*boolean*) - initialize CI hooks during project creation **Examples** @@ -113,10 +113,10 @@ netlify sites:delete **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `force` (*boolean*) - Delete without prompting (useful for CI). - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `force` (*boolean*) - Delete without prompting (useful for CI). **Examples** @@ -137,10 +137,10 @@ netlify sites:list **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in -- `json` (*boolean*) - Output project data as JSON - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in +- `json` (*boolean*) - Output project data as JSON --- diff --git a/docs/commands/status.md b/docs/commands/status.md index 9529891046a..8a76f13fe28 100644 --- a/docs/commands/status.md +++ b/docs/commands/status.md @@ -18,10 +18,10 @@ netlify status **Flags** -- `json` (*boolean*) - Output status information as JSON -- `verbose` (*boolean*) - Output system info - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `json` (*boolean*) - Output status information as JSON +- `verbose` (*boolean*) - Output system info | Subcommand | description | |:--------------------------- |:-----| @@ -41,9 +41,9 @@ netlify status:hooks **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in --- diff --git a/docs/commands/unlink.md b/docs/commands/unlink.md index 030a0506d15..c44e1f780e9 100644 --- a/docs/commands/unlink.md +++ b/docs/commands/unlink.md @@ -18,9 +18,9 @@ netlify unlink **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in diff --git a/docs/commands/watch.md b/docs/commands/watch.md index ac7062847d5..95a4c45727d 100644 --- a/docs/commands/watch.md +++ b/docs/commands/watch.md @@ -18,9 +18,9 @@ netlify watch **Flags** -- `filter` (*string*) - For monorepos, specify the name of the application to run the command in - `debug` (*boolean*) - Print debugging information - `auth` (*string*) - Netlify auth token - can be used to run this command without logging in +- `filter` (*string*) - For monorepos, specify the name of the application to run the command in **Examples** diff --git a/docs/index.md b/docs/index.md index ee86844d08a..9d981a2c306 100644 --- a/docs/index.md +++ b/docs/index.md @@ -132,6 +132,7 @@ Stream logs from your project | Subcommand | description | |:--------------------------- |:-----| | [`logs:deploy`](/commands/logs#logsdeploy) | Stream the logs of deploys currently being built to the console | +| [`logs:edge-functions`](/commands/logs#logsedge-functions) | Stream netlify edge function logs to the console | | [`logs:function`](/commands/logs#logsfunction) | Stream netlify function logs to the console | diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index 5b847a8654d..c6250819699 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -551,6 +551,8 @@ const runDeploy = async ({ functionLogsUrl: string edgeFunctionLogsUrl: string sourceZipFileName?: string + deployedFunctions: { name: string; id: string }[] + hasEdgeFunctions: boolean }> => { let results let deployId = existingDeployId @@ -662,6 +664,13 @@ const runDeploy = async ({ edgeFunctionLogsUrl += `?scope=deployid:${deployId}` } + const availableFunctions = (results.deploy.available_functions ?? []) as { n?: string; oid?: string }[] + const deployedFunctions = availableFunctions + .filter((fn): fn is { n: string; oid: string } => Boolean(fn.n && fn.oid)) + .map((fn) => ({ name: fn.n, id: fn.oid })) + + const hasEdgeFunctions = (results.edgeFunctionsCount ?? 0) > 0 + return { siteId: results.deploy.site_id, siteName: results.deploy.name, @@ -672,6 +681,8 @@ const runDeploy = async ({ functionLogsUrl, edgeFunctionLogsUrl, sourceZipFileName: uploadSourceZipResult?.sourceZipFileName, + deployedFunctions, + hasEdgeFunctions, } } @@ -779,6 +790,7 @@ interface JsonData { logs: string function_logs: string edge_function_logs: string + deployed_functions: { name: string; id: string }[] url?: string source_zip_filename?: string } @@ -796,10 +808,18 @@ const printResults = ({ results: Awaited> runBuildCommand: boolean }): void => { - const msgData: Record = { + const buildLogsData: Record = { 'Build logs': terminalLink(results.logsUrl, results.logsUrl, { fallback: false }), + } + + const functionLogsData: Record = { 'Function logs': terminalLink(results.functionLogsUrl, results.functionLogsUrl, { fallback: false }), - 'Edge function Logs': terminalLink(results.edgeFunctionLogsUrl, results.edgeFunctionLogsUrl, { fallback: false }), + 'Function CLI': `netlify logs:function --deploy-id ${results.deployId} `, + } + + const edgeFunctionLogsData: Record = { + 'Edge function logs': terminalLink(results.edgeFunctionLogsUrl, results.edgeFunctionLogsUrl, { fallback: false }), + 'Edge function CLI': `netlify logs:edge-functions --deploy-id ${results.deployId}`, } log('') @@ -816,6 +836,7 @@ const printResults = ({ logs: results.logsUrl, function_logs: results.functionLogsUrl, edge_function_logs: results.edgeFunctionLogsUrl, + deployed_functions: results.deployedFunctions, } if (deployToProduction) { jsonData.url = results.siteUrl @@ -847,7 +868,22 @@ const printResults = ({ }), ) - log(prettyjson.render(msgData)) + log(prettyjson.render(buildLogsData)) + + if (results.deployedFunctions.length > 0) { + log() + log(prettyjson.render(functionLogsData)) + } + + if (results.hasEdgeFunctions) { + log() + log(prettyjson.render(edgeFunctionLogsData)) + } + + if (results.deployedFunctions.length > 0 || results.hasEdgeFunctions) { + log() + log(chalk.dim('Use --from and --to to fetch historical logs (ISO 8601 format)')) + } if (!deployToProduction) { log() diff --git a/src/commands/logs/edge-functions.ts b/src/commands/logs/edge-functions.ts new file mode 100644 index 00000000000..6737118af8d --- /dev/null +++ b/src/commands/logs/edge-functions.ts @@ -0,0 +1,98 @@ +import { OptionValues } from 'commander' +import inquirer from 'inquirer' + +import { chalk, log } from '../../utils/command-helpers.js' +import { getWebSocket } from '../../utils/websockets/index.js' +import type BaseCommand from '../base-command.js' + +import { parseDateToMs, fetchHistoricalLogs, printHistoricalLogs, formatLogEntry } from './log-api.js' +import { CLI_LOG_LEVEL_CHOICES_STRING, LOG_LEVELS_LIST } from './log-levels.js' +import { getName } from './build.js' + +export const logsEdgeFunction = async (options: OptionValues, command: BaseCommand) => { + let deployId = options.deployId as string | undefined + await command.authenticate() + + const client = command.netlify.api + const { site } = command.netlify + const { id: siteId } = site + + if (!siteId) { + log('You must link a project before attempting to view edge function logs') + return + } + + const levels = options.level as string[] | undefined + if (levels && !levels.every((level) => LOG_LEVELS_LIST.includes(level))) { + log(`Invalid log level. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING.toString()}`) + } + + const levelsToPrint: string[] = levels || LOG_LEVELS_LIST + + if (options.from) { + const fromMs = parseDateToMs(options.from as string) + const toMs = options.to ? parseDateToMs(options.to as string) : Date.now() + + const url = `https://analytics.services.netlify.com/v2/sites/${siteId}/edge_function_logs?from=${fromMs.toString()}&to=${toMs.toString()}` + const data = await fetchHistoricalLogs({ url, accessToken: client.accessToken ?? '' }) + printHistoricalLogs(data, levelsToPrint) + return + } + + const userId = command.netlify.globalConfig.get('userId') as string + + if (!deployId) { + const deploys = await client.listSiteDeploys({ siteId }) + + if (deploys.length === 0) { + log('No deploys found for the project') + return + } + + if (deploys.length === 1) { + deployId = deploys[0].id + } else { + const { result } = (await inquirer.prompt({ + name: 'result', + type: 'list', + message: `Select a deploy\n\n${chalk.yellow('*')} indicates a deploy created by you`, + choices: deploys.map((deploy) => ({ + name: getName({ deploy, userId }), + value: deploy.id, + })), + })) as { result: string } + + deployId = result + } + } + + const ws = getWebSocket('wss://socketeer.services.netlify.com/edge-function/logs') + + ws.on('open', () => { + ws.send( + JSON.stringify({ + deploy_id: deployId, + site_id: siteId, + access_token: client.accessToken, + since: new Date().toISOString(), + }), + ) + }) + + ws.on('message', (data: string) => { + const logData = JSON.parse(data) as { level: string; message: string; timestamp?: string } + if (!levelsToPrint.includes(logData.level.toLowerCase())) { + return + } + log(formatLogEntry(logData)) + }) + + ws.on('close', () => { + log('Connection closed') + }) + + ws.on('error', (err: Error) => { + log('Connection error') + log(err.message) + }) +} diff --git a/src/commands/logs/functions.ts b/src/commands/logs/functions.ts index 54fa8f54599..22d0932cda4 100644 --- a/src/commands/logs/functions.ts +++ b/src/commands/logs/functions.ts @@ -5,6 +5,7 @@ import { chalk, log } from '../../utils/command-helpers.js' import { getWebSocket } from '../../utils/websockets/index.js' import type BaseCommand from '../base-command.js' +import { parseDateToMs, fetchHistoricalLogs, printHistoricalLogs } from './log-api.js' import { CLI_LOG_LEVEL_CHOICES_STRING, LOG_LEVELS, LOG_LEVELS_LIST } from './log-levels.js' function getLog(logData: { level: string; message: string }) { @@ -28,8 +29,10 @@ function getLog(logData: { level: string; message: string }) { } export const logsFunction = async (functionName: string | undefined, options: OptionValues, command: BaseCommand) => { + await command.authenticate() + const client = command.netlify.api - const { site } = command.netlify + const { site, siteInfo } = command.netlify const { id: siteId } = site if (options.level && !options.level.every((level: string) => LOG_LEVELS_LIST.includes(level))) { @@ -38,17 +41,24 @@ export const logsFunction = async (functionName: string | undefined, options: Op const levelsToPrint = options.level || LOG_LEVELS_LIST - // TODO: Update type once the open api spec is updated https://open-api.netlify.com/#tag/function/operation/searchSiteFunctions - const { functions = [] } = (await client.searchSiteFunctions({ siteId: siteId! })) as any + let functions: any[] + if (options.deployId) { + const deploy = (await client.getSiteDeploy({ siteId: siteId!, deployId: options.deployId })) as any + functions = deploy.available_functions ?? [] + } else { + // TODO: Update type once the open api spec is updated https://open-api.netlify.com/#tag/function/operation/searchSiteFunctions + const result = (await client.searchSiteFunctions({ siteId: siteId! })) as any + functions = result.functions ?? [] + } if (functions.length === 0) { - log(`No functions found for the project`) + log(`No functions found for the ${options.deployId ? 'deploy' : 'project'}`) return } let selectedFunction if (functionName) { - selectedFunction = functions.find((fn: any) => fn.n === functionName) + selectedFunction = functions.find((fn: any) => fn.n === functionName || fn.oid === functionName) } else { const { result } = await inquirer.prompt({ name: 'result', @@ -65,7 +75,18 @@ export const logsFunction = async (functionName: string | undefined, options: Op return } - const { a: accountId, oid: functionId } = selectedFunction + const { a: accountId, n: resolvedFunctionName, oid: functionId } = selectedFunction + + if (options.from) { + const fromMs = parseDateToMs(options.from) + const toMs = options.to ? parseDateToMs(options.to) : Date.now() + const branch = siteInfo.build_settings?.repo_branch ?? 'main' + + const url = `https://analytics.services.netlify.com/v2/sites/${siteId}/branch/${branch}/function_logs/${resolvedFunctionName}?from=${fromMs.toString()}&to=${toMs.toString()}` + const data = await fetchHistoricalLogs({ url, accessToken: client.accessToken ?? '' }) + printHistoricalLogs(data, levelsToPrint) + return + } const ws = getWebSocket('wss://socketeer.services.netlify.com/function/logs') diff --git a/src/commands/logs/index.ts b/src/commands/logs/index.ts index 3c1be8415ac..d0053ed1db6 100644 --- a/src/commands/logs/index.ts +++ b/src/commands/logs/index.ts @@ -22,11 +22,17 @@ export const createLogsFunctionCommand = (program: BaseCommand) => { .addOption( new Option('-l, --level ', `Log levels to stream. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`), ) - .addArgument(new Argument('[functionName]', 'Name of the function to stream logs for')) + .addOption(new Option('--deploy-id ', 'Deploy ID to look up the function from')) + .addOption(new Option('--from ', 'Start date for historical logs (ISO 8601 format)')) + .addOption(new Option('--to ', 'End date for historical logs (ISO 8601 format, defaults to now)')) + .addArgument(new Argument('[functionName]', 'Name or ID of the function to stream logs for')) .addExamples([ 'netlify logs:function', 'netlify logs:function my-function', + 'netlify logs:function my-function --deploy-id ', 'netlify logs:function my-function -l info warn', + 'netlify logs:function my-function --from 2026-01-01T00:00:00Z', + 'netlify logs:function my-function --from 2026-01-01T00:00:00Z --to 2026-01-02T00:00:00Z', ]) .description('Stream netlify function logs to the console') .action(async (functionName: string | undefined, options: OptionValues, command: BaseCommand) => { @@ -35,15 +41,46 @@ export const createLogsFunctionCommand = (program: BaseCommand) => { }) } +export const createLogsEdgeFunctionCommand = (program: BaseCommand) => { + program + .command('logs:edge-functions') + .addOption( + new Option('-l, --level ', `Log levels to stream. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`), + ) + .addOption(new Option('--deploy-id ', 'Deploy ID to stream edge function logs for')) + .addOption(new Option('--from ', 'Start date for historical logs (ISO 8601 format)')) + .addOption(new Option('--to ', 'End date for historical logs (ISO 8601 format, defaults to now)')) + .addExamples([ + 'netlify logs:edge-functions', + 'netlify logs:edge-functions --deploy-id ', + 'netlify logs:edge-functions --from 2026-01-01T00:00:00Z', + 'netlify logs:edge-functions --from 2026-01-01T00:00:00Z --to 2026-01-02T00:00:00Z', + 'netlify logs:edge-functions -l info warn', + ]) + .description('Stream netlify edge function logs to the console') + .action(async (options: OptionValues, command: BaseCommand) => { + const { logsEdgeFunction } = await import('./edge-functions.js') + await logsEdgeFunction(options, command) + }) +} + export const createLogsCommand = (program: BaseCommand) => { createLogsBuildCommand(program) createLogsFunctionCommand(program) + createLogsEdgeFunctionCommand(program) + return program .command('logs') .alias('log') .description('Stream logs from your project') - .addExamples(['netlify logs:deploy', 'netlify logs:function', 'netlify logs:function my-function']) + .addExamples([ + 'netlify logs:deploy', + 'netlify logs:function', + 'netlify logs:function my-function', + 'netlify logs:edge-functions', + 'netlify logs:edge-functions --deploy-id ', + ]) .action((_, command: BaseCommand) => command.help()) } diff --git a/src/commands/logs/log-api.ts b/src/commands/logs/log-api.ts new file mode 100644 index 00000000000..bd8312d2579 --- /dev/null +++ b/src/commands/logs/log-api.ts @@ -0,0 +1,71 @@ +import { chalk, log } from '../../utils/command-helpers.js' +import { LOG_LEVELS } from './log-levels.js' + +export function parseDateToMs(dateString: string): number { + const ms = new Date(dateString).getTime() + if (Number.isNaN(ms)) { + throw new Error(`Invalid date: ${dateString}`) + } + return ms +} + +export async function fetchHistoricalLogs({ + url, + accessToken, +}: { + url: string + accessToken: string +}): Promise { + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as { error?: string } + throw new Error(errorData.error ?? `HTTP ${response.status.toString()}: ${response.statusText}`) + } + + return response.json() +} + +export function formatLogEntry(entry: { timestamp?: string; level?: string; message?: string }): string { + const timestamp = entry.timestamp ? new Date(entry.timestamp).toISOString() : '' + let levelString = entry.level ?? '' + + switch (levelString.toUpperCase()) { + case LOG_LEVELS.INFO: + levelString = chalk.blueBright(levelString) + break + case LOG_LEVELS.WARN: + levelString = chalk.yellowBright(levelString) + break + case LOG_LEVELS.ERROR: + levelString = chalk.redBright(levelString) + break + default: + break + } + + const parts = [timestamp, levelString, entry.message ?? ''].filter(Boolean) + return parts.join(' ') +} + +export function printHistoricalLogs(data: unknown, levelsToPrint: string[]): void { + const entries = Array.isArray(data) ? (data as { timestamp?: string; level?: string; message?: string }[]) : [] + + if (entries.length === 0) { + log('No logs found for the specified time range') + return + } + + for (const entry of entries) { + const level = (entry.level ?? '').toLowerCase() + if (levelsToPrint.length > 0 && !levelsToPrint.includes(level)) { + continue + } + log(formatLogEntry(entry)) + } +} diff --git a/src/utils/deploy/deploy-site.ts b/src/utils/deploy/deploy-site.ts index 438bf7387de..6b76c95e45f 100644 --- a/src/utils/deploy/deploy-site.ts +++ b/src/utils/deploy/deploy-site.ts @@ -221,6 +221,7 @@ For more information, visit https://ntl.fyi/cli-native-modules.`) deployId, deploy, uploadList, + edgeFunctionsCount, } return deployManifest } diff --git a/tests/integration/commands/logs/edge-functions.test.ts b/tests/integration/commands/logs/edge-functions.test.ts new file mode 100644 index 00000000000..adebf139ac3 --- /dev/null +++ b/tests/integration/commands/logs/edge-functions.test.ts @@ -0,0 +1,224 @@ +import { Mock, afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest' + +import BaseCommand from '../../../../src/commands/base-command.js' +import { createLogsEdgeFunctionCommand } from '../../../../src/commands/logs/index.js' +import { LOG_LEVELS } from '../../../../src/commands/logs/log-levels.js' +import { log } from '../../../../src/utils/command-helpers.js' +import { getWebSocket } from '../../../../src/utils/websockets/index.js' +import { startMockApi } from '../../utils/mock-api-vitest.js' +import { getEnvironmentVariables } from '../../utils/mock-api.js' + +vi.mock('../../../../src/utils/websockets/index.js', () => ({ + getWebSocket: vi.fn(), +})) + +vi.mock('../../../../src/utils/command-helpers.js', async () => { + const actual = await vi.importActual('../../../../src/utils/command-helpers.js') + return { + ...actual, + log: vi.fn(), + } +}) + +vi.mock('inquirer', () => ({ + default: { + prompt: vi.fn().mockResolvedValue({ result: 'deploy-id-1' }), + registerPrompt: vi.fn(), + }, +})) + +const siteInfo = { + admin_url: 'https://app.netlify.com/projects/site-name/overview', + ssl_url: 'https://site-name.netlify.app/', + id: 'site_id', + name: 'site-name', + build_settings: { repo_url: 'https://github.com/owner/repo' }, +} + +const routes = [ + { + path: 'accounts', + response: [{ slug: 'test-account' }], + }, + { + path: 'sites', + response: [], + }, + { path: 'sites/site_id', response: siteInfo }, + { path: 'sites/site_id/service-instances', response: [] }, + { + path: 'user', + response: { name: 'test user', slug: 'test-user', email: 'user@test.com' }, + }, + { + path: 'sites/site_id/deploys', + response: [ + { + id: 'deploy-id-1', + context: 'production', + user_id: 'user-1', + review_id: null, + }, + ], + }, +] + +describe('logs:edge-functions command', () => { + const originalEnv = process.env + + let program: BaseCommand + + afterEach(() => { + vi.clearAllMocks() + process.env = { ...originalEnv } + }) + + beforeEach(() => { + program = new BaseCommand('netlify') + + createLogsEdgeFunctionCommand(program) + }) + + afterAll(() => { + vi.restoreAllMocks() + vi.resetModules() + + process.env = { ...originalEnv } + }) + + test('should setup the edge functions stream correctly', async () => { + const { apiUrl } = await startMockApi({ routes }) + const spyWebsocket = getWebSocket as unknown as Mock + const spyOn = vi.fn() + const spySend = vi.fn() + spyWebsocket.mockReturnValue({ + on: spyOn, + send: spySend, + }) + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + await program.parseAsync(['', '', 'logs:edge-functions']) + + expect(spyWebsocket).toHaveBeenCalledOnce() + expect(spyWebsocket).toHaveBeenCalledWith('wss://socketeer.services.netlify.com/edge-function/logs') + expect(spyOn).toHaveBeenCalledTimes(4) + }) + + test('should send the correct payload to the websocket', async () => { + const { apiUrl } = await startMockApi({ routes }) + const spyWebsocket = getWebSocket as unknown as Mock + const spyOn = vi.fn() + const spySend = vi.fn() + spyWebsocket.mockReturnValue({ + on: spyOn, + send: spySend, + }) + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + await program.parseAsync(['', '', 'logs:edge-functions']) + + const setupCall = spyOn.mock.calls.find((args) => args[0] === 'open') + expect(setupCall).toBeDefined() + + const openCallback = setupCall?.[1] as (() => void) | undefined + openCallback?.() + + expect(spySend).toHaveBeenCalledOnce() + const call = spySend.mock.calls[0] as string[] + const body = JSON.parse(call[0]) as Record + + expect(body.deploy_id).toEqual('deploy-id-1') + expect(body.site_id).toEqual('site_id') + expect(body.access_token).toEqual(env.NETLIFY_AUTH_TOKEN) + expect(body.since).toBeDefined() + }) + + test('should use deploy ID from --deploy-id option when provided', async () => { + const { apiUrl } = await startMockApi({ routes }) + const spyWebsocket = getWebSocket as unknown as Mock + const spyOn = vi.fn() + const spySend = vi.fn() + spyWebsocket.mockReturnValue({ + on: spyOn, + send: spySend, + }) + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + await program.parseAsync(['', '', 'logs:edge-functions', '--deploy-id', 'my-deploy-id']) + + const setupCall = spyOn.mock.calls.find((args) => args[0] === 'open') + const openCallback = setupCall?.[1] as (() => void) | undefined + openCallback?.() + + const call = spySend.mock.calls[0] as string[] + const body = JSON.parse(call[0]) as Record + + expect(body.deploy_id).toEqual('my-deploy-id') + }) + + test('should print only specified log levels', async () => { + const { apiUrl } = await startMockApi({ routes }) + const spyWebsocket = getWebSocket as unknown as Mock + const spyOn = vi.fn() + const spySend = vi.fn() + spyWebsocket.mockReturnValue({ + on: spyOn, + send: spySend, + }) + const spyLog = log as unknown as Mock + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + await program.parseAsync(['', '', 'logs:edge-functions', '--level', 'info']) + const messageCallback = spyOn.mock.calls.find((args) => args[0] === 'message') + const messageCallbackFunc = messageCallback?.[1] as ((data: string) => void) | undefined + + messageCallbackFunc?.(JSON.stringify({ level: LOG_LEVELS.INFO, message: 'Hello World' })) + messageCallbackFunc?.(JSON.stringify({ level: LOG_LEVELS.WARN, message: 'There was a warning' })) + + expect(spyLog).toHaveBeenCalledTimes(1) + }) + + test('should fetch historical logs when --from is specified', async () => { + const { apiUrl } = await startMockApi({ routes }) + const spyWebsocket = getWebSocket as unknown as Mock + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + const mockLogs = [{ timestamp: '2026-01-15T10:00:00Z', level: 'info', message: 'Edge function executed' }] + + const originalFetch = global.fetch + const spyFetch = vi.fn().mockImplementation((url: string) => { + if (url.includes('analytics.services.netlify.com')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockLogs), + }) + } + return originalFetch(url) + }) + global.fetch = spyFetch + + try { + await program.parseAsync(['', '', 'logs:edge-functions', '--from', '2026-01-01T00:00:00Z']) + + expect(spyWebsocket).not.toHaveBeenCalled() + const analyticsCall = spyFetch.mock.calls.find((args: string[]) => + args[0].includes('analytics.services.netlify.com'), + ) + expect(analyticsCall).toBeDefined() + expect((analyticsCall as string[])[0]).toContain('edge_function_logs') + expect((analyticsCall as string[])[0]).toContain('site_id') + } finally { + global.fetch = originalFetch + } + }) +}) diff --git a/tests/integration/commands/logs/functions.test.ts b/tests/integration/commands/logs/functions.test.ts index f7c740a11c2..d663ec38820 100644 --- a/tests/integration/commands/logs/functions.test.ts +++ b/tests/integration/commands/logs/functions.test.ts @@ -201,4 +201,106 @@ describe('logs:function command', () => { expect(spyLog).toHaveBeenCalledTimes(2) }) + + test('should find function by ID', async ({}) => { + const { apiUrl } = await startMockApi({ routes }) + const spyWebsocket = getWebSocket as unknown as Mock + const spyOn = vi.fn() + const spySend = vi.fn() + spyWebsocket.mockReturnValue({ + on: spyOn, + send: spySend, + }) + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + await program.parseAsync(['', '', 'logs:function', 'function-id']) + + const setupCall = spyOn.mock.calls.find((args) => args[0] === 'open') + const openCallback = setupCall?.[1] + openCallback?.() + + const call = spySend.mock.calls[0] + const body = JSON.parse(call[0]) + + expect(body.function_id).toEqual('function-id') + expect(body.site_id).toEqual('site_id') + }) + + test('should look up function from deploy when --deploy is specified', async ({}) => { + const deployRoutes = [ + ...routes, + { + path: 'sites/site_id/deploys/deploy-123', + response: { + id: 'deploy-123', + available_functions: [{ n: 'deploy-function', oid: 'deploy-fn-id' }], + }, + }, + ] + const { apiUrl } = await startMockApi({ routes: deployRoutes }) + const spyWebsocket = getWebSocket as unknown as Mock + const spyOn = vi.fn() + const spySend = vi.fn() + spyWebsocket.mockReturnValue({ + on: spyOn, + send: spySend, + }) + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + await program.parseAsync(['', '', 'logs:function', 'deploy-function', '--deploy-id', 'deploy-123']) + + const setupCall = spyOn.mock.calls.find((args) => args[0] === 'open') + const openCallback = setupCall?.[1] + openCallback?.() + + const call = spySend.mock.calls[0] + const body = JSON.parse(call[0]) + + expect(body.function_id).toEqual('deploy-fn-id') + expect(body.site_id).toEqual('site_id') + }) + + test('should fetch historical logs when --from is specified', async ({}) => { + const { apiUrl } = await startMockApi({ routes }) + const spyWebsocket = getWebSocket as unknown as Mock + + const env = getEnvironmentVariables({ apiUrl }) + Object.assign(process.env, env) + + const mockLogs = [{ timestamp: '2026-01-15T10:00:00Z', level: 'info', message: 'Function executed' }] + + const originalFetch = global.fetch + const spyFetch = vi.fn().mockImplementation((url: string) => { + const parsedUrl = new URL(url) + if (parsedUrl.hostname === 'analytics.services.netlify.com') { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(mockLogs), + }) + } + return originalFetch(url) + }) + global.fetch = spyFetch + + try { + await program.parseAsync(['', '', 'logs:function', 'cool-function', '--from', '2026-01-01T00:00:00Z']) + + expect(spyWebsocket).not.toHaveBeenCalled() + + const analyticsCall = spyFetch.mock.calls.find((args: string[]) => { + const parsedUrl = new URL(args[0]) + return parsedUrl.hostname === 'analytics.services.netlify.com' + }) + expect(analyticsCall).toBeDefined() + expect((analyticsCall as string[])[0]).toContain('function_logs') + expect((analyticsCall as string[])[0]).toContain('cool-function') + expect((analyticsCall as string[])[0]).toContain('site_id') + } finally { + global.fetch = originalFetch + } + }) })