Skip to content

Commit 78cb83c

Browse files
authored
feat(cwl): Create LiveTailSession object and registry. Adds MaxLine configuration (#5738)
## Problem 1. On the client side, VSCode needs to be aware of some additional context of a running LiveTail session, other than just the response from the StartLiveTail API call (Abort Controller, Max Lines config, TextDocument URI, etc). 2. Additionally, we want to be able to have multiple LiveTail sessions running, and have a way to organize all of them. ## Solution 1. Create a LiveTailSession class in AWSToolkit that will persist all of the metadata and context the LiveTail feature needs to operate. 2. Create a LiveTailSessionRegistry that maps a vscode URI to a LiveTailSession. The URI for a LiveTailSession is composed of the API request elements. This will allow us to tell when a user is making a duplicate request. This URI will also be used as the URI of the TextDocument to display the session results. ## Additional Changes * Adds VSCode preference config for LiveTail max events. This will be used to cap the number of live tail events can be in the TextDocument at a given time. When the limit is reached, the oldest log events will be dropped in order to fit new events streaming in. * Adds a missing String for "Tail Log Group"
1 parent b071ef1 commit 78cb83c

File tree

10 files changed

+3648
-2186
lines changed

10 files changed

+3648
-2186
lines changed

package-lock.json

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

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@
459459
},
460460
"dependencies": {
461461
"@amzn/codewhisperer-streaming": "file:../../src.gen/@amzn/codewhisperer-streaming",
462+
"@aws-sdk/client-cloudwatch-logs": "^3.666.0",
462463
"@aws-sdk/client-cognito-identity": "^3.637.0",
463464
"@aws-sdk/client-lambda": "^3.637.0",
464465
"@aws-sdk/client-sso": "^3.342.0",

packages/core/package.nls.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"AWS.command.downloadSchemaItemCode": "Download Code Bindings",
155155
"AWS.command.viewLogs": "View Logs",
156156
"AWS.command.cloudWatchLogs.searchLogGroup": "Search Log Group",
157+
"AWS.command.cloudWatchLogs.tailLogGroup": "Tail Log Group",
157158
"AWS.command.sam.newTemplate": "Create new SAM Template",
158159
"AWS.command.cloudFormation.newTemplate": "Create new CloudFormation Template",
159160
"AWS.command.quickStart": "View Quick Start",
@@ -231,6 +232,7 @@
231232
"AWS.cdk.explorerTitle": "CDK",
232233
"AWS.codecatalyst.explorerTitle": "CodeCatalyst",
233234
"AWS.cwl.limit.desc": "Maximum amount of log entries pulled per request from CloudWatch Logs (max 10000)",
235+
"AWS.cwl.liveTailMaxEvents.desc": "Maximum amount of log entries that can be kept in a single live tail session. When the limit is reached, the oldest events will be removed to accomodate new events (min 1000; max 10000)",
234236
"AWS.samcli.deploy.bucket.recentlyUsed": "Buckets recently used for SAM deployments",
235237
"AWS.submenu.amazonqEditorContextSubmenu.title": "Amazon Q",
236238
"AWS.submenu.auth.title": "Authentication",

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,7 @@ export function createURIFromArgs(
126126
return vscode.Uri.parse(uriStr)
127127
}
128128

129-
export class CloudWatchLogsSettings extends fromExtensionManifest('aws.cwl', { limit: Number }) {}
129+
export class CloudWatchLogsSettings extends fromExtensionManifest('aws.cwl', {
130+
limit: Number,
131+
liveTailMaxEvents: Number,
132+
}) {}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import { CloudWatchLogsClient, StartLiveTailCommand, StartLiveTailCommandOutput } from '@aws-sdk/client-cloudwatch-logs'
7+
import { LogStreamFilterResponse } from '../liveTailLogStreamSubmenu'
8+
import { CloudWatchLogsSettings } from '../cloudWatchLogsUtils'
9+
import { Settings, ToolkitError } from '../../../shared'
10+
import { createLiveTailURIFromArgs } from './liveTailSessionRegistry'
11+
12+
export type LiveTailSessionConfiguration = {
13+
logGroupName: string
14+
logStreamFilter?: LogStreamFilterResponse
15+
logEventFilterPattern?: string
16+
region: string
17+
}
18+
19+
export type LiveTailSessionClient = {
20+
cwlClient: CloudWatchLogsClient
21+
abortController: AbortController
22+
}
23+
24+
export class LiveTailSession {
25+
private liveTailClient: LiveTailSessionClient
26+
private _logGroupName: string
27+
private logStreamFilter?: LogStreamFilterResponse
28+
private logEventFilterPattern?: string
29+
private _maxLines: number
30+
private _uri: vscode.Uri
31+
32+
static settings = new CloudWatchLogsSettings(Settings.instance)
33+
34+
public constructor(configuration: LiveTailSessionConfiguration) {
35+
this._logGroupName = configuration.logGroupName
36+
this.logStreamFilter = configuration.logStreamFilter
37+
this.liveTailClient = {
38+
cwlClient: new CloudWatchLogsClient({ region: configuration.region }),
39+
abortController: new AbortController(),
40+
}
41+
this._maxLines = LiveTailSession.settings.get('liveTailMaxEvents', 10000)
42+
this._uri = createLiveTailURIFromArgs(configuration)
43+
}
44+
45+
public get maxLines() {
46+
return this._maxLines
47+
}
48+
49+
public get uri() {
50+
return this._uri
51+
}
52+
53+
public get logGroupName() {
54+
return this._logGroupName
55+
}
56+
57+
public startLiveTailSession(): Promise<StartLiveTailCommandOutput> {
58+
const command = this.buildStartLiveTailCommand()
59+
try {
60+
return this.liveTailClient.cwlClient.send(command, {
61+
abortSignal: this.liveTailClient.abortController.signal,
62+
})
63+
} catch (e) {
64+
throw new ToolkitError('Encountered error while trying to start LiveTail session.')
65+
}
66+
}
67+
68+
public stopLiveTailSession() {
69+
this.liveTailClient.abortController.abort()
70+
this.liveTailClient.cwlClient.destroy()
71+
}
72+
73+
private buildStartLiveTailCommand(): StartLiveTailCommand {
74+
let logStreamNamePrefix = undefined
75+
let logStreamName = undefined
76+
if (this.logStreamFilter) {
77+
if (this.logStreamFilter.type === 'prefix') {
78+
logStreamNamePrefix = this.logStreamFilter.filter
79+
logStreamName = undefined
80+
} else if (this.logStreamFilter.type === 'specific') {
81+
logStreamName = this.logStreamFilter.filter
82+
logStreamNamePrefix = undefined
83+
}
84+
}
85+
86+
return new StartLiveTailCommand({
87+
logGroupIdentifiers: [this.logGroupName],
88+
logStreamNamePrefixes: logStreamNamePrefix ? [logStreamNamePrefix] : undefined,
89+
logStreamNames: logStreamName ? [logStreamName] : undefined,
90+
logEventFilterPattern: this.logEventFilterPattern ? this.logEventFilterPattern : undefined,
91+
})
92+
}
93+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import { CLOUDWATCH_LOGS_LIVETAIL_SCHEME } from '../../../shared/constants'
7+
import { LiveTailSession, LiveTailSessionConfiguration } from './liveTailSession'
8+
import { ToolkitError } from '../../../shared'
9+
import { NestedMap } from '../../../shared/utilities/map'
10+
11+
export class LiveTailSessionRegistry extends NestedMap<vscode.Uri, LiveTailSession> {
12+
static #instance: LiveTailSessionRegistry
13+
14+
public static get instance() {
15+
return (this.#instance ??= new this())
16+
}
17+
18+
public constructor() {
19+
super()
20+
}
21+
22+
protected override hash(uri: vscode.Uri): string {
23+
return uri.toString()
24+
}
25+
26+
protected override get name(): string {
27+
return LiveTailSessionRegistry.name
28+
}
29+
30+
protected override get default(): LiveTailSession {
31+
throw new ToolkitError('No LiveTailSession found for provided uri.')
32+
}
33+
}
34+
35+
export function createLiveTailURIFromArgs(sessionData: LiveTailSessionConfiguration): vscode.Uri {
36+
let uriStr = `${CLOUDWATCH_LOGS_LIVETAIL_SCHEME}:${sessionData.region}:${sessionData.logGroupName}`
37+
38+
if (sessionData.logStreamFilter) {
39+
if (sessionData.logStreamFilter.type !== 'all') {
40+
uriStr += `:${sessionData.logStreamFilter.type}:${sessionData.logStreamFilter.filter}`
41+
} else {
42+
uriStr += `:${sessionData.logStreamFilter.type}`
43+
}
44+
}
45+
uriStr += sessionData.logEventFilterPattern ? `:${sessionData.logEventFilterPattern}` : ''
46+
47+
return vscode.Uri.parse(uriStr)
48+
}

packages/core/src/shared/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ export const ecsIamPermissionsUrl = vscode.Uri.parse(
128128
* URI scheme for CloudWatch Logs Virtual Documents
129129
*/
130130
export const CLOUDWATCH_LOGS_SCHEME = 'aws-cwl' // eslint-disable-line @typescript-eslint/naming-convention
131+
export const CLOUDWATCH_LOGS_LIVETAIL_SCHEME = 'aws-cwl-lt' // eslint-disable-line @typescript-eslint/naming-convention
132+
131133
export const AWS_SCHEME = 'aws' // eslint-disable-line @typescript-eslint/naming-convention
132134
export const amazonQDiffScheme = 'amazon-q-diff'
133135

packages/core/src/shared/settings-toolkit.gen.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const toolkitSettings = {
2222
"aws.stepfunctions.asl.maxItemsComputed": {},
2323
"aws.ssmDocument.ssm.maxItemsComputed": {},
2424
"aws.cwl.limit": {},
25+
"aws.cwl.liveTailMaxEvents": {},
2526
"aws.samcli.manuallySelectedBuckets": {},
2627
"aws.samcli.enableCodeLenses": {},
2728
"aws.suppressPrompts": {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import assert from 'assert'
7+
import {
8+
LiveTailSession,
9+
LiveTailSessionConfiguration,
10+
} from '../../../../awsService/cloudWatchLogs/registry/liveTailSession'
11+
import {
12+
createLiveTailURIFromArgs,
13+
LiveTailSessionRegistry,
14+
} from '../../../../awsService/cloudWatchLogs/registry/liveTailSessionRegistry'
15+
import { CLOUDWATCH_LOGS_LIVETAIL_SCHEME } from '../../../../shared/constants'
16+
17+
/**
18+
* Exposes protected methods so we can test them
19+
*/
20+
class TestLiveTailSessionRegistry extends LiveTailSessionRegistry {
21+
constructor() {
22+
super()
23+
}
24+
25+
override hash(uri: vscode.Uri): string {
26+
return super.hash(uri)
27+
}
28+
29+
override get default(): LiveTailSession {
30+
return super.default
31+
}
32+
}
33+
34+
describe('LiveTailRegistry', async function () {
35+
const session = new LiveTailSession({
36+
logGroupName: 'test-log-group',
37+
region: 'test-region',
38+
})
39+
40+
let liveTailSessionRegistry: TestLiveTailSessionRegistry
41+
42+
beforeEach(function () {
43+
liveTailSessionRegistry = new TestLiveTailSessionRegistry()
44+
})
45+
46+
it('hash()', function () {
47+
assert.deepStrictEqual(liveTailSessionRegistry.hash(session.uri), session.uri.toString())
48+
})
49+
50+
it('default()', function () {
51+
assert.throws(() => liveTailSessionRegistry.default)
52+
})
53+
})
54+
55+
describe('LiveTailSession URI', async function () {
56+
const testLogGroupName = 'test-log-group'
57+
const testRegion = 'test-region'
58+
const expectedUriBase = `${CLOUDWATCH_LOGS_LIVETAIL_SCHEME}:${testRegion}:${testLogGroupName}`
59+
60+
it('is correct with no logStream filter, no filter pattern', function () {
61+
const config: LiveTailSessionConfiguration = {
62+
logGroupName: testLogGroupName,
63+
region: testRegion,
64+
}
65+
const expectedUri = vscode.Uri.parse(expectedUriBase)
66+
const uri = createLiveTailURIFromArgs(config)
67+
assert.deepEqual(uri, expectedUri)
68+
})
69+
70+
it('is correct with no logStream filter, with filter pattern', function () {
71+
const config: LiveTailSessionConfiguration = {
72+
logGroupName: testLogGroupName,
73+
region: testRegion,
74+
logEventFilterPattern: 'test-filter',
75+
}
76+
const expectedUri = vscode.Uri.parse(`${expectedUriBase}:test-filter`)
77+
const uri = createLiveTailURIFromArgs(config)
78+
assert.deepEqual(uri, expectedUri)
79+
})
80+
81+
it('is correct with ALL logStream filter', function () {
82+
const config: LiveTailSessionConfiguration = {
83+
logGroupName: testLogGroupName,
84+
region: testRegion,
85+
logStreamFilter: {
86+
type: 'all',
87+
},
88+
}
89+
const expectedUri = vscode.Uri.parse(`${expectedUriBase}:all`)
90+
const uri = createLiveTailURIFromArgs(config)
91+
assert.deepEqual(uri, expectedUri)
92+
})
93+
94+
it('is correct with prefix logStream filter', function () {
95+
const config: LiveTailSessionConfiguration = {
96+
logGroupName: testLogGroupName,
97+
region: testRegion,
98+
logStreamFilter: {
99+
type: 'prefix',
100+
filter: 'test-prefix',
101+
},
102+
}
103+
const expectedUri = vscode.Uri.parse(`${expectedUriBase}:prefix:test-prefix`)
104+
const uri = createLiveTailURIFromArgs(config)
105+
assert.deepEqual(uri, expectedUri)
106+
})
107+
108+
it('is correct with specific logStream filter', function () {
109+
const config: LiveTailSessionConfiguration = {
110+
logGroupName: testLogGroupName,
111+
region: testRegion,
112+
logStreamFilter: {
113+
type: 'specific',
114+
filter: 'test-stream',
115+
},
116+
}
117+
const expectedUri = vscode.Uri.parse(`${expectedUriBase}:specific:test-stream`)
118+
const uri = createLiveTailURIFromArgs(config)
119+
assert.deepEqual(uri, expectedUri)
120+
})
121+
122+
it('is correct with specific logStream filter and filter pattern', function () {
123+
const config: LiveTailSessionConfiguration = {
124+
logGroupName: testLogGroupName,
125+
region: testRegion,
126+
logStreamFilter: {
127+
type: 'specific',
128+
filter: 'test-stream',
129+
},
130+
logEventFilterPattern: 'test-filter',
131+
}
132+
const expectedUri = vscode.Uri.parse(`${expectedUriBase}:specific:test-stream:test-filter`)
133+
const uri = createLiveTailURIFromArgs(config)
134+
assert.deepEqual(uri, expectedUri)
135+
})
136+
})

packages/toolkit/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,13 @@
162162
"description": "%AWS.cwl.limit.desc%",
163163
"maximum": 10000
164164
},
165+
"aws.cwl.liveTailMaxEvents": {
166+
"type": "number",
167+
"default": 10000,
168+
"description": "%AWS.cwl.liveTailMaxEvents.desc%",
169+
"minimum": 1000,
170+
"maximum": 10000
171+
},
165172
"aws.samcli.manuallySelectedBuckets": {
166173
"type": "object",
167174
"description": "%AWS.samcli.deploy.bucket.recentlyUsed%",

0 commit comments

Comments
 (0)