Skip to content

Commit b07ec98

Browse files
authored
feat(lambda): user can open CWL Live Tail from Lambda function #6423
Allow to "Tail Logs" from Lambda functions, which will get the configured Log Group for a particular function and start a Live Tail session in that Log Group. Some code was removed, because with the recently updated version of the SDK v2, we can take the configured log group directly from the function's configuration and we don't need that extra call using SDK V3 that was being made inside `deployedNode.ts` ## Problem To start a CloudWatch Live Tail session, a user needs to know their Log Group name and find it in the AWS explorer. For Lambda users, that might not be easy to find or well known all the time ## Solution We help the Live Tail experience for Lambda customers by allowing them to start a Live Tail session directly from their function in the AWS explorer and in the Application Builder section for deployed functions, without having to search for their Log Group explicitly.
1 parent 3f16d7d commit b07ec98

File tree

10 files changed

+135
-37
lines changed

10 files changed

+135
-37
lines changed

packages/core/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"AWS.command.appBuilder.deploy": "Deploy SAM Application",
9292
"AWS.command.appBuilder.build": "Build SAM Template",
9393
"AWS.command.appBuilder.searchLogs": "Search Logs",
94+
"AWS.command.appBuilder.tailLogs": "Tail Logs",
9495
"AWS.command.refreshappBuilderExplorer": "Refresh Application Builder Explorer",
9596
"AWS.command.applicationComposer.openDialog": "Open Template with Infrastructure Composer...",
9697
"AWS.command.auth.addConnection": "Add New Connection",

packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { createPlaceholderItem } from '../../../../shared/treeview/utils'
1010
import * as nls from 'vscode-nls'
1111

1212
import { getLogger } from '../../../../shared/logger/logger'
13-
import { FunctionConfiguration, LambdaClient, GetFunctionCommand } from '@aws-sdk/client-lambda'
1413
import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient'
1514
import globals from '../../../../shared/extensionGlobals'
1615
import { defaultPartition } from '../../../../shared/regions/regionProvider'
@@ -28,7 +27,6 @@ import {
2827
s3BucketType,
2928
} from '../../../../shared/cloudformation/cloudformation'
3029
import { ToolkitError } from '../../../../shared'
31-
import { getIAMConnection } from '../../../../auth/utils'
3230

3331
const localize = nls.loadMessageBundle()
3432
export interface DeployedResource {
@@ -89,8 +87,6 @@ export async function generateDeployedNode(
8987
const defaultClient = new DefaultLambdaClient(regionCode)
9088
const lambdaNode = new LambdaNode(regionCode, defaultClient)
9189
let configuration: Lambda.FunctionConfiguration
92-
let v3configuration
93-
let logGroupName
9490
try {
9591
configuration = (await defaultClient.getFunction(deployedResource.PhysicalResourceId))
9692
.Configuration as Lambda.FunctionConfiguration
@@ -101,31 +97,6 @@ export async function generateDeployedNode(
10197
code: 'lambdaClientError',
10298
})
10399
}
104-
const connection = await getIAMConnection({ prompt: false })
105-
if (!connection || connection.type !== 'iam') {
106-
return [
107-
createPlaceholderItem(
108-
localize(
109-
'AWS.appBuilder.explorerNode.unavailableDeployedResource',
110-
'[Failed to retrive deployed resource. Ensure your AWS account is connected.]'
111-
)
112-
),
113-
]
114-
}
115-
const cred = await connection.getCredentials()
116-
const v3Client = new LambdaClient({ region: regionCode, credentials: cred })
117-
118-
const v3command = new GetFunctionCommand({ FunctionName: deployedResource.PhysicalResourceId })
119-
try {
120-
v3configuration = (await v3Client.send(v3command)).Configuration as FunctionConfiguration
121-
logGroupName = v3configuration.LoggingConfig?.LogGroup
122-
} catch (error: any) {
123-
getLogger().error('Error getting Lambda V3 configuration: %O', error)
124-
}
125-
newDeployedResource.configuration = {
126-
...newDeployedResource.configuration,
127-
logGroupName: logGroupName,
128-
} as any
129100
break
130101
}
131102
case s3BucketType: {

packages/core/src/awsService/cloudWatchLogs/activation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ import { getLogger } from '../../shared/logger/logger'
2828
import { ToolkitError } from '../../shared'
2929
import { LiveTailCodeLensProvider } from './document/liveTailCodeLensProvider'
3030

31+
export const liveTailRegistry = LiveTailSessionRegistry.instance
32+
export const liveTailCodeLensProvider = new LiveTailCodeLensProvider(liveTailRegistry)
3133
export async function activate(context: vscode.ExtensionContext, configuration: Settings): Promise<void> {
3234
const registry = LogDataRegistry.instance
33-
const liveTailRegistry = LiveTailSessionRegistry.instance
3435

3536
const documentProvider = new LogDataDocumentProvider(registry)
3637
const liveTailDocumentProvider = new LiveTailDocumentProvider()
37-
const liveTailCodeLensProvider = new LiveTailCodeLensProvider(liveTailRegistry)
3838
context.subscriptions.push(
3939
vscode.languages.registerCodeLensProvider(
4040
{
@@ -150,7 +150,7 @@ export async function activate(context: vscode.ExtensionContext, configuration:
150150
)
151151
}
152152

153-
function getFunctionLogGroupName(configuration: any) {
153+
export function getFunctionLogGroupName(configuration: any) {
154154
const logGroupPrefix = '/aws/lambda/'
155-
return configuration.logGroupName || logGroupPrefix + configuration.FunctionName
155+
return configuration.LoggingConfig?.LogGroup || logGroupPrefix + configuration.FunctionName
156156
}

packages/core/src/awsService/cloudWatchLogs/commands/tailLogGroup.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ import {
1717
import { getLogger, globals, ToolkitError } from '../../../shared'
1818
import { uriToKey } from '../cloudWatchLogsUtils'
1919
import { LiveTailCodeLensProvider } from '../document/liveTailCodeLensProvider'
20+
import { LogStreamFilterResponse } from '../wizard/liveTailLogStreamSubmenu'
2021

2122
export async function tailLogGroup(
2223
registry: LiveTailSessionRegistry,
2324
source: string,
2425
codeLensProvider: LiveTailCodeLensProvider,
25-
logData?: { regionName: string; groupName: string }
26+
logData?: { regionName: string; groupName: string },
27+
logStreamFilterData?: LogStreamFilterResponse
2628
): Promise<void> {
2729
await telemetry.cloudwatchlogs_startLiveTail.run(async (span) => {
28-
const wizard = new TailLogGroupWizard(logData)
30+
const wizard = new TailLogGroupWizard(logData, logStreamFilterData)
2931
const wizardResponse = await wizard.run()
3032
if (!wizardResponse) {
3133
throw new CancellationError('user')

packages/core/src/awsService/cloudWatchLogs/wizard/tailLogGroupWizard.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface TailLogGroupWizardResponse {
2424
}
2525

2626
export class TailLogGroupWizard extends Wizard<TailLogGroupWizardResponse> {
27-
public constructor(logGroupInfo?: CloudWatchLogsGroupInfo) {
27+
public constructor(logGroupInfo?: CloudWatchLogsGroupInfo, logStreamInfo?: LogStreamFilterResponse) {
2828
super({
2929
initState: {
3030
regionLogGroupSubmenuResponse: logGroupInfo
@@ -33,6 +33,7 @@ export class TailLogGroupWizard extends Wizard<TailLogGroupWizardResponse> {
3333
region: logGroupInfo.regionName,
3434
}
3535
: undefined,
36+
logStreamFilter: logStreamInfo ?? undefined,
3637
},
3738
})
3839
this.form.regionLogGroupSubmenuResponse.bindPrompter(createRegionLogGroupSubmenu)

packages/core/src/lambda/activation.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import * as vscode from 'vscode'
7+
import { Lambda } from 'aws-sdk'
78
import { deleteLambda } from './commands/deleteLambda'
89
import { uploadLambdaCommand } from './commands/uploadLambda'
910
import { LambdaFunctionNode } from './explorer/lambdaFunctionNode'
@@ -18,6 +19,11 @@ import { copyLambdaUrl } from './commands/copyLambdaUrl'
1819
import { ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode'
1920
import { isTreeNode, TreeNode } from '../shared/treeview/resourceTreeDataProvider'
2021
import { getSourceNode } from '../shared/utilities/treeNodeUtils'
22+
import { tailLogGroup } from '../awsService/cloudWatchLogs/commands/tailLogGroup'
23+
import { liveTailRegistry, liveTailCodeLensProvider } from '../awsService/cloudWatchLogs/activation'
24+
import { getFunctionLogGroupName } from '../awsService/cloudWatchLogs/activation'
25+
import { ToolkitError, isError } from '../shared'
26+
import { LogStreamFilterResponse } from '../awsService/cloudWatchLogs/wizard/liveTailLogStreamSubmenu'
2127

2228
/**
2329
* Activates Lambda components.
@@ -76,6 +82,40 @@ export async function activate(context: ExtContext): Promise<void> {
7682

7783
Commands.register('aws.launchDebugConfigForm', async (node: ResourceNode) =>
7884
registerSamDebugInvokeVueCommand(context.extensionContext, { resource: node })
79-
)
85+
),
86+
87+
Commands.register('aws.appBuilder.tailLogs', async (node: LambdaFunctionNode | TreeNode) => {
88+
let functionConfiguration: Lambda.FunctionConfiguration
89+
try {
90+
const sourceNode = getSourceNode<LambdaFunctionNode>(node)
91+
functionConfiguration = sourceNode.configuration
92+
const logGroupInfo = {
93+
regionName: sourceNode.regionCode,
94+
groupName: getFunctionLogGroupName(functionConfiguration),
95+
}
96+
97+
const source = isTreeNode(node) ? 'AppBuilder' : 'AwsExplorerLambdaNode'
98+
// Show all log streams without having to choose
99+
const logStreamFilterData: LogStreamFilterResponse = { type: 'all' }
100+
await tailLogGroup(
101+
liveTailRegistry,
102+
source,
103+
liveTailCodeLensProvider,
104+
logGroupInfo,
105+
logStreamFilterData
106+
)
107+
} catch (err) {
108+
if (isError(err as Error, 'ResourceNotFoundException', "LogGroup doesn't exist.")) {
109+
// If we caught this error, then we know `functionConfiguration` actually has a value
110+
throw ToolkitError.chain(
111+
err,
112+
`Unable to fetch logs. Log group for function '${functionConfiguration!.FunctionName}' does not exist. ` +
113+
'Invoking your function at least once will create the log group.'
114+
)
115+
} else {
116+
throw err
117+
}
118+
}
119+
})
80120
)
81121
}

packages/core/src/test/awsService/cloudWatchLogs/commands/tailLogGroup.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { getTestWindow } from '../../../shared/vscode/window'
1919
import { CloudWatchLogsSettings, uriToKey } from '../../../../awsService/cloudWatchLogs/cloudWatchLogsUtils'
2020
import { DefaultAwsContext, ToolkitError, waitUntil } from '../../../../shared'
2121
import { LiveTailCodeLensProvider } from '../../../../awsService/cloudWatchLogs/document/liveTailCodeLensProvider'
22+
import { PrompterTester } from '../../../shared/wizards/prompterTester'
2223

2324
describe('TailLogGroup', function () {
2425
const testLogGroup = 'test-log-group'
@@ -142,6 +143,48 @@ describe('TailLogGroup', function () {
142143
assert.strictEqual(stopLiveTailSessionSpy.calledOnce, true)
143144
})
144145

146+
it(`doesn't ask for stream filter if passed as parameter`, async function () {
147+
sandbox.stub(DefaultAwsContext.prototype, 'getCredentialAccountId').returns(testAwsAccountId)
148+
sandbox.stub(DefaultAwsContext.prototype, 'getCredentials').returns(Promise.resolve(testAwsCredentials))
149+
150+
// Returns one frame
151+
async function* generator(): AsyncIterable<StartLiveTailResponseStream> {
152+
yield getSessionUpdateFrame(false, `${testMessage}-1`, 1732276800000)
153+
}
154+
155+
startLiveTailSessionSpy = sandbox
156+
.stub(LiveTailSession.prototype, 'startLiveTailSession')
157+
.returns(Promise.resolve(generator()))
158+
159+
const prompterTester = PrompterTester.init()
160+
.handleInputBox('Provide log event filter pattern', (inputBox) => {
161+
inputBox.acceptValue('filter')
162+
})
163+
.build()
164+
165+
// Set maxLines to 1.
166+
cloudwatchSettingsSpy = sandbox.stub(CloudWatchLogsSettings.prototype, 'get').returns(1)
167+
168+
// The mock stream doesn't 'close', causing tailLogGroup to not return. If we `await`, it will never resolve.
169+
// Run it in the background and use waitUntil to poll its state.
170+
void tailLogGroup(
171+
registry,
172+
testSource,
173+
codeLensProvider,
174+
{
175+
groupName: testLogGroup,
176+
regionName: testRegion,
177+
},
178+
{ type: 'all' }
179+
)
180+
await waitUntil(async () => registry.size !== 0, { interval: 100, timeout: 1000 })
181+
182+
prompterTester.assertCall('Provide log event filter pattern', 1)
183+
184+
assert.strictEqual(startLiveTailSessionSpy.calledOnce, true)
185+
assert.strictEqual(registry.size, 1)
186+
})
187+
145188
it('throws if crendentials are undefined', async function () {
146189
sandbox.stub(DefaultAwsContext.prototype, 'getCredentials').returns(Promise.resolve(undefined))
147190
wizardSpy = sandbox.stub(TailLogGroupWizard.prototype, 'run').callsFake(async function () {

packages/core/src/test/awsService/cloudWatchLogs/wizard/tailLogGroupWizard.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ describe('TailLogGroupWizard', async function () {
4444
tester.filterPattern.assertShowSecond()
4545
})
4646

47+
it('skips logStream filter when logStream info is provided', async function () {
48+
sandbox.stub(DefaultAwsContext.prototype, 'getCredentialAccountId').returns(testAwsAccountId)
49+
const wizard = new TailLogGroupWizard(
50+
{
51+
groupName: testLogGroupName,
52+
regionName: testRegion,
53+
},
54+
{ type: 'specific', filter: 'log-group-name' }
55+
)
56+
const tester = await createWizardTester(wizard)
57+
tester.regionLogGroupSubmenuResponse.assertDoesNotShow()
58+
tester.logStreamFilter.assertDoesNotShow()
59+
tester.filterPattern.assertShowFirst()
60+
})
61+
4762
it('builds LogGroup Arn properly', async function () {
4863
sandbox.stub(DefaultAwsContext.prototype, 'getCredentialAccountId').returns(testAwsAccountId)
4964
const arn = buildLogGroupArn(testLogGroupName, testRegion)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "From the Lambda treeview in AWS Explorer, you can now right-click on a function name and start a CloudWatch Logs Live Tail sessions for the selected function."
4+
}

packages/toolkit/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,10 @@
902902
"command": "aws.appBuilderForFileExplorer.refresh",
903903
"when": "false"
904904
},
905+
{
906+
"command": "aws.appBuilder.tailLogs",
907+
"when": "false"
908+
},
905909
{
906910
"command": "aws.appBuilder.viewDocs",
907911
"when": "false"
@@ -1641,6 +1645,11 @@
16411645
"when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/",
16421646
"group": "0@3"
16431647
},
1648+
{
1649+
"command": "aws.appBuilder.tailLogs",
1650+
"when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/",
1651+
"group": "0@4"
1652+
},
16441653
{
16451654
"command": "aws.deleteCloudFormation",
16461655
"when": "view == aws.explorer && viewItem == awsCloudFormationNode",
@@ -2140,6 +2149,11 @@
21402149
"command": "aws.appBuilder.searchLogs",
21412150
"when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/",
21422151
"group": "inline@2"
2152+
},
2153+
{
2154+
"command": "aws.appBuilder.tailLogs",
2155+
"when": "view =~ /^(aws.explorer|aws.appBuilder|aws.appBuilderForFileExplorer)$/ && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable|awsCloudFormationFunctionNode)$/",
2156+
"group": "inline@3"
21432157
}
21442158
],
21452159
"aws.toolkit.auth": [
@@ -3749,6 +3763,13 @@
37493763
"category": "%AWS.title%",
37503764
"icon": "$(search-view-icon)"
37513765
},
3766+
{
3767+
"command": "aws.appBuilder.tailLogs",
3768+
"title": "%AWS.command.appBuilder.tailLogs%",
3769+
"enablement": "isCloud9 || !aws.isWebExtHost",
3770+
"category": "%AWS.title%",
3771+
"icon": "$(search-show-context)"
3772+
},
37523773
{
37533774
"command": "aws.appBuilder.deploy",
37543775
"title": "%AWS.command.appBuilder.deploy%",

0 commit comments

Comments
 (0)