Skip to content

Commit ed323d7

Browse files
feat(ec2): provide link for customer to view system logs (#5633)
## Problem Limited ability to diagnose problems with EC2 instances provided in the toolkit. ## Solution Add a right click option to provide system log of Ec2 instance in read-only file view. ### Implementation Details - Log view does NOT live update. This would require refactoring the CWL significantly. Better to get the feature out and see if this is something customers want/need. - To reduce code duplication two new utility components were added `decodeBase64` in`core/src/shared/utilities/textUtilities.ts` and `UriSchema` in `core/src/shared/utilities/uriUtils.ts`. - `decodeBase64` is a helper function to wrap the use of `Buffer.from(X, "base64").toString()` throughout the toolkit. - `UriSchema` provides a general framework for translating between an object and an URI, and vice versa. It allows us to avoid writing the `isValid` method in all cases. With more work this could likely be more general, but is only used twice so unclear if it is worth pursuing. - These changes involved changing some function calls, especially in the CWL code, but does not impact functionality. ### Pictures <img width="1231" alt="image" src="https://github.com/user-attachments/assets/0991c51e-6e27-4102-a85f-08246a7faab0"> <img width="1508" alt="image" src="https://github.com/user-attachments/assets/558d831f-7a7c-475b-9400-719ce8fd99d7"> --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Justin M. Keyes <[email protected]>
1 parent c44fa30 commit ed323d7

28 files changed

+238
-86
lines changed

packages/core/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
"AWS.command.ec2.stopInstance": "Stop EC2 Instance",
130130
"AWS.command.ec2.rebootInstance": "Reboot EC2 Instance",
131131
"AWS.command.ec2.copyInstanceId": "Copy Instance Id",
132+
"AWS.command.ec2.viewLogs": "View EC2 Logs",
132133
"AWS.command.ecr.copyTagUri": "Copy Tag URI",
133134
"AWS.command.ecr.copyRepositoryUri": "Copy Repository URI",
134135
"AWS.command.ecr.createRepository": "Create Repository...",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { CancellationError } from '../../shared/utilities/timeoutUtils'
66
import { telemetry } from '../../shared/telemetry/telemetry'
77
import { showInputBox } from '../../shared/ui/inputPrompter'
8-
import { createURIFromArgs, isLogStreamUri, recordTelemetryFilter } from './cloudWatchLogsUtils'
8+
import { cwlUriSchema, isLogStreamUri, recordTelemetryFilter } from './cloudWatchLogsUtils'
99
import { prepareDocument } from './commands/searchLogGroup'
1010
import { getActiveDocumentUri } from './document/logDataDocumentProvider'
1111
import { CloudWatchLogsData, filterLogEventsFromUri, LogDataRegistry } from './registry/logDataRegistry'
@@ -98,7 +98,7 @@ export async function changeLogSearchParams(
9898
throw new CancellationError('user')
9999
}
100100

101-
const newUri = createURIFromArgs(newData.logGroupInfo, newData.parameters)
101+
const newUri = cwlUriSchema.form({ logGroupInfo: newData.logGroupInfo, parameters: newData.parameters })
102102
await prepareDocument(newUri, newData, registry)
103103
})
104104
}

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

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { telemetry } from '../../shared/telemetry/telemetry'
66
import * as vscode from 'vscode'
77
import { CLOUDWATCH_LOGS_SCHEME } from '../../shared/constants'
88
import { fromExtensionManifest } from '../../shared/settings'
9-
import { CloudWatchLogsData, CloudWatchLogsGroupInfo } from './registry/logDataRegistry'
9+
import { CloudWatchLogsArgs, CloudWatchLogsData, CloudWatchLogsGroupInfo } from './registry/logDataRegistry'
1010
import { CloudWatchLogsParameters } from './registry/logDataRegistry'
11+
import { UriSchema } from '../../shared/utilities/uriUtils'
1112

1213
// URIs are the only vehicle for delivering information to a TextDocumentContentProvider.
1314
// The following functions are used to structure and destructure relevant information to/from a URI.
@@ -32,8 +33,7 @@ export function recordTelemetryFilter(logData: CloudWatchLogsData): void {
3233
export function uriToKey(uri: vscode.Uri): string {
3334
if (uri.query) {
3435
try {
35-
const { filterPattern, startTime, endTime, limit, streamNameOptions } =
36-
parseCloudWatchLogsUri(uri).parameters
36+
const { filterPattern, startTime, endTime, limit, streamNameOptions } = cwlUriSchema.parse(uri).parameters
3737
const parts = [uri.path, filterPattern, startTime, endTime, limit, streamNameOptions]
3838
return parts.map((p) => p ?? '').join(':')
3939
} catch {
@@ -52,18 +52,15 @@ export function uriToKey(uri: vscode.Uri): string {
5252
* message as the actual log group search. That results in a more fluid UX.
5353
*/
5454
export function msgKey(logGroupInfo: CloudWatchLogsGroupInfo): string {
55-
const uri = createURIFromArgs(logGroupInfo, {})
55+
const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: {} })
5656
return uri.toString()
5757
}
5858

5959
/**
6060
* Destructures an awsCloudWatchLogs URI into its component pieces.
6161
* @param uri URI for a Cloudwatch Logs file
6262
*/
63-
export function parseCloudWatchLogsUri(uri: vscode.Uri): {
64-
logGroupInfo: CloudWatchLogsGroupInfo
65-
parameters: CloudWatchLogsParameters
66-
} {
63+
function parseCloudWatchLogsUri(uri: vscode.Uri): CloudWatchLogsArgs {
6764
const parts = uri.path.split(':')
6865

6966
if (uri.scheme !== CLOUDWATCH_LOGS_SCHEME) {
@@ -85,18 +82,8 @@ export function parseCloudWatchLogsUri(uri: vscode.Uri): {
8582
}
8683
}
8784

88-
/** True if given URI is a valid Cloud Watch Logs Uri */
89-
export function isCwlUri(uri: vscode.Uri): boolean {
90-
try {
91-
parseCloudWatchLogsUri(uri)
92-
return true
93-
} catch {
94-
return false
95-
}
96-
}
97-
9885
export function patternFromCwlUri(uri: vscode.Uri): CloudWatchLogsParameters['filterPattern'] {
99-
return parseCloudWatchLogsUri(uri).parameters.filterPattern
86+
return cwlUriSchema.parse(uri).parameters.filterPattern
10087
}
10188

10289
/**
@@ -105,7 +92,7 @@ export function patternFromCwlUri(uri: vscode.Uri): CloudWatchLogsParameters['fi
10592
* @returns
10693
*/
10794
export function isLogStreamUri(uri: vscode.Uri): boolean {
108-
const logGroupInfo = parseCloudWatchLogsUri(uri).logGroupInfo
95+
const logGroupInfo = cwlUriSchema.parse(uri).logGroupInfo
10996
return logGroupInfo.streamName !== undefined
11097
}
11198

@@ -115,15 +102,13 @@ export function isLogStreamUri(uri: vscode.Uri): boolean {
115102
* @param streamName Log stream name
116103
* @param regionName AWS region
117104
*/
118-
export function createURIFromArgs(
119-
logGroupInfo: CloudWatchLogsGroupInfo,
120-
parameters: CloudWatchLogsParameters
121-
): vscode.Uri {
122-
let uriStr = `${CLOUDWATCH_LOGS_SCHEME}:${logGroupInfo.regionName}:${logGroupInfo.groupName}`
123-
uriStr += logGroupInfo.streamName ? `:${logGroupInfo.streamName}` : ''
105+
function createURIFromArgs(args: CloudWatchLogsArgs): vscode.Uri {
106+
let uriStr = `${CLOUDWATCH_LOGS_SCHEME}:${args.logGroupInfo.regionName}:${args.logGroupInfo.groupName}`
107+
uriStr += args.logGroupInfo.streamName ? `:${args.logGroupInfo.streamName}` : ''
124108

125-
uriStr += `?${encodeURIComponent(JSON.stringify(parameters))}`
109+
uriStr += `?${encodeURIComponent(JSON.stringify(args.parameters))}`
126110
return vscode.Uri.parse(uriStr)
127111
}
112+
export const cwlUriSchema = new UriSchema<CloudWatchLogsArgs>(parseCloudWatchLogsUri, createURIFromArgs)
128113

129114
export class CloudWatchLogsSettings extends fromExtensionManifest('aws.cwl', { limit: Number }) {}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as nls from 'vscode-nls'
77
const localize = nls.loadMessageBundle()
88

99
import * as vscode from 'vscode'
10-
import { isLogStreamUri, parseCloudWatchLogsUri } from '../cloudWatchLogsUtils'
10+
import { cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils'
1111
import { copyToClipboard } from '../../../shared/utilities/messages'
1212

1313
export async function copyLogResource(uri?: vscode.Uri): Promise<void> {
@@ -20,7 +20,7 @@ export async function copyLogResource(uri?: vscode.Uri): Promise<void> {
2020
throw new Error('no active text editor, or undefined URI')
2121
}
2222
}
23-
const parsedUri = parseCloudWatchLogsUri(uri)
23+
const parsedUri = cwlUriSchema.parse(uri)
2424
const resourceName = isLogStreamUri(uri) ? parsedUri.logGroupInfo.streamName : parsedUri.logGroupInfo.groupName
2525

2626
if (!resourceName) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as vscode from 'vscode'
77
import * as nls from 'vscode-nls'
88
const localize = nls.loadMessageBundle()
99

10-
import { isLogStreamUri, parseCloudWatchLogsUri } from '../cloudWatchLogsUtils'
10+
import { cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils'
1111
import { telemetry, CloudWatchResourceType, Result } from '../../../shared/telemetry/telemetry'
1212
import fs from '../../../shared/fs/fs'
1313

@@ -28,7 +28,7 @@ export async function saveCurrentLogDataContent(): Promise<void> {
2828
const workspaceDir = vscode.workspace.workspaceFolders
2929
? vscode.workspace.workspaceFolders[0].uri
3030
: vscode.Uri.file(fs.getUserHomeDir())
31-
const uriComponents = parseCloudWatchLogsUri(uri)
31+
const uriComponents = cwlUriSchema.parse(uri)
3232
const logGroupInfo = uriComponents.logGroupInfo
3333

3434
const localizedLogFile = localize('AWS.command.saveCurrentLogDataContent.logfile', 'Log File')

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '../registry/logDataRegistry'
1717
import { DataQuickPickItem } from '../../../shared/ui/pickerPrompter'
1818
import { isValidResponse, isWizardControl, Wizard, WIZARD_RETRY } from '../../../shared/wizards/wizard'
19-
import { createURIFromArgs, msgKey, parseCloudWatchLogsUri, recordTelemetryFilter } from '../cloudWatchLogsUtils'
19+
import { cwlUriSchema, msgKey, recordTelemetryFilter } from '../cloudWatchLogsUtils'
2020
import { DefaultCloudWatchLogsClient } from '../../../shared/clients/cloudWatchLogsClient'
2121
import { CancellationError } from '../../../shared/utilities/timeoutUtils'
2222
import { getLogger } from '../../../shared/logger'
@@ -29,6 +29,7 @@ import { createBackButton, createExitButton, createHelpButton } from '../../../s
2929
import { PromptResult } from '../../../shared/ui/prompter'
3030
import { ToolkitError } from '../../../shared/errors'
3131
import { Messages } from '../../../shared/utilities/messages'
32+
import { showFile } from '../../../shared/utilities/textDocumentUtilities'
3233

3334
const localize = nls.loadMessageBundle()
3435

@@ -65,9 +66,7 @@ export async function prepareDocument(uri: vscode.Uri, logData: CloudWatchLogsDa
6566
try {
6667
// Gets the data: calls filterLogEventsFromUri().
6768
await registry.fetchNextLogEvents(uri)
68-
const doc = await vscode.workspace.openTextDocument(uri)
69-
await vscode.window.showTextDocument(doc, { preview: false })
70-
await vscode.languages.setTextDocumentLanguage(doc, 'log')
69+
await showFile(uri)
7170
} catch (err) {
7271
if (CancellationError.isUserCancelled(err)) {
7372
throw err
@@ -78,7 +77,7 @@ export async function prepareDocument(uri: vscode.Uri, logData: CloudWatchLogsDa
7877
localize(
7978
'AWS.cwl.searchLogGroup.errorRetrievingLogs',
8079
'Failed to get logs for {0}',
81-
parseCloudWatchLogsUri(uri).logGroupInfo.groupName
80+
cwlUriSchema.parse(uri).logGroupInfo.groupName
8281
)
8382
)
8483
}
@@ -105,7 +104,7 @@ export async function searchLogGroup(
105104
}
106105

107106
const userResponse = handleWizardResponse(response, registry)
108-
const uri = createURIFromArgs(userResponse.logGroupInfo, userResponse.parameters)
107+
const uri = cwlUriSchema.form({ logGroupInfo: userResponse.logGroupInfo, parameters: userResponse.parameters })
109108
await prepareDocument(uri, userResponse, registry)
110109
})
111110
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ import {
2121
initLogData as initLogData,
2222
filterLogEventsFromUri,
2323
} from '../registry/logDataRegistry'
24-
import { createURIFromArgs } from '../cloudWatchLogsUtils'
2524
import { prepareDocument, searchLogGroup } from './searchLogGroup'
2625
import { telemetry, Result } from '../../../shared/telemetry/telemetry'
2726
import { CancellationError } from '../../../shared/utilities/timeoutUtils'
2827
import { formatLocalized } from '../../../shared/utilities/textUtilities'
28+
import { cwlUriSchema } from '../cloudWatchLogsUtils'
2929

3030
export async function viewLogStream(node: LogGroupNode, registry: LogDataRegistry): Promise<void> {
3131
await telemetry.cloudwatchlogs_open.run(async (span) => {
@@ -52,7 +52,7 @@ export async function viewLogStream(node: LogGroupNode, registry: LogDataRegistr
5252
limit: registry.configuration.get('limit', 10000),
5353
}
5454

55-
const uri = createURIFromArgs(logGroupInfo, parameters)
55+
const uri = cwlUriSchema.form({ logGroupInfo: logGroupInfo, parameters: parameters })
5656
const logData = initLogData(logGroupInfo, parameters, filterLogEventsFromUri)
5757
await prepareDocument(uri, logData, registry)
5858
})

packages/core/src/awsService/cloudWatchLogs/document/logDataDocumentProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import * as vscode from 'vscode'
66
import { CloudWatchLogsGroupInfo, LogDataRegistry, UriString } from '../registry/logDataRegistry'
77
import { getLogger } from '../../../shared/logger'
8-
import { isCwlUri } from '../cloudWatchLogsUtils'
98
import { generateTextFromLogEvents, LineToLogStreamMap } from './textContent'
9+
import { cwlUriSchema } from '../cloudWatchLogsUtils'
1010

1111
export class LogDataDocumentProvider implements vscode.TextDocumentContentProvider {
1212
/** Resolves the correct {@link LineToLogStreamMap} instance for a given URI */
@@ -26,7 +26,7 @@ export class LogDataDocumentProvider implements vscode.TextDocumentContentProvid
2626
}
2727

2828
public provideTextDocumentContent(uri: vscode.Uri): string {
29-
if (!isCwlUri(uri)) {
29+
if (!cwlUriSchema.isValid(uri)) {
3030
throw new Error(`Uri is not a CWL Uri, so no text can be provided: ${uri.toString()}`)
3131
}
3232
const events = this.registry.fetchCachedLogEvents(uri)

packages/core/src/awsService/cloudWatchLogs/document/logStreamsCodeLensProvider.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@
66
import * as vscode from 'vscode'
77
import { CLOUDWATCH_LOGS_SCHEME } from '../../../shared/constants'
88
import { CloudWatchLogsGroupInfo, LogDataRegistry } from '../registry/logDataRegistry'
9-
import {
10-
CloudWatchLogsSettings,
11-
createURIFromArgs,
12-
isLogStreamUri,
13-
parseCloudWatchLogsUri,
14-
} from '../cloudWatchLogsUtils'
9+
import { CloudWatchLogsSettings, cwlUriSchema, isLogStreamUri } from '../cloudWatchLogsUtils'
1510
import { LogDataDocumentProvider } from './logDataDocumentProvider'
1611

1712
type IdWithLine = { streamId: string; lineNum: number }
@@ -43,7 +38,7 @@ export class LogStreamCodeLensProvider implements vscode.CodeLensProvider {
4338
return []
4439
}
4540

46-
const logGroupInfo = parseCloudWatchLogsUri(uri).logGroupInfo
41+
const logGroupInfo = cwlUriSchema.parse(uri).logGroupInfo
4742

4843
if (logGroupInfo.streamName) {
4944
// This means we have a stream file not a log search.
@@ -64,7 +59,11 @@ export class LogStreamCodeLensProvider implements vscode.CodeLensProvider {
6459
createLogStreamCodeLens(logGroupInfo: CloudWatchLogsGroupInfo, idWithLine: IdWithLine): vscode.CodeLens {
6560
const settings = new CloudWatchLogsSettings()
6661
const limit = settings.get('limit', 1000)
67-
const streamUri = createURIFromArgs({ ...logGroupInfo, streamName: idWithLine.streamId }, { limit: limit })
62+
const cwlArgs = {
63+
logGroupInfo: { ...logGroupInfo, streamName: idWithLine.streamId },
64+
parameters: { limit: limit },
65+
}
66+
const streamUri = cwlUriSchema.form(cwlArgs)
6867
const cmd: vscode.Command = {
6968
command: 'aws.loadLogStreamFile',
7069
arguments: [streamUri, this.registry],

packages/core/src/awsService/cloudWatchLogs/registry/logDataRegistry.ts

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

66
import * as vscode from 'vscode'
77
import { CloudWatchLogs } from 'aws-sdk'
8-
import { CloudWatchLogsSettings, parseCloudWatchLogsUri, uriToKey, msgKey } from '../cloudWatchLogsUtils'
8+
import { CloudWatchLogsSettings, uriToKey, msgKey, cwlUriSchema } from '../cloudWatchLogsUtils'
99
import { DefaultCloudWatchLogsClient } from '../../../shared/clients/cloudWatchLogsClient'
1010
import { waitTimeout } from '../../../shared/utilities/timeoutUtils'
1111
import { Messages } from '../../../shared/utilities/messages'
@@ -190,7 +190,7 @@ export class LogDataRegistry {
190190
if (this.isRegistered(uri)) {
191191
throw new Error(`Already registered: ${uri.toString()}`)
192192
}
193-
const data = parseCloudWatchLogsUri(uri)
193+
const data = cwlUriSchema.parse(uri)
194194
this.setLogData(uri, initLogData(data.logGroupInfo, data.parameters, retrieveLogsFunction))
195195
}
196196

@@ -281,6 +281,11 @@ export function initLogData(
281281
}
282282
}
283283

284+
export type CloudWatchLogsArgs = {
285+
logGroupInfo: CloudWatchLogsGroupInfo
286+
parameters: CloudWatchLogsParameters
287+
}
288+
284289
export type CloudWatchLogsGroupInfo = {
285290
groupName: string
286291
regionName: string

0 commit comments

Comments
 (0)