Skip to content

Commit e6dff84

Browse files
committed
Merge branch 'topic/vscode-logging' into 'master'
Set up logging in the VS Code extension See merge request eng/ide/ada_language_server!1285
2 parents 545f416 + 54312cd commit e6dff84

File tree

7 files changed

+615
-52
lines changed

7 files changed

+615
-52
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"script": "compile",
2525
"path": "integration/vscode/ada",
2626
"group": "build",
27-
"problemMatcher": [],
27+
"problemMatcher": ["$tsc"],
2828
"label": "npm: compile",
2929
"detail": "node ./node_modules/typescript/bin/tsc",
3030
"presentation": {

integration/vscode/ada/package-lock.json

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

integration/vscode/ada/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,10 +790,11 @@
790790
"dependencies": {
791791
"@types/command-exists": "1.2.0",
792792
"command-exists": "1.2.9",
793+
"fast-xml-parser": "4.2.5",
793794
"fp-ts": "2.12.0",
794795
"process": "0.11.10",
795796
"vscode-languageclient": "7.0.0",
796-
"ws": "8.13.0",
797-
"fast-xml-parser": "4.2.5"
797+
"winston": "3.10.0",
798+
"ws": "8.13.0"
798799
}
799800
}

integration/vscode/ada/src/clients.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
LanguageClientOptions,
88
ServerOptions,
99
} from 'vscode-languageclient/node';
10-
import { mainLogChannel } from './extension';
10+
import { logger } from './extension';
1111
import GnatTaskProvider from './gnatTaskProvider';
1212
import GprTaskProvider from './gprTaskProvider';
1313
import { logErrorAndThrow } from './helpers';
@@ -128,7 +128,7 @@ function createClient(
128128
logErrorAndThrow(
129129
`The Ada language server given in the ALS environment ` +
130130
`variable does not exist: ${serverExecPath}`,
131-
mainLogChannel
131+
logger
132132
);
133133
}
134134
} else {
@@ -138,11 +138,13 @@ function createClient(
138138
`language server for your architecture (${process.arch}) ` +
139139
`and platform (${process.platform}) ` +
140140
`at the expected location: ${serverExecPath}`,
141-
mainLogChannel
141+
logger
142142
);
143143
}
144144
}
145145

146+
logger.debug(`Using ALS at: ${serverExecPath}`);
147+
146148
// The debug options for the server
147149
// let debugOptions = { execArgv: [] };
148150
// If the extension is launched in debug mode then the debug server options are used

integration/vscode/ada/src/commands.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
22
import { SymbolKind } from 'vscode';
33
import { ContextClients } from './clients';
44
import { getOrAskForProgram } from './debugConfigProvider';
5-
import { mainLogChannel } from './extension';
5+
import { mainOutputChannel } from './extension';
66
import { getEnclosingSymbol } from './taskProviders';
77

88
export function registerCommands(context: vscode.ExtensionContext, clients: ContextClients) {
@@ -13,7 +13,7 @@ export function registerCommands(context: vscode.ExtensionContext, clients: Cont
1313
vscode.commands.registerCommand('ada.subprogramBox', addSupbrogramBoxCommand)
1414
);
1515
context.subscriptions.push(
16-
vscode.commands.registerCommand('ada.showExtensionOutput', () => mainLogChannel.show())
16+
vscode.commands.registerCommand('ada.showExtensionOutput', () => mainOutputChannel.show())
1717
);
1818
context.subscriptions.push(
1919
vscode.commands.registerCommand('ada.showAdaLSOutput', () =>

integration/vscode/ada/src/extension.ts

Lines changed: 141 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
import * as process from 'process';
2020
import * as vscode from 'vscode';
2121

22-
import { LanguageClient, Middleware } from 'vscode-languageclient/node';
22+
import { MESSAGE } from 'triple-beam';
23+
import { ExecuteCommandRequest, LanguageClient, Middleware } from 'vscode-languageclient/node';
24+
import winston, { format, transports } from 'winston';
25+
import Transport from 'winston-transport';
2326
import { ALSClientFeatures } from './alsClientFeatures';
2427
import { alsCommandExecutor } from './alsExecuteCommand';
2528
import { ContextClients } from './clients';
@@ -28,39 +31,116 @@ import { initializeDebugging } from './debugConfigProvider';
2831
import { initializeTestView } from './gnattest';
2932
import {
3033
assertSupportedEnvironments,
34+
getCustomEnvSettingName,
3135
getEvaluatedCustomEnv,
3236
setCustomEnvironment,
37+
startedInDebugMode,
3338
} from './helpers';
34-
import { ExecuteCommandRequest } from 'vscode-languageclient/node';
3539

3640
const ADA_CONTEXT = 'ADA_PROJECT_CONTEXT';
3741
export let contextClients: ContextClients;
38-
export let mainLogChannel: vscode.OutputChannel;
42+
43+
/**
44+
* The `vscode.OutputChannel` that hosts messages from the extension. This is
45+
* different from the channels associated with the language servers.
46+
*/
47+
export let mainOutputChannel: vscode.OutputChannel;
48+
49+
/**
50+
* A Winston logger object associated with the main output channel that allows
51+
* logging messages in a uniform format.
52+
*/
53+
export const logger: winston.Logger = winston.createLogger({
54+
format: format.combine(
55+
// Include a timestamp
56+
format.timestamp({
57+
format: 'YYYY-MM-DD HH:mm:ss.SSS',
58+
}),
59+
// Annotate log messages with a label
60+
format.label({ label: 'Ada Extension' }),
61+
// Pad message levels for alignment
62+
format.padLevels(),
63+
// Include a stack trace for logged Error objects
64+
format.errors({ stack: true }),
65+
// Perform printf-style %s,%d replacements
66+
format.splat()
67+
),
68+
});
69+
70+
/**
71+
* This is a custom Winston transport that forwards logged messages onto a given
72+
* `vscode.OutputChannel`.
73+
*/
74+
class VSCodeOutputChannelTransport extends Transport {
75+
outputChannel: vscode.OutputChannel;
76+
77+
constructor(channel: vscode.OutputChannel, opts?: Transport.TransportStreamOptions) {
78+
super(opts);
79+
this.outputChannel = channel;
80+
}
81+
82+
/**
83+
* This implementation is based on the Winston documentation for custom transports.
84+
*
85+
* @param info - the log entry info object
86+
* @param next - a callback
87+
*/
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89+
public log(info: any, next: () => void) {
90+
setImmediate(() => {
91+
this.emit('logged', info);
92+
});
93+
94+
/*
95+
* The formatted message is stored under the 'message' symbol in the info object.
96+
*/
97+
// eslint-disable-next-line max-len
98+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
99+
this.outputChannel.appendLine(info[MESSAGE]);
100+
101+
if (next) {
102+
next();
103+
}
104+
}
105+
}
39106

40107
export async function activate(context: vscode.ExtensionContext): Promise<void> {
41-
// Create an output channel for the extension. There are dedicated channels
42-
// for the Ada and Gpr language servers, and this one is a general channel
43-
// for non-LSP features of the extension.
44-
mainLogChannel = vscode.window.createOutputChannel('Ada Extension');
45-
mainLogChannel.appendLine('Starting Ada extension');
108+
setUpLogging();
109+
110+
logger.info('Starting Ada extension');
111+
112+
try {
113+
await activateExtension(context);
114+
} catch (error) {
115+
logger.error('Error while starting Ada extension. ', error);
116+
throw error;
117+
}
118+
119+
logger.info('Finished starting Ada extension');
120+
}
121+
122+
async function activateExtension(context: vscode.ExtensionContext) {
123+
assertSupportedEnvironments(logger);
46124

47125
// Log the environment that the extension (and all VS Code) will be using
48126
const customEnv = getEvaluatedCustomEnv();
49127

50128
if (customEnv && Object.keys(customEnv).length > 0) {
51-
mainLogChannel.appendLine('Setting environment variables:');
129+
logger.info('Setting environment variables:');
52130
for (const varName in customEnv) {
53131
const varValue: string = customEnv[varName];
54-
mainLogChannel.appendLine(`${varName}=${varValue}`);
132+
logger.info(`${varName}=${varValue}`);
55133
}
134+
} else {
135+
logger.debug('No custom environment variables set in %s', getCustomEnvSettingName());
56136
}
57137

58138
// Set the custom environment into the current node process. This must be
59139
// done before calling assertSupportedEnvironments in order to set the ALS
60140
// environment variable if provided.
61141
setCustomEnvironment();
62142

63-
assertSupportedEnvironments(mainLogChannel);
143+
assertSupportedEnvironments(logger);
64144

65145
// Create the Ada and GPR clients.
66146
contextClients = new ContextClients(context);
@@ -72,6 +152,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
72152
contextClients.adaClient.registerFeature(new ALSClientFeatures());
73153

74154
contextClients.start();
155+
75156
context.subscriptions.push(contextClients);
76157

77158
context.subscriptions.push(
@@ -91,8 +172,56 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
91172
initializeDebugging(context);
92173

93174
registerCommands(context, contextClients);
175+
}
176+
177+
function setUpLogging() {
178+
// Create an output channel for the extension. There are dedicated channels
179+
// for the Ada and Gpr language servers, and this one is a general channel
180+
// for non-LSP features of the extension.
181+
mainOutputChannel = vscode.window.createOutputChannel('Ada Extension');
94182

95-
mainLogChannel.appendLine('Started Ada extension');
183+
/*
184+
* This is a printing formatter that converts log entries to a string. It
185+
* used both for logging to the output channel and to the console.
186+
*/
187+
const printfFormatter = format.printf((info) => {
188+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
189+
return `${info.timestamp} [${info.label}] ${info.level.toUpperCase()} ${info.message} ${
190+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
191+
info.stack ?? ''
192+
}`;
193+
});
194+
195+
/*
196+
* Add a transport to direct log messages to the main
197+
* `vscode.OutputChannel`. Set the log level to 'info' to include 'error',
198+
* 'warn' and 'info'. See winston documentation for log levels:
199+
* https://github.com/winstonjs/winston#logging-levels
200+
*
201+
* TODO consider making the log level configurable through a verbosity
202+
* extension setting
203+
*/
204+
logger.add(
205+
new VSCodeOutputChannelTransport(mainOutputChannel, {
206+
format: printfFormatter,
207+
level: 'info',
208+
})
209+
);
210+
211+
if (startedInDebugMode()) {
212+
// In debug mode, print log messages to the console with colors. Use
213+
// level 'debug' for more verbosity.
214+
logger.add(
215+
new transports.Console({
216+
format: format.combine(
217+
printfFormatter,
218+
// Colorization must be applied after the finalizing printf formatter
219+
format.colorize({ all: true })
220+
),
221+
level: 'debug',
222+
})
223+
);
224+
}
96225
}
97226

98227
export async function deactivate() {

integration/vscode/ada/src/helpers.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import { platform } from 'os';
1919
import * as path from 'path';
2020
import * as vscode from 'vscode';
2121
import { ExecuteCommandRequest, LanguageClient } from 'vscode-languageclient/node';
22-
import { contextClients } from './extension';
22+
import winston from 'winston';
23+
import { contextClients, logger } from './extension';
2324

2425
/**
2526
* Substitue any variable reference present in the given string. VS Code
@@ -113,6 +114,16 @@ export function substituteVariables(str: string, recursive = false): string {
113114
*/
114115

115116
export function getCustomEnv() {
117+
const env_config_name = getCustomEnvSettingName();
118+
119+
const custom_env = vscode.workspace
120+
.getConfiguration()
121+
.get<{ [name: string]: string }>(env_config_name);
122+
123+
return custom_env;
124+
}
125+
126+
export function getCustomEnvSettingName() {
116127
const user_platform = platform();
117128
let env_config_name = 'terminal.integrated.env';
118129

@@ -126,12 +137,7 @@ export function getCustomEnv() {
126137
default:
127138
env_config_name += '.linux';
128139
}
129-
130-
const custom_env = vscode.workspace
131-
.getConfiguration()
132-
.get<{ [name: string]: string }>(env_config_name);
133-
134-
return custom_env;
140+
return env_config_name;
135141
}
136142

137143
export function getEvaluatedCustomEnv() {
@@ -169,7 +175,7 @@ export function setCustomEnvironment() {
169175
}
170176
}
171177

172-
export function assertSupportedEnvironments(mainChannel: vscode.OutputChannel) {
178+
export function assertSupportedEnvironments(mainChannel: winston.Logger) {
173179
if (process.env.ALS) {
174180
// The User provided an external ALS executable. Do not perform any
175181
// platform support checks because we may be on an unsupported platform
@@ -198,11 +204,15 @@ export function assertSupportedEnvironments(mainChannel: vscode.OutputChannel) {
198204
`architecture '${process.arch}' and platform '${process.platform}'`;
199205
logErrorAndThrow(msg, mainChannel);
200206
}
207+
208+
logger.debug(
209+
`Asserted compatibility with runtime environment: ${process.arch}, ${process.platform}`
210+
);
201211
}
202212

203-
export function logErrorAndThrow(msg: string, channel: vscode.OutputChannel) {
213+
export function logErrorAndThrow(msg: string, logger: winston.Logger) {
204214
void vscode.window.showErrorMessage(msg);
205-
channel.appendLine('[Error] ' + msg);
215+
logger.error(msg);
206216
throw new Error(msg);
207217
}
208218

@@ -306,3 +316,15 @@ export class AdaMain {
306316
return vscode.workspace.asRelativePath(this.execFullPath);
307317
}
308318
}
319+
320+
/**
321+
*
322+
* @returns true if the Node process was started with debug command line arguments
323+
*/
324+
export function startedInDebugMode() {
325+
const args = process.execArgv;
326+
if (args) {
327+
return args.some((arg) => /^--(debug(-brk)?|inspect-brk)=?/.test(arg));
328+
}
329+
return false;
330+
}

0 commit comments

Comments
 (0)