Skip to content

Commit f190f03

Browse files
authored
Users can select a CloudWatch Logs Log Stream from a dynamically-loaded QuickPick (gated feature) (#1111)
Created general use abstractions for converting AWS calls into iterators and using iterators within Quick Picks.
1 parent 342ce4c commit f190f03

File tree

16 files changed

+1088
-32
lines changed

16 files changed

+1088
-32
lines changed

.changes/1.10.0.json

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
{
2-
"date": "2020-05-27",
3-
"version": "1.10.0",
4-
"entries": [
5-
{
6-
"type": "Feature",
7-
"description": "Add basic visualisation capability for step function state machines defined in YAML."
8-
},
9-
{
10-
"type": "Feature",
11-
"description": "Step Functions Linter: Resource property of Task state will accept any string instead of just arn. Additional disallowed properties will be marked as invalid."
12-
},
13-
{
14-
"type": "Feature",
15-
"description": "If a file conflict is detected when downloading event schemas code bindings, a confirmation prompt is shown"
16-
}
17-
]
18-
}
2+
"date": "2020-05-27",
3+
"version": "1.10.0",
4+
"entries": [
5+
{
6+
"type": "Feature",
7+
"description": "Add basic visualisation capability for step function state machines defined in YAML."
8+
},
9+
{
10+
"type": "Feature",
11+
"description": "Step Functions Linter: Resource property of Task state will accept any string instead of just arn. Additional disallowed properties will be marked as invalid."
12+
},
13+
{
14+
"type": "Feature",
15+
"description": "If a file conflict is detected when downloading event schemas code bindings, a confirmation prompt is shown"
16+
}
17+
]
18+
}

package-lock.json

Lines changed: 35 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,7 @@
869869
"@types/glob": "^7.1.1",
870870
"@types/js-yaml": "^3.12.0",
871871
"@types/lodash": "^4.14.136",
872-
"@types/lolex": "^3.1.1",
872+
"@types/lolex": "^5.1.0",
873873
"@types/marked": "^0.6.5",
874874
"@types/mocha": "^7.0.2",
875875
"@types/node": "^10.14.22",
@@ -895,7 +895,7 @@
895895
"husky": "^2.3.0",
896896
"istanbul": "^0.4.5",
897897
"json-schema-to-typescript": "^8.2.0",
898-
"lolex": "^4.2.0",
898+
"lolex": "^5.1.0",
899899
"marked": "^0.7.0",
900900
"mocha": "^7.1.1",
901901
"mocha-junit-reporter": "^1.23.3",

package.nls.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@
269269
"AWS.sam.debugger.extraEnvVars": "The following environment variables are not found in the targeted template and will not be overridden: {0}",
270270
"AWS.sam.debugger.missingRuntime": "Debug Configurations with an invoke target of \"{0}\" require a valid Lambda runtime value, expected one of [{1}]",
271271
"AWS.sam.local.invoke.python.server.not.available": "Unable to communicate with the Python Debug Adapter. The debugger might not successfully attach to your SAM Application.",
272+
"aws.cloudWatchLogs.viewLogStream.workflow.prompt": "Select a log stream",
273+
"aws.cloudWatchLogs.viewLogStream.workflow.noStreams": "[No Log Events found]",
272274
"AWS.samcli.detect.settings.updated": "Settings updated.",
273275
"AWS.samcli.detect.settings.not.updated": "No settings changes necessary.",
274276
"AWS.samcli.deploy.general.error": "An error occurred while deploying a SAM Application. {0}",
@@ -356,7 +358,11 @@
356358
"AWS.generic.response.no": "No",
357359
"AWS.generic.response.yes": "Yes",
358360
"AWS.generic.notImplemented": "Not implemented",
361+
"AWS.generic.refresh": "Refresh",
359362
"AWS.template.error.showErrorDetails.title": "Error details for",
360363
"AWS.template.error.showErrorDetails.errorCode": "Error code",
361-
"AWS.template.error.showErrorDetails.errorMessage": "Error message"
364+
"AWS.template.error.showErrorDetails.errorMessage": "Error message",
365+
"AWS.picker.dynamic.noItemsFound.detail": "Click here to go back",
366+
"AWS.picker.dynamic.noItemsFound.label": "[No items found]",
367+
"AWS.picker.dynamic.errorNode.label": "There was an error retrieving more items."
362368
}

src/awsexplorer/activation.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as vscode from 'vscode'
77

8+
import { viewLogStream } from '../cloudWatchLogs/commands/viewLogStream'
89
import { LogGroupNode } from '../cloudWatchLogs/explorer/logGroupNode'
910
import { submitFeedback } from '../feedback/commands/submitFeedback'
1011
import { deleteCloudFormation } from '../lambda/commands/deleteCloudFormation'
@@ -200,8 +201,9 @@ async function registerAwsExplorerCommands(
200201
)
201202

202203
context.subscriptions.push(
203-
vscode.commands.registerCommand('aws.cloudWatchLogs.viewLogStream', async (node: LogGroupNode) =>
204-
vscode.window.showInformationMessage('Not implemented')
204+
vscode.commands.registerCommand(
205+
'aws.cloudWatchLogs.viewLogStream',
206+
async (node: LogGroupNode) => await viewLogStream(node)
205207
)
206208
)
207209
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*!
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as nls from 'vscode-nls'
7+
const localize = nls.loadMessageBundle()
8+
9+
import * as vscode from 'vscode'
10+
import * as moment from 'moment'
11+
import * as picker from '../../shared/ui/picker'
12+
import { MultiStepWizard, WizardStep } from '../../shared/wizards/multiStepWizard'
13+
import { LogGroupNode } from '../explorer/logGroupNode'
14+
import { CloudWatchLogs } from 'aws-sdk'
15+
import { ext } from '../../shared/extensionGlobals'
16+
import { CloudWatchLogsClient } from '../../shared/clients/cloudWatchLogsClient'
17+
import * as telemetry from '../../shared/telemetry/telemetry'
18+
import { LOCALIZED_DATE_FORMAT } from '../../shared/constants'
19+
import { getPaginatedAwsCallIter, IteratorTransformer } from '../../shared/utilities/collectionUtils'
20+
21+
export interface SelectLogStreamResponse {
22+
region: string
23+
logGroupName: string
24+
logStreamName: string
25+
}
26+
27+
export async function viewLogStream(node: LogGroupNode): Promise<void> {
28+
let result: telemetry.Result = 'Succeeded'
29+
const logStreamResponse = await new SelectLogStreamWizard(node).run()
30+
if (logStreamResponse) {
31+
vscode.window.showInformationMessage(
32+
`Not implemented but here's the deets:
33+
region: ${logStreamResponse.region}
34+
logGroup: ${logStreamResponse.logGroupName}
35+
logStream: ${logStreamResponse.logStreamName}`
36+
)
37+
} else {
38+
result = 'Cancelled'
39+
}
40+
41+
telemetry.recordCloudwatchlogsOpenStream({ result })
42+
}
43+
44+
export interface SelectLogStreamWizardContext {
45+
pickLogStream(): Promise<string | undefined>
46+
}
47+
48+
export class DefaultSelectLogStreamWizardContext implements SelectLogStreamWizardContext {
49+
public constructor(private readonly regionCode: string, private readonly logGroupName: string) {}
50+
51+
public async pickLogStream(): Promise<string | undefined> {
52+
let telemetryResult: telemetry.Result = 'Succeeded'
53+
54+
const client: CloudWatchLogsClient = ext.toolkitClientBuilder.createCloudWatchLogsClient(this.regionCode)
55+
const request: CloudWatchLogs.DescribeLogStreamsRequest = {
56+
logGroupName: this.logGroupName,
57+
orderBy: 'LastEventTime',
58+
descending: true,
59+
}
60+
const qp = picker.createQuickPick({})
61+
const populator = new IteratorTransformer(
62+
() =>
63+
getPaginatedAwsCallIter({
64+
awsCall: request => client.describeLogStreams(request),
65+
nextTokenNames: {
66+
request: 'nextToken',
67+
response: 'nextToken',
68+
},
69+
request,
70+
}),
71+
response => convertDescribeLogStreamsToQuickPickItems(response)
72+
)
73+
74+
const controller = new picker.IteratingQuickPickController(qp, populator)
75+
controller.startRequests()
76+
const choices = await picker.promptUser({
77+
picker: qp,
78+
onDidTriggerButton: (button, resolve, reject) =>
79+
controller.iteratingOnDidTriggerButton(button, resolve, reject),
80+
})
81+
82+
const val = picker.verifySinglePickerOutput(choices)
83+
84+
let result = val?.label
85+
86+
// handle no items for a group as a cancel
87+
if (!result || result === picker.IteratingQuickPickController.NO_ITEMS_ITEM.label) {
88+
result = undefined
89+
telemetryResult = 'Cancelled'
90+
}
91+
// retry handled by caller -- should this be a "Failed"?
92+
// of note: we don't track if an error pops up, we just track if the error is selected.
93+
if (result === picker.IteratingQuickPickController.ERROR_ITEM.label) {
94+
telemetryResult = 'Failed'
95+
}
96+
97+
telemetry.recordCloudwatchlogsOpenGroup({ result: telemetryResult })
98+
return result
99+
}
100+
}
101+
102+
export function convertDescribeLogStreamsToQuickPickItems(
103+
response: CloudWatchLogs.DescribeLogStreamsResponse
104+
): vscode.QuickPickItem[] {
105+
return (response.logStreams ?? []).map<vscode.QuickPickItem>(stream => ({
106+
label: stream.logStreamName!,
107+
detail: stream.lastEventTimestamp
108+
? moment(stream.lastEventTimestamp).format(LOCALIZED_DATE_FORMAT)
109+
: localize('aws.cloudWatchLogs.viewLogStream.workflow.noStreams', '[No Log Events found]'),
110+
}))
111+
}
112+
113+
export class SelectLogStreamWizard extends MultiStepWizard<SelectLogStreamResponse> {
114+
private readonly response: Partial<SelectLogStreamResponse>
115+
116+
public constructor(
117+
node: LogGroupNode,
118+
private readonly context: SelectLogStreamWizardContext = new DefaultSelectLogStreamWizardContext(
119+
node.regionCode,
120+
node.logGroup.logGroupName!
121+
)
122+
) {
123+
super()
124+
this.response = {
125+
region: node.regionCode,
126+
logGroupName: node.logGroup.logGroupName,
127+
}
128+
}
129+
130+
protected get startStep(): WizardStep {
131+
return this.SELECT_STREAM
132+
}
133+
134+
protected getResult(): SelectLogStreamResponse | undefined {
135+
if (!this.response.region || !this.response.logGroupName || !this.response.logStreamName) {
136+
return undefined
137+
}
138+
139+
return {
140+
region: this.response.region,
141+
logGroupName: this.response.logGroupName,
142+
logStreamName: this.response.logStreamName,
143+
}
144+
}
145+
146+
private readonly SELECT_STREAM: WizardStep = async () => {
147+
const returnVal = await this.context.pickLogStream()
148+
149+
// retry on error
150+
if (returnVal === picker.IteratingQuickPickController.ERROR_ITEM.label) {
151+
return this.SELECT_STREAM
152+
}
153+
154+
this.response.logStreamName = returnVal
155+
156+
return undefined
157+
}
158+
}

src/shared/clients/cloudWatchLogsClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ export interface CloudWatchLogsClient {
99
readonly regionCode: string
1010

1111
describeLogGroups(): AsyncIterableIterator<CloudWatchLogs.LogGroup>
12+
13+
describeLogStreams(
14+
request: CloudWatchLogs.DescribeLogStreamsRequest
15+
): Promise<CloudWatchLogs.DescribeLogStreamsResponse>
1216
}

src/shared/clients/defaultCloudWatchLogsClient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ export class DefaultCloudWatchLogsClient implements CloudWatchLogsClient {
2222
} while (request.nextToken)
2323
}
2424

25+
public async describeLogStreams(
26+
request: CloudWatchLogs.DescribeLogStreamsRequest
27+
): Promise<CloudWatchLogs.DescribeLogStreamsResponse> {
28+
const sdkClient = await this.createSdkClient()
29+
30+
return sdkClient.describeLogStreams(request).promise()
31+
}
32+
2533
protected async invokeDescribeLogGroups(
2634
request: CloudWatchLogs.DescribeLogGroupsRequest,
2735
sdkClient: CloudWatchLogs

src/shared/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,13 @@ export const sfnCreateStateMachineNameParamUrl: string =
5353
export const sfnDeveloperGuideUrl: string = 'https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html'
5454
export const sfnUpdateStateMachineUrl: string =
5555
'https://docs.aws.amazon.com/step-functions/latest/apireference/API_UpdateStateMachine.html'
56+
57+
/**
58+
* Moment format for rendering readable dates.
59+
*
60+
* Same format used in the S3 console, but it's also locale-aware.
61+
*
62+
* US: Jan 5, 2020 5:30:20 PM GMT-0700
63+
* GB: 5 Jan 2020 17:30:20 GMT+0100
64+
*/
65+
export const LOCALIZED_DATE_FORMAT = 'll LTS [GMT]ZZ'

0 commit comments

Comments
 (0)