Skip to content

Commit

Permalink
feat: support specification of log levels to stream (#6200)
Browse files Browse the repository at this point in the history
* feat: support specification of log levels to stream

* test: add tests

* refactor: use const object for values

* docs: update the docs

---------

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
ericapisani and kodiakhq[bot] authored Nov 20, 2023
1 parent fa31196 commit 18500f6
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 9 deletions.
4 changes: 3 additions & 1 deletion docs/commands/logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,15 @@ netlify logs:function
**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

**Examples**

```bash
netlify logs:function my-function
netlify logs:function
netlify logs:function my-function
netlify logs:function my-function -l info warn
```

---
Expand Down
38 changes: 33 additions & 5 deletions src/commands/logs/functions.mts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import { Argument, OptionValues } from 'commander'
import { Argument, Option, OptionValues } from 'commander'
import inquirer from 'inquirer'

import { chalk, log } from '../../utils/command-helpers.mjs'
import { getWebSocket } from '../../utils/websockets/index.mjs'
import type BaseCommand from '../base-command.mjs'

// Source: Source: https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced
export const LOG_LEVELS = {
TRACE: 'TRACE',
DEBUG: 'DEBUG',
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR',
FATAL: 'FATAL',
}
const LOG_LEVELS_LIST = Object.values(LOG_LEVELS).map((level) => level.toLowerCase())
const CLI_LOG_LEVEL_CHOICES_STRING = LOG_LEVELS_LIST.map((level) => ` ${level}`)

function getLog(logData: { level: string; message: string }) {
let logString = ''
switch (logData.level) {
case 'INFO':
case LOG_LEVELS.INFO:
logString += chalk.blueBright(logData.level)
break
case 'WARN':
case LOG_LEVELS.WARN:
logString += chalk.yellowBright(logData.level)
break
case 'ERROR':
case LOG_LEVELS.ERROR:
logString += chalk.redBright(logData.level)
break
default:
Expand All @@ -30,6 +42,12 @@ const logsFunction = async (functionName: string | undefined, options: OptionVal
const { site } = command.netlify
const { id: siteId } = site

if (options.level && !options.level.every((level: string) => LOG_LEVELS_LIST.includes(level))) {
log(`Invalid log level. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`)
}

const levelsToPrint = options.level || LOG_LEVELS_LIST

const { functions = [] } = await client.searchSiteFunctions({ siteId })

if (functions.length === 0) {
Expand Down Expand Up @@ -73,6 +91,9 @@ const logsFunction = async (functionName: string | undefined, options: OptionVal

ws.on('message', (data: string) => {
const logData = JSON.parse(data)
if (!levelsToPrint.includes(logData.level.toLowerCase())) {
return
}
log(getLog(logData))
})

Expand All @@ -90,7 +111,14 @@ export const createLogsFunctionCommand = (program: BaseCommand) =>
program
.command('logs:function')
.alias('logs:functions')
.addOption(
new Option('-l, --level <levels...>', `Log levels to stream. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`),
)
.addArgument(new Argument('[functionName]', 'Name of the function to stream logs for'))
.addExamples(['netlify logs:function my-function', 'netlify logs:function'])
.addExamples([
'netlify logs:function',
'netlify logs:function my-function',
'netlify logs:function my-function -l info warn',
])
.description('(Beta) Stream netlify function logs to the console')
.action(logsFunction)
79 changes: 76 additions & 3 deletions tests/integration/commands/logs/functions.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { Mock, afterEach, beforeEach, describe, expect, test, vi } from 'vitest'

import BaseCommand from '../../../../src/commands/base-command.mjs'
import { createLogsFunctionCommand } from '../../../../src/commands/logs/functions.mjs'
import { LOG_LEVELS, createLogsFunctionCommand } from '../../../../src/commands/logs/functions.mjs'
import { getWebSocket } from '../../../../src/utils/websockets/index.mjs'
import { log } from '../../../../src/utils/command-helpers.mjs'
import { startMockApi } from '../../utils/mock-api-vitest.ts'
import { getEnvironmentVariables } from '../../utils/mock-api.mjs'

vi.mock('../../../../src/utils/websockets/index.mjs', () => ({
getWebSocket: vi.fn(),
}))

vi.mock('../../../../src/utils/command-helpers.mjs', async () => {
const actual = await vi.importActual("../../../../src/utils/command-helpers.mjs")
return {
...actual,
log: vi.fn(),
}
})

vi.mock('inquirer', () => ({
default: {
prompt: vi.fn().mockResolvedValue({ result: 'cool-function' }),
Expand Down Expand Up @@ -67,7 +76,7 @@ describe('logs:function command', () => {
createLogsFunctionCommand(program)
})

test('should setup the functions stream correctly', async ({}) => {
test('should setup the functions stream correctly', async ({ }) => {
const { apiUrl } = await startMockApi({ routes })
const spyWebsocket = getWebSocket as unknown as Mock<any, any>
const spyOn = vi.fn()
Expand All @@ -86,7 +95,7 @@ describe('logs:function command', () => {
expect(spyOn).toHaveBeenCalledTimes(4)
})

test('should send the correct payload to the websocket', async ({}) => {
test('should send the correct payload to the websocket', async ({ }) => {
const { apiUrl } = await startMockApi({ routes })
const spyWebsocket = getWebSocket as unknown as Mock<any, any>
const spyOn = vi.fn()
Expand Down Expand Up @@ -117,4 +126,68 @@ describe('logs:function command', () => {
expect(body.account_id).toEqual('account')
expect(body.access_token).toEqual(env.NETLIFY_AUTH_TOKEN)
})

test('should print only specified log levels', async ({ }) => {
const { apiUrl } = await startMockApi({ routes })
const spyWebsocket = getWebSocket as unknown as Mock<any, any>
const spyOn = vi.fn()
const spySend = vi.fn()
spyWebsocket.mockReturnValue({
on: spyOn,
send: spySend,
})
const spyLog = log as unknown as Mock<any, any>

const env = getEnvironmentVariables({ apiUrl })
Object.assign(process.env, env)

await program.parseAsync(['', '', 'logs:function', '--level', 'info'])
const messageCallback = spyOn.mock.calls.find((args) => args[0] === 'message')
const messageCallbackFunc = messageCallback[1]
const mockInfoData = {
"level": LOG_LEVELS.INFO,
"message": "Hello World",
}
const mockWarnData = {
"level": LOG_LEVELS.WARN,
"message": "There was a warning",
}

messageCallbackFunc(JSON.stringify(mockInfoData))
messageCallbackFunc(JSON.stringify(mockWarnData))

expect(spyLog).toHaveBeenCalledTimes(1)
})

test('should print all the log levels', async ({ }) => {
const { apiUrl } = await startMockApi({ routes })
const spyWebsocket = getWebSocket as unknown as Mock<any, any>
const spyOn = vi.fn()
const spySend = vi.fn()
spyWebsocket.mockReturnValue({
on: spyOn,
send: spySend,
})
const spyLog = log as unknown as Mock<any, any>

const env = getEnvironmentVariables({ apiUrl })
Object.assign(process.env, env)

await program.parseAsync(['', '', 'logs:function'])
const messageCallback = spyOn.mock.calls.find((args) => args[0] === 'message')
const messageCallbackFunc = messageCallback[1]
const mockInfoData = {
"level": LOG_LEVELS.INFO,
"message": "Hello World",
}
const mockWarnData = {
"level": LOG_LEVELS.WARN,
"message": "There was a warning",
}

messageCallbackFunc(JSON.stringify(mockInfoData))
messageCallbackFunc(JSON.stringify(mockWarnData))

expect(spyLog).toHaveBeenCalledTimes(2)
})
})

2 comments on commit 18500f6

@github-actions
Copy link

Choose a reason for hiding this comment

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

📊 Benchmark results

  • Dependency count: 1,396
  • Package size: 404 MB
  • Number of ts-expect-error directives: 1,536

@github-actions
Copy link

Choose a reason for hiding this comment

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

📊 Benchmark results

  • Dependency count: 1,396
  • Package size: 404 MB
  • Number of ts-expect-error directives: 1,536

Please sign in to comment.