diff --git a/package.json b/package.json index 7eb1591..ecaf885 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,16 @@ "title": "%vscode-dapr.applications.stop-app.title%", "category": "Dapr" }, + { + "command": "vscode-dapr.applications.view-app-logs", + "title": "%vscode-dapr.applications.view-app-logs.title%", + "category": "Dapr" + }, + { + "command": "vscode-dapr.applications.view-dapr-logs", + "title": "%vscode-dapr.applications.view-dapr-logs.title%", + "category": "Dapr" + }, { "command": "vscode-dapr.help.getStarted", "title": "%vscode-dapr.help.getStarted.title%", @@ -184,6 +194,14 @@ "command": "vscode-dapr.applications.debug", "when": "never" }, + { + "command": "vscode-dapr.applications.view-app-logs", + "when": "never" + }, + { + "command": "vscode-dapr.applications.view-dapr-logs", + "when": "never" + }, { "command": "vscode-dapr.runs.debug", "when": "never" @@ -229,6 +247,16 @@ "when": "view == vscode-dapr.views.applications && viewItem =~ /application/", "group": "stop" }, + { + "command": "vscode-dapr.applications.view-app-logs", + "when": "view == vscode-dapr.views.applications && viewItem =~ /application/ && viewItem =~ /hasLogs/", + "group": "logs" + }, + { + "command": "vscode-dapr.applications.view-dapr-logs", + "when": "view == vscode-dapr.views.applications && viewItem =~ /application/ && viewItem =~ /hasLogs/", + "group": "logs" + }, { "command": "vscode-dapr.runs.debug", "when": "view == vscode-dapr.views.applications && viewItem =~ /run/ && viewItem =~ /attachable/", diff --git a/package.nls.json b/package.nls.json index b531f30..8292752 100644 --- a/package.nls.json +++ b/package.nls.json @@ -5,6 +5,8 @@ "vscode-dapr.applications.publish-message.title": "Publish Message to Application", "vscode-dapr.applications.publish-all-message.title": "Publish Message to All Applications", "vscode-dapr.applications.stop-app.title": "Stop Application", + "vscode-dapr.applications.view-app-logs.title": "View Application Logs", + "vscode-dapr.applications.view-dapr-logs.title": "View Dapr Logs", "vscode-dapr.configuration.paths.daprPath.description": "The full path to the dapr binary.", "vscode-dapr.configuration.paths.daprdPath.description": "The full path to the daprd binary.", diff --git a/src/commands/applications/debugApplication.ts b/src/commands/applications/debugApplication.ts index 706ba79..469da79 100644 --- a/src/commands/applications/debugApplication.ts +++ b/src/commands/applications/debugApplication.ts @@ -109,7 +109,7 @@ export async function debugApplication(application: DaprApplication): Promise (context: IActionContext, node: DaprApplicationNode | undefined): Promise => { if (node == undefined) { - throw new Error(localize('commands.applications.debugApplication.noPaletteSupport', 'Debugging requires selecting an application in the Dapr view.')); + throw new Error(localize('commands.applications.viewLogs.noPaletteSupport', 'Debugging requires selecting an application in the Dapr view.')); } return debugApplication(node.application); diff --git a/src/commands/applications/viewAppLogs.ts b/src/commands/applications/viewAppLogs.ts new file mode 100644 index 0000000..1711550 --- /dev/null +++ b/src/commands/applications/viewAppLogs.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as nls from 'vscode-nls'; +import DaprApplicationNode from "../../views/applications/daprApplicationNode"; +import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { getLocalizationPathForFile } from '../../util/localization'; +import { viewLogs } from './viewLogs'; + +const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); + +const createViewAppLogsCommand = () => (context: IActionContext, node: DaprApplicationNode | undefined): Promise => { + if (node == undefined) { + throw new Error(localize('commands.applications.viewAppLogs.noPaletteSupport', 'Viewing application logs requires selecting an application in the Dapr view.')); + } + + return viewLogs(node.application, 'app'); +} + +export default createViewAppLogsCommand; diff --git a/src/commands/applications/viewDaprLogs.ts b/src/commands/applications/viewDaprLogs.ts new file mode 100644 index 0000000..7196022 --- /dev/null +++ b/src/commands/applications/viewDaprLogs.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as nls from 'vscode-nls'; +import DaprApplicationNode from "../../views/applications/daprApplicationNode"; +import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { getLocalizationPathForFile } from '../../util/localization'; +import { viewLogs } from "./viewLogs"; + +const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); + +const createViewDaprLogsCommand = () => (context: IActionContext, node: DaprApplicationNode | undefined): Promise => { + if (node == undefined) { + throw new Error(localize('commands.applications.viewDaprLogs.noPaletteSupport', 'Viewing Dapr logs requires selecting an application in the Dapr view.')); + } + + return viewLogs(node.application, 'daprd'); +} + +export default createViewDaprLogsCommand; diff --git a/src/commands/applications/viewLogs.ts b/src/commands/applications/viewLogs.ts new file mode 100644 index 0000000..61d83d8 --- /dev/null +++ b/src/commands/applications/viewLogs.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as nls from 'vscode-nls'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getLocalizationPathForFile } from '../../util/localization'; +import { DaprApplication } from "../../services/daprApplicationProvider"; +import { fromRunFilePath, getAppId } from "../../util/runFileReader"; + +const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); + +export type DaprLogType = 'app' | 'daprd'; + +export async function viewLogs(application: DaprApplication, type: DaprLogType): Promise { + if (!application.runTemplatePath) { + throw new Error(localize('commands.applications.viewLogs.noRunFile', 'Logs can be viewed only when applications are started via a run file.')); + } + + const runFile = await fromRunFilePath(vscode.Uri.file(application.runTemplatePath)); + + const runFileApplication = (runFile.apps ?? []).find(app => application.appId === getAppId(app)); + + if (!runFileApplication) { + throw new Error(localize('commands.applications.viewLogs.appNotFound', 'The application \'{0}\' was not found in the run file \'{1}\'.', application.appId, application.runTemplatePath)); + } + + if (!runFileApplication.appDirPath) { + throw new Error(localize('commands.applications.viewLogs.appDirNotFound', 'The directory for application \'{0}\' was not found in the run file \'{1}\'.', application.appId, application.runTemplatePath)); + } + + const runFileDirectory = path.dirname(application.runTemplatePath); + const appDirectory = path.join(runFileDirectory, runFileApplication.appDirPath, '.dapr', 'logs'); + + const pattern = `${application.appId}_${type}_*.log`; + const relativePattern = new vscode.RelativePattern(appDirectory, pattern); + + const files = await vscode.workspace.findFiles(relativePattern); + + if (files.length === 0) { + throw new Error(localize('commands.applications.viewLogs.logNotFound', 'No logs for application \'{0}\' were found.', application.appId)); + } + + const newestFile = files.reduce((newestFile, nextFile) => newestFile.fsPath.localeCompare(nextFile.fsPath) < 0 ? nextFile : newestFile); + + await vscode.window.showTextDocument(newestFile); +} diff --git a/src/debug/daprDebugConfigurationProvider.ts b/src/debug/daprDebugConfigurationProvider.ts index e55294a..6a34cbc 100644 --- a/src/debug/daprDebugConfigurationProvider.ts +++ b/src/debug/daprDebugConfigurationProvider.ts @@ -1,11 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as fs from 'fs/promises'; import * as nls from 'vscode-nls'; -import * as path from 'path'; import * as vscode from 'vscode'; -import { load } from 'js-yaml'; import { getLocalizationPathForFile } from '../util/localization'; import { DaprApplication, DaprApplicationProvider } from '../services/daprApplicationProvider'; import { filter, first, firstValueFrom, map, race, timeout } from 'rxjs'; @@ -13,6 +10,7 @@ import { debugApplication } from '../commands/applications/debugApplication'; import { UserInput } from '../services/userInput'; import { withAggregateTokens } from '../util/aggregateCancellationTokenSource'; import { fromCancellationToken } from '../util/observableCancellationToken'; +import { fromRunFilePath, getAppId } from '../util/runFileReader'; const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); @@ -22,36 +20,12 @@ export interface DaprDebugConfiguration extends vscode.DebugConfiguration { runFile: string; } -interface DaprRunApplication { - appDirPath?: string; - appID?: string; -} - -interface DaprRunFile { - apps?: DaprRunApplication[]; -} - -function getAppId(app: DaprRunApplication): string { - if (app.appID) { - return app.appID; - } - - if (app.appDirPath) { - return path.basename(app.appDirPath); - } - - throw new Error(localize('debug.daprDebugConfigurationProvider.unableToDetermineAppId', 'Unable to determine a configured application\'s ID.')); -} - async function getAppIdsToDebug(configuration: DaprDebugConfiguration): Promise> { if (configuration.includeApps) { return new Set(configuration.includeApps); } - const runFileContent = await fs.readFile(configuration.runFile, { encoding: 'utf8' }); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const runFile = load(runFileContent) as DaprRunFile; + const runFile = await fromRunFilePath(vscode.Uri.file(configuration.runFile)); const appIds = new Set(); diff --git a/src/extension.ts b/src/extension.ts index 261d0b1..910262e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -43,6 +43,8 @@ import { AsyncDisposable } from './util/asyncDisposable'; import createStartRunCommand from './commands/applications/startRun'; import createStopRunCommand from './commands/applications/stopRun'; import { DaprDebugConfigurationProvider } from './debug/daprDebugConfigurationProvider'; +import createViewAppLogsCommand from './commands/applications/viewAppLogs'; +import createViewDaprLogsCommand from './commands/applications/viewDaprLogs'; interface ExtensionPackage { engines: { [key: string]: string }; @@ -102,6 +104,8 @@ export function activate(context: vscode.ExtensionContext): Promise { telemetryProvider.registerCommandWithTelemetry('vscode-dapr.applications.publish-all-message', createPublishAllMessageCommand(daprApplicationProvider, daprClient, ext.outputChannel, ui, context.workspaceState)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.publish-message', createPublishMessageCommand(daprApplicationProvider, daprClient, ext.outputChannel, ui, context.workspaceState)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.stop-app', createStopCommand(daprCliClient, ui)); + telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.view-app-logs', createViewAppLogsCommand()); + telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.applications.view-dapr-logs', createViewDaprLogsCommand()); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.readDocumentation', createReadDocumentationCommand(ui)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.getStarted', createGetStartedCommand(ui)); telemetryProvider.registerContextCommandWithTelemetry('vscode-dapr.help.installDapr', createInstallDaprCommand(ui)); diff --git a/src/util/runFileReader.ts b/src/util/runFileReader.ts new file mode 100644 index 0000000..4839ee2 --- /dev/null +++ b/src/util/runFileReader.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as nls from 'vscode-nls'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { getLocalizationPathForFile } from '../util/localization'; +import { load } from "js-yaml"; + +const localize = nls.loadMessageBundle(getLocalizationPathForFile(__filename)); + +export interface DaprRunApplication { + appDirPath?: string; + appID?: string; +} + +export interface DaprRunFile { + apps?: DaprRunApplication[]; +} + +export async function fromRunFilePath(path: vscode.Uri): Promise{ + const runFileContent = await vscode.workspace.fs.readFile(path); + + if (!runFileContent) { + throw new Error(localize('util.runFileReader.noContent', 'There is no run file content at path: {0}', path.fsPath)); + } + + return fromRunFileContent(runFileContent.toString()); +} + +export function fromRunFileContent(content: string): DaprRunFile { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const runFile = load(content) as DaprRunFile; + + return runFile; +} + +export function getAppId(app: DaprRunApplication): string { + if (app.appID) { + return app.appID; + } + + if (app.appDirPath) { + return path.basename(app.appDirPath); + } + + throw new Error(localize('util.runFileReader.unableToDetermineAppId', 'Unable to determine a configured application\'s ID.')); +} diff --git a/src/views/applications/daprApplicationNode.ts b/src/views/applications/daprApplicationNode.ts index f500332..2827527 100644 --- a/src/views/applications/daprApplicationNode.ts +++ b/src/views/applications/daprApplicationNode.ts @@ -14,7 +14,11 @@ export default class DaprApplicationNode implements TreeNode { getTreeItem(): Promise { const item = new vscode.TreeItem(this.application.appId, vscode.TreeItemCollapsibleState.Collapsed); - item.contextValue = ['application', this.application.appPid !== undefined ? 'attachable' : ''].join(' '); + item.contextValue = [ + 'application', + this.application.appPid !== undefined ? 'attachable' : '', + this.application.runTemplatePath ? 'hasLogs' : '' + ].join(' '); item.iconPath = new vscode.ThemeIcon(this.application.appPid !== undefined ? 'server-process' : 'browser'); return Promise.resolve(item);