Skip to content
Open
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
23 changes: 23 additions & 0 deletions docs/commands/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@
- `timeout` (*string*) - Timeout to wait for deployment to finish
- `trigger` (*boolean*) - Trigger a new build of your project on Netlify without uploading local files

| Subcommand | description |
|:--------------------------- |:-----|
| [`deploy:logs`](/commands/deploy#deploylogs) | Stream the logs of deploys currently being built to the console |

Check warning on line 53 in docs/commands/deploy.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'Subcommand'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'Subcommand'?", "location": {"path": "docs/commands/deploy.md", "range": {"start": {"line": 53, "column": 3}}}, "severity": "WARNING"}


**Examples**

```bash
Expand All @@ -64,5 +69,23 @@
netlify deploy --create-site my-new-site --team my-team # Create site and deploy
```

---
## `deploy:logs`

Stream the logs of deploys currently being built to the console

**Usage**

```bash
netlify deploy: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

---

<!-- AUTO-GENERATED-CONTENT:END -->
26 changes: 26 additions & 0 deletions docs/commands/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
| [`functions:create`](/commands/functions#functionscreate) | Create a new function locally |
| [`functions:invoke`](/commands/functions#functionsinvoke) | Trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions |
| [`functions:list`](/commands/functions#functionslist) | List functions that exist locally |
| [`functions:logs`](/commands/functions#functionslogs) | Stream netlify function logs to the console |
| [`functions:serve`](/commands/functions#functionsserve) | Serve functions locally |


Expand Down Expand Up @@ -155,6 +156,31 @@
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

---
## `functions:logs`

Stream netlify function logs to the console

**Usage**

```bash
netlify functions:logs
```

**Arguments**

- functionName - Name or ID of the function to stream logs for

Check warning on line 172 in docs/commands/functions.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'functionName'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'functionName'?", "location": {"path": "docs/commands/functions.md", "range": {"start": {"line": 172, "column": 3}}}, "severity": "WARNING"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resolve docs lint warnings in the new argument/flag text.

Line 172 (functionName) and Line 176 (“look up”) will keep triggering lint-docs warnings; rewording here will keep docs clean.

✏️ Proposed doc-only fix
-- functionName - Name or ID of the function to stream logs for
+- function-name - Name or ID of the function to stream logs for
...
-- `deploy-id` (*string*) - Deploy ID to look up the function from
+- `deploy-id` (*string*) - Deploy ID to find the function from

Also applies to: 176-176

🧰 Tools
🪛 GitHub Check: lint-docs

[warning] 172-172:
[vale] reported by reviewdog 🐶
[base.spelling] Spellcheck: did you really mean 'functionName'?

Raw Output:
{"message": "[base.spelling] Spellcheck: did you really mean 'functionName'?", "location": {"path": "docs/commands/functions.md", "range": {"start": {"line": 172, "column": 3}}}, "severity": "WARNING"}

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/commands/functions.md` at line 172, The docs text for the functionName
argument is triggering lint warnings; update the parameter description and the
“look up” phrasing to avoid the flagged wording: change the line that reads
`functionName - Name or ID of the function to stream logs for` to a clearer
phrasing such as `functionName — Name or ID of the function whose logs will be
streamed`, and replace any instance of the phrase “look up” (line with “look
up”) with a more lint‑friendly verb like “retrieve” or “find” to eliminate the
warnings while preserving meaning.


**Flags**

- `deploy-id` (*string*) - Deploy ID to look up the function from

Check warning on line 176 in docs/commands/functions.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.accessibilityVision] Don't use vision-based terms. Use something inclusive like 'check', 'search', or 'examine' instead of 'look'. Raw Output: {"message": "[base.accessibilityVision] Don't use vision-based terms. Use something inclusive like 'check', 'search', or 'examine' instead of 'look'.", "location": {"path": "docs/commands/functions.md", "range": {"start": {"line": 176, "column": 41}}}, "severity": "WARNING"}
- `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
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
- `to` (*string*) - End date for historical logs (ISO 8601 format, defaults to now)

---
## `functions:serve`

Expand Down
42 changes: 41 additions & 1 deletion docs/commands/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
| 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 |
Copy link
Copy Markdown
Member

@eduardoboucas eduardoboucas Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why this is plural and logs:function is singular, but it still itches! 😖

In the future, I think we could rename logs:function to logs:functions and start accepting multiple function names, since there's nothing stopping us from listening to different streams and interleaving them, just like we do with edge functions.

| [`logs:function`](/commands/logs#logsfunction) | Stream netlify function logs to the console |


Expand All @@ -33,6 +34,8 @@
netlify logs:deploy
netlify logs:function
netlify logs:function my-function
netlify logs:edge-functions
netlify logs:edge-functions --deploy-id <deploy-id>
```

---
Expand All @@ -52,6 +55,37 @@
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

---
## `logs:edge-functions`

Stream netlify edge function logs to the console

**Usage**

```bash
netlify logs:edge-functions
```

**Flags**

- `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
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
- `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 <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`

Expand All @@ -65,21 +99,27 @@

**Arguments**

- functionName - Name of the function to stream logs for
- functionName - Name or ID of the function to stream logs for

Check warning on line 102 in docs/commands/logs.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'functionName'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'functionName'?", "location": {"path": "docs/commands/logs.md", "range": {"start": {"line": 102, "column": 3}}}, "severity": "WARNING"}

**Flags**

- `deploy-id` (*string*) - Deploy ID to look up the function from

Check warning on line 106 in docs/commands/logs.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.accessibilityVision] Don't use vision-based terms. Use something inclusive like 'check', 'search', or 'examine' instead of 'look'. Raw Output: {"message": "[base.accessibilityVision] Don't use vision-based terms. Use something inclusive like 'check', 'search', or 'examine' instead of 'look'.", "location": {"path": "docs/commands/logs.md", "range": {"start": {"line": 106, "column": 41}}}, "severity": "WARNING"}
- `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
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
- `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 <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
```

---
Expand Down
7 changes: 7 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ Provision a production ready Postgres database with a single command

Deploy your project to Netlify

| Subcommand | description |
|:--------------------------- |:-----|
| [`deploy:logs`](/commands/deploy#deploylogs) | Stream the logs of deploys currently being built to the console |


### [dev](/commands/dev)

Local dev server
Expand Down Expand Up @@ -110,6 +115,7 @@ Manage netlify functions
| [`functions:create`](/commands/functions#functionscreate) | Create a new function locally |
| [`functions:invoke`](/commands/functions#functionsinvoke) | Trigger a function while in netlify dev with simulated data, good for testing function calls including Netlify's Event Triggered Functions |
| [`functions:list`](/commands/functions#functionslist) | List functions that exist locally |
| [`functions:logs`](/commands/functions#functionslogs) | Stream netlify function logs to the console |
| [`functions:serve`](/commands/functions#functionsserve) | Serve functions locally |


Expand All @@ -132,6 +138,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 |


Expand Down
44 changes: 40 additions & 4 deletions src/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -672,6 +681,8 @@ const runDeploy = async ({
functionLogsUrl,
edgeFunctionLogsUrl,
sourceZipFileName: uploadSourceZipResult?.sourceZipFileName,
deployedFunctions,
hasEdgeFunctions,
}
}

Expand Down Expand Up @@ -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
}
Expand All @@ -796,10 +808,18 @@ const printResults = ({
results: Awaited<ReturnType<typeof prepAndRunDeploy>>
runBuildCommand: boolean
}): void => {
const msgData: Record<string, string> = {
const buildLogsData: Record<string, string> = {
'Build logs': terminalLink(results.logsUrl, results.logsUrl, { fallback: false }),
'Function logs': terminalLink(results.functionLogsUrl, results.functionLogsUrl, { fallback: false }),
'Edge function Logs': terminalLink(results.edgeFunctionLogsUrl, results.edgeFunctionLogsUrl, { fallback: false }),
}

const functionLogsData: Record<string, string> = {
'Functions logs': terminalLink(results.functionLogsUrl, results.functionLogsUrl, { fallback: false }),
'Functions CLI': `netlify functions:logs --deploy-id ${results.deployId} <function-name-or-id>`,
}

const edgeFunctionLogsData: Record<string, string> = {
'Edge Functions logs': terminalLink(results.edgeFunctionLogsUrl, results.edgeFunctionLogsUrl, { fallback: false }),
'Edge Functions CLI': `netlify logs:edge-functions --deploy-id ${results.deployId}`,
}

log('')
Expand All @@ -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
Expand Down Expand Up @@ -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 <datetime> and --to <datetime> to fetch historical logs (ISO 8601 format)'))
}

if (!deployToProduction) {
log()
Expand Down
104 changes: 104 additions & 0 deletions src/commands/logs/edge-functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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,
buildEdgeFunctionLogsUrl,
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

--deploy-id becomes a no-op in historical mode.

deployId is captured from the CLI options, but the --from path always builds a site-wide analytics URL and returns immediately. netlify logs:edge-functions --deploy-id <id> --from ... therefore ignores the requested deploy and can show unrelated logs. Either include deploy scoping in the historical request or fail fast for this flag combination.

Also applies to: 32-39

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/logs/edge-functions.ts` at line 13, The CLI's historical code
path ignores the deployId option (deployId / options.deployId) when --from is
used, causing --deploy-id to be a no-op; update the historical request
builder/handler (the code branch that handles options.from / "historical mode")
to include deployId as a filter/query parameter when constructing the historical
analytics/logs URL/request (or alternatively, detect the unsupported combination
and fail fast with a clear error), and apply the same fix to the related code
around the other occurrences noted (the block covering lines 32-39) so deploy
scoping is respected or rejected consistently.

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.join(',')}`)
}

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 = buildEdgeFunctionLogsUrl({ siteId, from: fromMs, to: toMs })
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')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: I hate that we're exposing this. We should set up a customer-facing domain name for this.


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))
})
Comment on lines +88 to +94
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard websocket parsing to prevent command crashes.

Line 89 assumes every frame is valid JSON; one malformed payload will throw and terminate the stream.

🛡️ Proposed defensive parsing
   ws.on('message', (data: string) => {
-    const logData = JSON.parse(data) as { level: string; message: string; timestamp?: string }
+    let logData: { level: string; message: string; timestamp?: string }
+    try {
+      logData = JSON.parse(data) as { level: string; message: string; timestamp?: string }
+    } catch {
+      log('Received malformed log payload')
+      return
+    }
+
     if (!levelsToPrint.includes(logData.level.toLowerCase())) {
       return
     }
     log(formatLogEntry(logData))
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/logs/edge-functions.ts` around lines 88 - 94, The websocket
message handler assumes every message is valid JSON and will crash on malformed
frames; wrap the JSON.parse and subsequent processing in a try/catch inside the
ws.on('message', ...) callback (where you currently reference levelsToPrint,
formatLogEntry, and log), ignore or warn on parse errors and return early for
invalid payloads, and only proceed to check levelsToPrint and call
log(formatLogEntry(...)) if parsing succeeds.


ws.on('close', () => {
log('Connection closed')
})

ws.on('error', (err: Error) => {
log('Connection error')
log(err.message)
})
}
Loading
Loading