Skip to content

Commit

Permalink
[cloud-run][flare] Get recent logs (#1011)
Browse files Browse the repository at this point in the history
* Get cloud run logs

* Add pagination to get more than 1k logs

* Refactor, add error handling, and tests for `getLogs()`

* Add tests for `saveLogsFile()`

* Fix dependency conflicts by explicitly adding the `agent-base` and `http-proxy-agent` packages.

* Add new packages to LICENSE-3rdparty.csv

* Fix license

* Use `@google-cloud/logging-min` instead of `@google-cloud/logging`

* Fix test

* Refactor getting logs so and error doesn't end the program. Just skip

* Refactor, fix tests

* Add test

* Fix `lambda/flare.ts` prompt try/catch

* Refactor

* Fix warnings

* Skip logs that aren't HTTP requests and don't have a text payload

* Fix lint

* Refactor request limit checking

* Refactor

* Resolve

* Nit

* Remove unnecessary `advanceTimers`

* - Use `@google-cloud/logging` instead of `@google-cloud/logging-min`.
- Add JSdoc headers to interfaces.ts
- Fix `logFileMappings` to have the fileName be the key instead of the value
- Nits

* Update LICENSE-3rdparty.csv

* Remove pagination

* Update comment

* Update src/commands/cloud-run/interfaces.ts

Co-authored-by: jordan gonzález <[email protected]>

* Remove `agent-base` package

* Update license

---------

Co-authored-by: jordan gonzález <[email protected]>
  • Loading branch information
nhulston and duncanista authored Aug 8, 2023
1 parent 8f211e1 commit 113a652
Show file tree
Hide file tree
Showing 10 changed files with 601 additions and 63 deletions.
2 changes: 2 additions & 0 deletions LICENSE-3rdparty.csv
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ ajv-formats,import,MIT,Copyright (c) 2020 Evgeny Poberezkin
jszip,import,MIT,Copyright (c) 2009-2016 Stuart Knightley and other contributors
@google-cloud/run,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors
google-auth-library,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors
@google-cloud/logging,import,Apache-2.0,Copyright (c) 2023 Google LLC and other contributors
http-proxy-agent,import,MIT,Copyright (c) 2013 Nathan Rajlich
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@aws-sdk/client-sfn": "^3.358.0",
"@aws-sdk/credential-providers": "^3.358.0",
"@aws-sdk/property-provider": "^3.357.0",
"@google-cloud/logging": "^10.5.0",
"@google-cloud/run": "^0.6.0",
"@types/datadog-metrics": "0.6.1",
"ajv": "^8.12.0",
Expand All @@ -82,6 +83,7 @@
"fuzzy": "^0.1.3",
"glob": "7.1.4",
"google-auth-library": "^8.9.0",
"http-proxy-agent": "^7.0.0",
"inquirer": "^8.2.5",
"inquirer-checkbox-plus-prompt": "^1.4.2",
"js-yaml": "3.13.1",
Expand Down
62 changes: 55 additions & 7 deletions src/commands/cloud-run/__tests__/__snapshots__/flare.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ GCP credentials verified!
}
}
💾 Saving configuration...
💾 Saving files to mock-folder/.datadog-ci...
• Saved function config to ./service_config.json
🚫 The flare files were not sent as it was executed in dry run mode.
ℹ️ Your output files are located at: mock-folder/.datadog-ci
Expand All @@ -64,6 +65,47 @@ GCP credentials verified!
"
`;

exports[`cloud-run flare getLogs handles httpRequest payload correctly 1`] = `
Array [
Object {
"logName": "mock-logname",
"message": "\\"GET 200. responseSize: 1.27 KB. latency: 1500 ms. requestUrl: /test-endpoint\\"",
"severity": "DEFAULT",
"timestamp": "2023-07-28 00:00:00",
},
]
`;

exports[`cloud-run flare getLogs handles textPayload correctly 1`] = `
Array [
Object {
"logName": "mock-logname",
"message": "\\"Some text payload\\"",
"severity": "DEFAULT",
"timestamp": "2023-07-28 00:00:00",
},
]
`;

exports[`cloud-run flare getLogs handles when a log is an HTTP request and has a textPayload 1`] = `
Array [
Object {
"logName": "mock-logname",
"message": "\\"Test log 1\\"",
"severity": "DEFAULT",
"timestamp": "2023-07-28 00:00:00",
},
Object {
"logName": "mock-logname",
"message": "\\"some text payload. 504. responseSize: 0 Bytes. latency: unknown ms. requestUrl: \\"",
"severity": "",
"timestamp": "2023-07-28 00:00:01",
},
]
`;

exports[`cloud-run flare getLogs throws an error when \`getEntries\` fails 1`] = `[Error: getEntries failed]`;

exports[`cloud-run flare maskConfig should mask a Cloud Run config correctly 1`] = `
Object {
"template": Object {
Expand Down Expand Up @@ -153,7 +195,8 @@ GCP credentials verified!
}
}
💾 Saving configuration...
💾 Saving files to mock-folder/.datadog-ci...
• Saved function config to ./service_config.json
🚫 The flare files were not sent based on your selection.
Expand Down Expand Up @@ -191,7 +234,8 @@ GCP credentials verified!
}
}
💾 Saving configuration...
💾 Saving files to mock-folder/.datadog-ci...
• Saved function config to ./service_config.json
🚀 Sending to Datadog Support...
Expand Down Expand Up @@ -271,7 +315,8 @@ GCP credentials verified!
}
}
💾 Saving configuration...
💾 Saving files to mock-folder/.datadog-ci...
• Saved function config to ./service_config.json
🚀 Sending to Datadog Support...
Expand Down Expand Up @@ -309,7 +354,8 @@ GCP credentials verified!
}
}
💾 Saving configuration...
💾 Saving files to mock-folder/.datadog-ci...
• Saved function config to ./service_config.json
🚀 Sending to Datadog Support...
Expand Down Expand Up @@ -347,7 +393,8 @@ GCP credentials verified!
}
}
💾 Saving configuration...
💾 Saving files to mock-folder/.datadog-ci...
• Saved function config to ./service_config.json
🚀 Sending to Datadog Support...
Expand Down Expand Up @@ -381,7 +428,8 @@ GCP credentials verified!
}
}
💾 Saving configuration...
💾 Saving files to mock-folder/.datadog-ci...
• Saved function config to ./service_config.json
🚀 Sending to Datadog Support...
Expand Down
171 changes: 163 additions & 8 deletions src/commands/cloud-run/__tests__/flare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'fs'
import process from 'process'
import stream from 'stream'

import {Logging} from '@google-cloud/logging'
import {GoogleAuth} from 'google-auth-library'

import {API_KEY_ENV_VAR, CI_API_KEY_ENV_VAR} from '../../../constants'
Expand All @@ -11,21 +12,25 @@ import {
MOCK_DATADOG_API_KEY,
MOCK_FLARE_FOLDER_PATH,
} from '../../../helpers/__tests__/fixtures'
import * as fsModule from '../../../helpers/fs'
import * as helpersPromptModule from '../../../helpers/prompt'

import * as flareModule from '../flare'
import {checkAuthentication, getCloudRunServiceConfig, maskConfig} from '../flare'
import {checkAuthentication, getCloudRunServiceConfig, getLogs, maskConfig, MAX_LOGS, saveLogsFile} from '../flare'

import {makeCli} from './fixtures'

const MOCK_REGION = 'us-east1'
const MOCK_SERVICE = 'mock-service'
const MOCK_PROJECT = 'mock-project'
const MOCK_LOG_CLIENT = new Logging({projectId: MOCK_PROJECT})
const MOCK_REQUIRED_FLAGS = [
'cloud-run',
'flare',
'-s',
'service',
MOCK_SERVICE,
'-p',
'project',
MOCK_PROJECT,
'-r',
MOCK_REGION,
'-c',
Expand Down Expand Up @@ -86,6 +91,8 @@ jest.mock('@google-cloud/run', () => {
jest.spyOn(helpersPromptModule, 'requestConfirmation').mockResolvedValue(true)
jest.mock('util')
jest.mock('jszip')
jest.mock('@google-cloud/logging')
jest.useFakeTimers({now: new Date(Date.UTC(2023, 0))})

// File system mocks
process.cwd = jest.fn().mockReturnValue(MOCK_CWD)
Expand Down Expand Up @@ -127,7 +134,7 @@ describe('cloud-run flare', () => {
const cli = makeCli()
const context = createMockContext()
const code = await cli.run(
['cloud-run', 'flare', '-p', 'project', '-r', MOCK_REGION, '-c', '123', '-e', '[email protected]'],
['cloud-run', 'flare', '-p', MOCK_PROJECT, '-r', MOCK_REGION, '-c', '123', '-e', '[email protected]'],
context as any
)
expect(code).toBe(1)
Expand All @@ -139,7 +146,7 @@ describe('cloud-run flare', () => {
const cli = makeCli()
const context = createMockContext()
const code = await cli.run(
['cloud-run', 'flare', '-s', 'service', '-r', MOCK_REGION, '-c', '123', '-e', '[email protected]'],
['cloud-run', 'flare', '-s', MOCK_SERVICE, '-r', MOCK_REGION, '-c', '123', '-e', '[email protected]'],
context as any
)
expect(code).toBe(1)
Expand All @@ -151,7 +158,7 @@ describe('cloud-run flare', () => {
const cli = makeCli()
const context = createMockContext()
const code = await cli.run(
['cloud-run', 'flare', '-s', 'service', '-p', 'project', '-c', '123', '-e', '[email protected]'],
['cloud-run', 'flare', '-s', MOCK_SERVICE, '-p', MOCK_PROJECT, '-c', '123', '-e', '[email protected]'],
context as any
)
expect(code).toBe(1)
Expand All @@ -163,7 +170,7 @@ describe('cloud-run flare', () => {
const cli = makeCli()
const context = createMockContext()
const code = await cli.run(
['cloud-run', 'flare', '-s', 'service', '-p', 'project', '-r', MOCK_REGION, '-e', '[email protected]'],
['cloud-run', 'flare', '-s', MOCK_SERVICE, '-p', MOCK_PROJECT, '-r', MOCK_REGION, '-e', '[email protected]'],
context as any
)
expect(code).toBe(1)
Expand All @@ -175,7 +182,7 @@ describe('cloud-run flare', () => {
const cli = makeCli()
const context = createMockContext()
const code = await cli.run(
['cloud-run', 'flare', '-s', 'service', '-p', 'project', '-r', MOCK_REGION, '-c', '123'],
['cloud-run', 'flare', '-s', MOCK_SERVICE, '-p', MOCK_PROJECT, '-r', MOCK_REGION, '-c', '123'],
context as any
)
expect(code).toBe(1)
Expand Down Expand Up @@ -324,4 +331,152 @@ describe('cloud-run flare', () => {
expect(output).toContain('🚫 The flare files were not sent based on your selection.')
})
})

describe('getLogs', () => {
const logName = 'mock-logname'
const mockLogs = [
{metadata: {severity: 'DEFAULT', timestamp: '2023-07-28 00:00:00', logName, textPayload: 'Log 1'}},
{metadata: {severity: 'INFO', timestamp: '2023-07-28 00:00:00', logName, textPayload: 'Log 2'}},
{metadata: {severity: 'NOTICE', timestamp: '2023-07-28 01:01:01', logName, textPayload: 'Log 3'}},
]
const MOCK_GET_ENTRIES = MOCK_LOG_CLIENT.getEntries as jest.Mock
MOCK_GET_ENTRIES.mockResolvedValue([mockLogs, {pageToken: undefined}])
const expectedOrder = 'timestamp asc'

it('uses correct filter when `severityFilter` is unspecified', async () => {
await getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION)
const expectedFilter = `resource.labels.service_name="${MOCK_SERVICE}" AND resource.labels.location="${MOCK_REGION}" AND timestamp>="2022-12-31T00:00:00.000Z" AND (textPayload:* OR httpRequest:*)`

expect(MOCK_LOG_CLIENT.getEntries).toHaveBeenCalledWith({
filter: expectedFilter,
orderBy: expectedOrder,
pageSize: MAX_LOGS,
})
})

it('uses correct filter when `severityFilter` is defined', async () => {
await getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION, ' AND severity>="WARNING"')
const expectedFilter = `resource.labels.service_name="${MOCK_SERVICE}" AND resource.labels.location="${MOCK_REGION}" AND timestamp>="2022-12-31T00:00:00.000Z" AND (textPayload:* OR httpRequest:*) AND severity>="WARNING"`

expect(MOCK_LOG_CLIENT.getEntries).toHaveBeenCalledWith({
filter: expectedFilter,
orderBy: expectedOrder,
pageSize: MAX_LOGS,
})
})

it('converts logs to the CloudRunLog interface correctly', async () => {
const page1 = [
{metadata: {severity: 'DEFAULT', timestamp: '2023-07-28 00:00:00', logName, textPayload: 'Test log'}},
]
MOCK_GET_ENTRIES.mockResolvedValueOnce([page1, {pageToken: undefined}])

const logs = await getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION)

expect(logs).toEqual([
{
severity: 'DEFAULT',
timestamp: '2023-07-28 00:00:00',
logName,
message: '"Test log"',
},
])
})

it('throws an error when `getEntries` fails', async () => {
const error = new Error('getEntries failed')
MOCK_GET_ENTRIES.mockRejectedValueOnce(error)

await expect(getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION)).rejects.toMatchSnapshot()
})

it('returns an empty array when no logs are returned', async () => {
MOCK_GET_ENTRIES.mockResolvedValueOnce([[], {pageToken: undefined}])
const logs = await getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION)

expect(logs).toEqual([])
})

it('handles httpRequest payload correctly', async () => {
const page = [
{
metadata: {
severity: 'DEFAULT',
timestamp: '2023-07-28 00:00:00',
logName,
httpRequest: {
requestMethod: 'GET',
status: 200,
responseSize: '1300',
latency: {seconds: '1', nanos: '500000000'},
requestUrl: '/test-endpoint',
},
},
},
]
MOCK_GET_ENTRIES.mockResolvedValueOnce([page, {pageToken: undefined}])

const logs = await getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION)
expect(logs).toMatchSnapshot()
})

it('handles textPayload correctly', async () => {
const page = [
{
metadata: {
severity: 'DEFAULT',
timestamp: '2023-07-28 00:00:00',
logName,
textPayload: 'Some text payload',
},
},
]
MOCK_GET_ENTRIES.mockResolvedValueOnce([page, {pageToken: undefined}])

const logs = await getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION)
expect(logs).toMatchSnapshot()
})

it('handles when a log is an HTTP request and has a textPayload', async () => {
const page = [
{metadata: {severity: 'DEFAULT', timestamp: '2023-07-28 00:00:00', logName, textPayload: 'Test log 1'}},
{
metadata: {
httpRequest: {
status: 504,
},
timestamp: '2023-07-28 00:00:01',
logName,
textPayload: 'some text payload.',
},
},
]
MOCK_GET_ENTRIES.mockResolvedValueOnce([page, {pageToken: undefined}])

const logs = await getLogs(MOCK_LOG_CLIENT, MOCK_SERVICE, MOCK_REGION)

expect(logs).toMatchSnapshot()
})
})

describe('saveLogsFile', () => {
const mockLogs = [
{severity: 'DEFAULT', timestamp: '2023-07-28 00:00:00', logName: 'mock-logname', message: 'Test log 1'},
{severity: 'INFO', timestamp: '2023-07-28 00:00:01', logName: 'mock-logname', message: 'Test log 2'},
{severity: 'NOTICE', timestamp: '2023-07-28 01:01:01', logName: 'mock-logname', message: 'Test log 3'},
]
const writeFileSpy = jest.spyOn(fsModule, 'writeFile')
const mockFilePath = 'path/to/logs.csv'

it('should save logs to file correctly', () => {
saveLogsFile(mockLogs, mockFilePath)
const expectedContent = [
'severity,timestamp,logName,message',
'"DEFAULT","2023-07-28 00:00:00","mock-logname","Test log 1"',
'"INFO","2023-07-28 00:00:01","mock-logname","Test log 2"',
'"NOTICE","2023-07-28 01:01:01","mock-logname","Test log 3"',
].join('\n')
expect(writeFileSpy).toHaveBeenCalledWith(mockFilePath, expectedContent)
})
})
})
Loading

0 comments on commit 113a652

Please sign in to comment.