From 08208347a5a41b8f47751a268aafb85dd57d4b2e Mon Sep 17 00:00:00 2001 From: Shahar Soel <4233843+bd82@users.noreply.github.com> Date: Sun, 31 Jan 2021 13:49:29 +0200 Subject: [PATCH] feat: logger wrapper package (#163) --- .gitignore | 4 + examples/extension-wrapper/.gitignore | 2 + .../extension-wrapper/.vscode/launch.json | 17 ++ .../extension-wrapper/.vscode/settings.json | 3 + examples/extension-wrapper/.vscode/tasks.json | 20 +++ examples/extension-wrapper/README.md | 35 ++++ examples/extension-wrapper/package.json | 63 +++++++ examples/extension-wrapper/src/commands.ts | 20 +++ examples/extension-wrapper/src/extension.ts | 9 + examples/extension-wrapper/src/logger.ts | 56 ++++++ examples/extension-wrapper/tsconfig.json | 16 ++ package.json | 3 +- packages/wrapper/.npmignore | 13 ++ packages/wrapper/CHANGELOG.md | 0 packages/wrapper/CONTRIBUTING.md | 3 + packages/wrapper/README.md | 34 ++++ packages/wrapper/api.d.ts | 64 +++++++ packages/wrapper/package.json | 48 ++++++ packages/wrapper/src/api.ts | 2 + packages/wrapper/src/configure-logger.ts | 50 ++++++ packages/wrapper/src/helper-types.d.ts | 33 ++++ packages/wrapper/src/noop-logger.ts | 19 +++ .../wrapper/src/settings-changes-handler.ts | 46 +++++ packages/wrapper/src/settings.ts | 12 ++ .../wrapper/test/configure-logger-spec.ts | 81 +++++++++ packages/wrapper/test/noop-logger-spec.ts | 31 ++++ .../test/settings-changes-handler-spec.ts | 159 ++++++++++++++++++ packages/wrapper/test/settings-spec.ts | 51 ++++++ packages/wrapper/tsconfig.json | 16 ++ yarn.lock | 19 ++- 30 files changed, 927 insertions(+), 2 deletions(-) create mode 100644 examples/extension-wrapper/.gitignore create mode 100644 examples/extension-wrapper/.vscode/launch.json create mode 100644 examples/extension-wrapper/.vscode/settings.json create mode 100644 examples/extension-wrapper/.vscode/tasks.json create mode 100644 examples/extension-wrapper/README.md create mode 100644 examples/extension-wrapper/package.json create mode 100644 examples/extension-wrapper/src/commands.ts create mode 100644 examples/extension-wrapper/src/extension.ts create mode 100644 examples/extension-wrapper/src/logger.ts create mode 100644 examples/extension-wrapper/tsconfig.json create mode 100644 packages/wrapper/.npmignore create mode 100644 packages/wrapper/CHANGELOG.md create mode 100644 packages/wrapper/CONTRIBUTING.md create mode 100644 packages/wrapper/README.md create mode 100644 packages/wrapper/api.d.ts create mode 100644 packages/wrapper/package.json create mode 100644 packages/wrapper/src/api.ts create mode 100644 packages/wrapper/src/configure-logger.ts create mode 100644 packages/wrapper/src/helper-types.d.ts create mode 100644 packages/wrapper/src/noop-logger.ts create mode 100644 packages/wrapper/src/settings-changes-handler.ts create mode 100644 packages/wrapper/src/settings.ts create mode 100644 packages/wrapper/test/configure-logger-spec.ts create mode 100644 packages/wrapper/test/noop-logger-spec.ts create mode 100644 packages/wrapper/test/settings-changes-handler-spec.ts create mode 100644 packages/wrapper/test/settings-spec.ts create mode 100644 packages/wrapper/tsconfig.json diff --git a/.gitignore b/.gitignore index 43738e1..866e20a 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,7 @@ package-lock.json test/.log-out test/.log-out/* +# compiled sources +dist +tsconfig.tsbuildinfo + diff --git a/examples/extension-wrapper/.gitignore b/examples/extension-wrapper/.gitignore new file mode 100644 index 0000000..aeee732 --- /dev/null +++ b/examples/extension-wrapper/.gitignore @@ -0,0 +1,2 @@ +node_modules +*.vsix diff --git a/examples/extension-wrapper/.vscode/launch.json b/examples/extension-wrapper/.vscode/launch.json new file mode 100644 index 0000000..c3b3cbb --- /dev/null +++ b/examples/extension-wrapper/.vscode/launch.json @@ -0,0 +1,17 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/src/**/*.js"] + } + ] +} diff --git a/examples/extension-wrapper/.vscode/settings.json b/examples/extension-wrapper/.vscode/settings.json new file mode 100644 index 0000000..194c9b6 --- /dev/null +++ b/examples/extension-wrapper/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.insertSpaces": false +} diff --git a/examples/extension-wrapper/.vscode/tasks.json b/examples/extension-wrapper/.vscode/tasks.json new file mode 100644 index 0000000..078ff7e --- /dev/null +++ b/examples/extension-wrapper/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/examples/extension-wrapper/README.md b/examples/extension-wrapper/README.md new file mode 100644 index 0000000..c0f89ca --- /dev/null +++ b/examples/extension-wrapper/README.md @@ -0,0 +1,35 @@ +# @vscode-logging Extension-Wrapper Example + +This folder contains a sample VS code extension that demonstrates +how to integrate @vscode-logging/**wrapper** in a VSCode extension. + +## Running the Sample + +### Initial setup (once) + +- `yarn` in this repository's **root**. + +### Run/Debug the Example Extension in VSCode + +- Open this example folder in VSCode. +- Press F5 in VSCode to start the example Extension. + +### Basic logging flow + +- Activate the **Hello World Command**: `View Menu -> Command Palette -> Hello World` +- Open the **Output Panel**: `View Menu -> Output` +- Switch to the `logging-wrapper-example` output Channel using the dropdown. +- Inspect the output channel log and note that the location of the log **Files** was written. +- Inspect the contents of the log files using your favorite file system explorer. + +### Changing logging Configuration. + +- Open the **Settings Page**: `File -> Preferences -> Settings` +- Locate the **Example_Logging: Logging Level** configuration. +- Play with these settings and inspect how the logging output changes. + +## Points of Interest in this Example: + +- [extension.ts](./src/extension.ts) - `configureLogger` API invocation (inside `activate` function). +- [logger.ts](./src/logger.ts) - State management for the logger instance. +- [commands.ts](./src/commands.ts) - The implementation of the `Hello World` Command. diff --git a/examples/extension-wrapper/package.json b/examples/extension-wrapper/package.json new file mode 100644 index 0000000..1781165 --- /dev/null +++ b/examples/extension-wrapper/package.json @@ -0,0 +1,63 @@ +{ + "name": "vscode-logging-extension-wrapper-example", + "private": true, + "displayName": "logging-wrapper-example", + "description": "Example using @vscode-logging/logger and @vscode-logging/wrapper in a VSCode Extension", + "version": "0.1.0", + "publisher": "SAP", + "engines": { + "vscode": "^1.52.0" + }, + "activationEvents": [ + "onCommand:extension.helloWorld" + ], + "main": "./dist/src/extension.js", + "scripts": { + "ci": "npm-run-all clean compile", + "clean": "rimraf dist", + "compile": "tsc" + }, + "contributes": { + "commands": [ + { + "command": "extension.helloWorld", + "title": "Hello World" + } + ], + "configuration": { + "type": "object", + "title": "Example_Logging", + "properties": { + "Example_Logging.loggingLevel": { + "type": "string", + "enum": [ + "off", + "fatal", + "error", + "warn", + "info", + "debug", + "trace" + ], + "default": "error", + "description": "The verbosity of logging. The Order is None < fatal < error < warn < info < debug < trace.", + "scope": "window" + }, + "Example_Logging.sourceLocationTracking": { + "type": "boolean", + "default": false, + "description": "Should Source Code Location Info be added to log entries, DANGER - May be very slow, only use in debugging scenarios", + "scope": "window" + } + } + } + }, + "dependencies": { + "@vscode-logging/wrapper": "^0.1.0" + }, + "devDependencies": { + "@types/vscode": "^1.52.0", + "@vscode-logging/types": "^0.1.3" + }, + "peerDependencies": {} +} diff --git a/examples/extension-wrapper/src/commands.ts b/examples/extension-wrapper/src/commands.ts new file mode 100644 index 0000000..ddfbca5 --- /dev/null +++ b/examples/extension-wrapper/src/commands.ts @@ -0,0 +1,20 @@ +import { ExtensionContext, commands, window } from "vscode"; +import { getLogger } from "./logger"; + +export function registerCommands( + subscriptions: ExtensionContext["subscriptions"] +): void { + let logMessagesCounter = 1; + const commandSubscription = commands.registerCommand( + "extension.helloWorld", + // By providing a real function name rather then a `fat arrow` ( ()=>{} ) + // The `sourceLocationTracking` can provide a meaningful output. + function registerCallback() { + window.showInformationMessage("Hello Cruel World!"); + getLogger().error( + `Hip Hip Hurray, the Command was executed! counter: <${logMessagesCounter++}>` + ); + } + ); + subscriptions.push(commandSubscription); +} diff --git a/examples/extension-wrapper/src/extension.ts b/examples/extension-wrapper/src/extension.ts new file mode 100644 index 0000000..119d7b2 --- /dev/null +++ b/examples/extension-wrapper/src/extension.ts @@ -0,0 +1,9 @@ +import { ExtensionContext } from "vscode"; +import { getLogger, initLogger } from "./logger"; +import { registerCommands } from "./commands"; + +export async function activate(context: ExtensionContext): Promise { + await initLogger(context); + registerCommands(context.subscriptions); + getLogger().info("extension is active, hip hip hurray!"); +} diff --git a/examples/extension-wrapper/src/logger.ts b/examples/extension-wrapper/src/logger.ts new file mode 100644 index 0000000..75bee38 --- /dev/null +++ b/examples/extension-wrapper/src/logger.ts @@ -0,0 +1,56 @@ +/** + * This file manages the logger's state. + */ +import { readFile as readFileCallback } from "fs"; +import { promisify } from "util"; +import { resolve } from "path"; +import { ok } from "assert"; +import { ExtensionContext, window, workspace } from "vscode"; +import { IChildLogger, IVSCodeExtLogger } from "@vscode-logging/types"; +import { configureLogger, NOOP_LOGGER } from "@vscode-logging/wrapper"; + +const readFile = promisify(readFileCallback); + +// On file load we initialize our logger to `NOOP_LOGGER` +// this is done because the "real" logger cannot be initialized during file load. +// only once the `activate` function has been called in extension.ts +// as the `ExtensionContext` argument to `activate` contains the required `logPath` +let loggerImpel: IVSCodeExtLogger = NOOP_LOGGER; + +export function getLogger(): IChildLogger { + return loggerImpel; +} + +function setLogger(newLogger: IVSCodeExtLogger): void { + loggerImpel = newLogger; +} + +const LOGGING_LEVEL_PROP = "Example_Logging.loggingLevel"; +const SOURCE_LOCATION_PROP = "Example_Logging.sourceLocationTracking"; + +export async function initLogger(context: ExtensionContext): Promise { + const meta = JSON.parse( + await readFile(resolve(context.extensionPath, "package.json"), "utf8") + ); + + // By asserting the existence of the properties in the package.json + // at runtime, we avoid many copy-pasta mistakes... + const configProps = meta?.contributes?.configuration?.properties; + ok(configProps?.[LOGGING_LEVEL_PROP]); + ok(configProps?.[SOURCE_LOCATION_PROP]); + + const extLogger = configureLogger({ + extName: meta.displayName, + logPath: context.logPath, + logOutputChannel: window.createOutputChannel(meta.displayName), + // set to `true` if you also want your VSCode extension to log to the console. + logConsole: false, + loggingLevelProp: LOGGING_LEVEL_PROP, + sourceLocationProp: SOURCE_LOCATION_PROP, + subscriptions: context.subscriptions, + onDidChangeConfiguration: workspace.onDidChangeConfiguration, + getConfiguration: workspace.getConfiguration + }); + + setLogger(extLogger); +} diff --git a/examples/extension-wrapper/tsconfig.json b/examples/extension-wrapper/tsconfig.json new file mode 100644 index 0000000..c26e211 --- /dev/null +++ b/examples/extension-wrapper/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "baseUrl": ".", + "module": "commonjs", + "lib": ["es7"], + "target": "es2017", + "declaration": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true, + "strict": true + }, + "include": ["./src/**/*.ts", "./api.d.ts"] +} diff --git a/package.json b/package.json index c98da57..91744a9 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "nyc": "15.0.0", "prettier": "1.19.1", "shx": "0.3.2", - "typescript": "4.1.2" + "typescript": "4.1.2", + "rimraf": "^3.0.2" }, "husky": { "hooks": { diff --git a/packages/wrapper/.npmignore b/packages/wrapper/.npmignore new file mode 100644 index 0000000..f95fa3c --- /dev/null +++ b/packages/wrapper/.npmignore @@ -0,0 +1,13 @@ +# TypeScript +tsconfig.json + +# NYC +.nyc_output +nyc.config.js +coverage + +# tests +test + +# Dev Docs +CONTRIBUTING.md diff --git a/packages/wrapper/CHANGELOG.md b/packages/wrapper/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/packages/wrapper/CONTRIBUTING.md b/packages/wrapper/CONTRIBUTING.md new file mode 100644 index 0000000..c4b073a --- /dev/null +++ b/packages/wrapper/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contribution Guide + +Please see the top level [Contribution Guide](../../CONTRIBUTING.md). diff --git a/packages/wrapper/README.md b/packages/wrapper/README.md new file mode 100644 index 0000000..faea1ee --- /dev/null +++ b/packages/wrapper/README.md @@ -0,0 +1,34 @@ +# @vscode-logging/wrapper + +A **optional** and **opinionated** wrapper utilities for @vscode-logging/logger. + +## Installation + +With npm: + +- `npm install @vscode-logging/wrapper --save` + +With Yarn: + +- `yarn add @vscode-logging/wrapper` + +## Usage + +Please see the [TypeScript Definitions](./api.d.ts) for full API details. + +## Example + +An example VSCode extension is available in the [examples' folder](https://github.com/SAP/vscode-logging/tree/master/examples/extension-wrapper). + +## Support + +Please open [issues](https://github.com/SAP/vscode-logging/issues) on github. + +## Contributing + +See [CONTRIBUTING.md](./CONTRIBUTING.md). + +## License + +Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. +This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the [LICENSE file](../../LICENSE). diff --git a/packages/wrapper/api.d.ts b/packages/wrapper/api.d.ts new file mode 100644 index 0000000..7f440bc --- /dev/null +++ b/packages/wrapper/api.d.ts @@ -0,0 +1,64 @@ +import { workspace, ExtensionContext } from "vscode"; +import { IVSCodeExtLogger } from "@vscode-logging/types"; +import { BasicOutputChannel } from "@vscode-logging/logger"; + +/** + * An "empty" Logger implementation. + * This value should be used to initialize an extension's logger + * before the `activate` function has been called. + */ +export declare const NOOP_LOGGER: IVSCodeExtLogger; + +export interface ConfigureLoggerOpts { + /** + * @see {GetExtensionLoggerOpts.extName} + */ + extName: string; + /** + * @see {GetExtensionLoggerOpts.logPath} + */ + logPath: string; + loggingLevelProp: string; + sourceLocationProp: string; + /** + * @see {GetExtensionLoggerOpts.logOutputChannel} + */ + logOutputChannel?: BasicOutputChannel; + /** + * @see {GetExtensionLoggerOpts.logConsole} + */ + logConsole?: boolean; + /** + * The vscode's extension subscriptions + * This is normally available via the `activate` function's `context` + * parameter. + */ + subscriptions: ExtensionContext["subscriptions"]; + /** + * The `vscode.workspace.getConfiguration` method. + * Note this is the method itself, not the returned value from executing it. + */ + getConfiguration: typeof workspace.getConfiguration; + /** + * The `vscode.workspace.onDidChangeConfiguration` method. + * Note this is the method itself, not the returned value from executing it. + */ + onDidChangeConfiguration: typeof workspace.onDidChangeConfiguration; +} + +/** + * The main opinionated wrapper utility. + * This will do the following: + * + * 1. Initialize an @vscode-logging/logger instance. + * 2. Log the initial file `logPath` and `logLevel`. + * 3. Register a listener for VSCode workspace configuration changes + * for the `logLevel` and `sourceLocationTracking` + * + * - Note that this utility is slightly opinionated and has more more restrictive + * than the @vscode-logging/logger APIs, e.g: + * 1. `logPath` param is mandatory. + * 2. default logLevel (if none is found in the workspace configuration) is `error`. + * 3. ... + */ +export function configureLogger(opts: ConfigureLoggerOpts): IVSCodeExtLogger; diff --git a/packages/wrapper/package.json b/packages/wrapper/package.json new file mode 100644 index 0000000..1415150 --- /dev/null +++ b/packages/wrapper/package.json @@ -0,0 +1,48 @@ +{ + "name": "@vscode-logging/wrapper", + "version": "0.1.0", + "description": "opinionated DI based wrapper for @vscode-logging/logger", + "keywords": [ + "vscode", + "logger" + ], + "files": [ + ".reuse", + "LICENSES", + "dist/src", + "api.d.ts" + ], + "main": "dist/src/api.js", + "repository": "https://github.com/sap/vscode-logging/", + "license": "Apache-2.0", + "typings": "./api.d.ts", + "dependencies": { + "@vscode-logging/types": "^0.1.3", + "@vscode-logging/logger": "^1.2.1", + "@types/vscode": "1.52.0" + }, + "devDependencies": { + "@types/lodash": "^4.14.167", + "lodash": "^4.17.20" + }, + "scripts": { + "ci": "npm-run-all clean compile coverage:*", + "clean": "rimraf ./dist ./coverage", + "compile": "tsc", + "test": "mocha \"./dist/test/**/*spec.js\"", + "coverage:run": "nyc mocha \"./dist/test/**/*spec.js\"", + "coverage:check": "nyc check-coverage --lines 100 --branches 100 --statements 100 --functions 100" + }, + "publishConfig": { + "access": "public" + }, + "nyc": { + "include": [ + "dist/src/**/*.js" + ], + "reporter": [ + "text", + "lcov" + ] + } +} diff --git a/packages/wrapper/src/api.ts b/packages/wrapper/src/api.ts new file mode 100644 index 0000000..f311f8b --- /dev/null +++ b/packages/wrapper/src/api.ts @@ -0,0 +1,2 @@ +export { configureLogger } from "./configure-logger"; +export { NOOP_LOGGER } from "./noop-logger"; diff --git a/packages/wrapper/src/configure-logger.ts b/packages/wrapper/src/configure-logger.ts new file mode 100644 index 0000000..4158220 --- /dev/null +++ b/packages/wrapper/src/configure-logger.ts @@ -0,0 +1,50 @@ +import { getExtensionLogger } from "@vscode-logging/logger"; +import { IVSCodeExtLogger } from "@vscode-logging/types"; +import { ConfigureLoggerOpts } from "../api"; +import { + getLoggingLevelSetting, + getSourceLocationTrackingSetting +} from "./settings"; +import { + listenToLogSettingsChanges, + logLoggerDetails +} from "./settings-changes-handler"; + +export function configureLogger(opts: ConfigureLoggerOpts): IVSCodeExtLogger { + const logLevelSetting = getLoggingLevelSetting({ + getConfiguration: opts.getConfiguration, + loggingLevelProp: opts.loggingLevelProp + }); + + const sourceLocationTrackingSettings = getSourceLocationTrackingSetting({ + getConfiguration: opts.getConfiguration, + sourceLocationProp: opts.sourceLocationProp + }); + + const extensionLogger = getExtensionLogger({ + extName: opts.extName, + level: logLevelSetting, + logPath: opts.logPath, + sourceLocationTracking: sourceLocationTrackingSettings, + logConsole: opts.logConsole, + logOutputChannel: opts.logOutputChannel + }); + + logLoggerDetails({ + logger: extensionLogger, + logPath: opts.logPath, + logLevel: logLevelSetting + }); + + listenToLogSettingsChanges({ + subscriptions: opts.subscriptions, + onDidChangeConfiguration: opts.onDidChangeConfiguration, + getConfiguration: opts.getConfiguration, + loggingLevelProp: opts.loggingLevelProp, + sourceLocationProp: opts.sourceLocationProp, + logger: extensionLogger, + logPath: opts.logPath + }); + + return extensionLogger; +} diff --git a/packages/wrapper/src/helper-types.d.ts b/packages/wrapper/src/helper-types.d.ts new file mode 100644 index 0000000..7efb0b2 --- /dev/null +++ b/packages/wrapper/src/helper-types.d.ts @@ -0,0 +1,33 @@ +import { + IChildLogger, + IVSCodeExtLogger, + LogLevel +} from "@vscode-logging/types"; +// importing "vscode" in a **d.ts** files avoids a actual runtime dependency to vscode. +import { ExtensionContext, workspace } from "vscode"; + +export interface GetLoggingLevelOpts { + getConfiguration: typeof workspace.getConfiguration; + loggingLevelProp: string; +} + +export interface GetSourceLocationOpts { + getConfiguration: typeof workspace.getConfiguration; + sourceLocationProp: string; +} + +export interface ListenToLogSettingsOpts { + subscriptions: ExtensionContext["subscriptions"]; + onDidChangeConfiguration: typeof workspace.onDidChangeConfiguration; + getConfiguration: typeof workspace.getConfiguration; + loggingLevelProp: string; + sourceLocationProp: string; + logger: IVSCodeExtLogger; + logPath: string; +} + +export interface LogLoggerDetailsOpts { + logger: IChildLogger; + logPath: string; + logLevel: LogLevel; +} diff --git a/packages/wrapper/src/noop-logger.ts b/packages/wrapper/src/noop-logger.ts new file mode 100644 index 0000000..b9e7cc3 --- /dev/null +++ b/packages/wrapper/src/noop-logger.ts @@ -0,0 +1,19 @@ +import { IVSCodeExtLogger, IChildLogger } from "@vscode-logging/types"; + +export function noop() {} + +export const NOOP_LOGGER: IVSCodeExtLogger = { + changeLevel: noop, + changeSourceLocationTracking: noop, + debug: noop, + error: noop, + fatal: noop, + getChildLogger(opts: { label: string }): IChildLogger { + return this; + }, + info: noop, + trace: noop, + warn: noop +}; + +Object.freeze(NOOP_LOGGER); diff --git a/packages/wrapper/src/settings-changes-handler.ts b/packages/wrapper/src/settings-changes-handler.ts new file mode 100644 index 0000000..035683d --- /dev/null +++ b/packages/wrapper/src/settings-changes-handler.ts @@ -0,0 +1,46 @@ +import { LogLevel } from "@vscode-logging/types"; +import { ListenToLogSettingsOpts, LogLoggerDetailsOpts } from "./helper-types"; +import { + getLoggingLevelSetting, + getSourceLocationTrackingSetting +} from "./settings"; + +export function logLoggerDetails(opts: LogLoggerDetailsOpts): void { + opts.logger.info(`Start Logging in Log Level: <${opts.logLevel}>`); + opts.logger.info(`Full Logs can be found in the <${opts.logPath}> folder.`); +} + +export function listenToLogSettingsChanges( + opts: ListenToLogSettingsOpts +): void { + opts.subscriptions.push( + opts.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(opts.loggingLevelProp)) { + const logLevel: LogLevel = getLoggingLevelSetting({ + loggingLevelProp: opts.loggingLevelProp, + getConfiguration: opts.getConfiguration + }); + opts.logger.changeLevel(logLevel); + logLoggerDetails({ + logger: opts.logger, + logPath: opts.logPath, + logLevel + }); + } + }) + ); + + opts.subscriptions.push( + opts.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(opts.sourceLocationProp)) { + const newSourceLocationTracking: boolean = getSourceLocationTrackingSetting( + { + sourceLocationProp: opts.sourceLocationProp, + getConfiguration: opts.getConfiguration + } + ); + opts.logger.changeSourceLocationTracking(newSourceLocationTracking); + } + }) + ); +} diff --git a/packages/wrapper/src/settings.ts b/packages/wrapper/src/settings.ts new file mode 100644 index 0000000..23496fe --- /dev/null +++ b/packages/wrapper/src/settings.ts @@ -0,0 +1,12 @@ +import { LogLevel } from "@vscode-logging/logger"; +import { GetLoggingLevelOpts, GetSourceLocationOpts } from "./helper-types"; + +export function getLoggingLevelSetting(opts: GetLoggingLevelOpts): LogLevel { + return opts.getConfiguration().get(opts.loggingLevelProp, "error"); +} + +export function getSourceLocationTrackingSetting( + opts: GetSourceLocationOpts +): boolean { + return opts.getConfiguration().get(opts.sourceLocationProp, false); +} diff --git a/packages/wrapper/test/configure-logger-spec.ts b/packages/wrapper/test/configure-logger-spec.ts new file mode 100644 index 0000000..2abbde5 --- /dev/null +++ b/packages/wrapper/test/configure-logger-spec.ts @@ -0,0 +1,81 @@ +import { expect } from "chai"; +import { ExtensionContext, WorkspaceConfiguration } from "vscode"; +import { IChildLogger, LogLevel } from "@vscode-logging/types"; +import { BasicOutputChannel } from "@vscode-logging/logger"; +import { configureLogger } from "../src/api"; +import { ConfigureLoggerOpts } from "../api"; + +describe("The `configureLogger` main wrapper utility function", () => { + let outputChannelMock: BasicOutputChannel; + let subscriptions: ExtensionContext["subscriptions"]; + let onDidChangeConfiguration: ConfigureLoggerOpts["onDidChangeConfiguration"]; + let loggingLevelProp: string; + let sourceLocationProp: string; + let configLogLevel: LogLevel; + let getConfiguration: ConfigureLoggerOpts["getConfiguration"]; + let loggedLines: Record[]; + + beforeEach(() => { + loggedLines = []; + subscriptions = []; + }); + + before(() => { + outputChannelMock = { + appendLine(value: string): void { + loggedLines.push(JSON.parse(value)); + }, + dispose(): void {}, + show(): void {} + }; + + // we are not testing configuration changes in this spec, these will be tested + // in `settings-changes-handler-spec` + onDidChangeConfiguration = () => { + return { dispose() {} }; + }; + + loggingLevelProp = "my_vscode_ext.loggingLevel"; + sourceLocationProp = "my_vscode_ext.sourceLocationTracking"; + configLogLevel = "debug"; + const settingsMap = new Map([ + [loggingLevelProp, configLogLevel], + [sourceLocationProp, false] + ]); + // don't worry be happy (or unknown)... + getConfiguration = _ => (settingsMap as unknown) as WorkspaceConfiguration; + }); + + function configureLoggerHelper(): IChildLogger { + return configureLogger({ + extName: "my_vscode_ext", + // only logging "in memory" during our test. + logPath: (undefined as unknown) as string, + loggingLevelProp: "my_vscode_ext.loggingLevel", + sourceLocationProp: "my_vscode_ext.sourceLocationTracking", + logOutputChannel: outputChannelMock, + subscriptions, + getConfiguration, + onDidChangeConfiguration + }); + } + + it("will log initial details on configure", () => { + expect(loggedLines).to.be.empty; + configureLoggerHelper(); + expect(loggedLines).to.have.lengthOf(2); + const loggerDetailsEntry = loggedLines[0]; + expect(loggerDetailsEntry.message).to.include(configLogLevel); + }); + + it("will return a VSCodeExtLogger that can be used for 'regular logging' e.g log/info/debug", () => { + expect(loggedLines).to.be.empty; + const testLogger = configureLoggerHelper(); + loggedLines = []; + testLogger.debug("hello world", { param1: 666 }); + expect(loggedLines).to.have.lengthOf(1); + const loggerEntry = loggedLines[0]; + expect(loggerEntry.message).to.equal("hello world"); + expect(loggerEntry.param1).to.equal(666); + }); +}); diff --git a/packages/wrapper/test/noop-logger-spec.ts b/packages/wrapper/test/noop-logger-spec.ts new file mode 100644 index 0000000..612b193 --- /dev/null +++ b/packages/wrapper/test/noop-logger-spec.ts @@ -0,0 +1,31 @@ +import { expect } from "chai"; +import { keys, difference, forEach } from "lodash"; +import { noop, NOOP_LOGGER } from "../src/noop-logger"; + +describe("The no-operation logger", () => { + const noopFuncProps = difference(keys(NOOP_LOGGER), ["getChildLogger"]); + + forEach(noopFuncProps, funcPropName => { + it(`exposes a no operation method for <${funcPropName}>`, () => { + // @ts-expect-error -- runtime reflection + expect(NOOP_LOGGER[funcPropName]).to.eql(noop); + }); + }); + + it("uses an **empty** method for the noop implementation", () => { + const noopSource = noop.toString(); + // + // The `cov_...` section in optional capturing group handles the case of execution under nyc wrapper + // - instrumented code, in which case `toString` would return something like: + // ``` + // function noop(){cov_1w547p8w8s().f[0]++;}' + // ``` + expect(noopSource).to.match( + /function\s*\w+\s*\(\s*\)\s*{\s*(cov_\w+\(\)\.f\[\d]\+\+;)?}/ + ); + }); + + it("implements by returning 'itself'", () => { + expect(NOOP_LOGGER.getChildLogger({ label: "foo" })).to.equal(NOOP_LOGGER); + }); +}); diff --git a/packages/wrapper/test/settings-changes-handler-spec.ts b/packages/wrapper/test/settings-changes-handler-spec.ts new file mode 100644 index 0000000..67e1043 --- /dev/null +++ b/packages/wrapper/test/settings-changes-handler-spec.ts @@ -0,0 +1,159 @@ +import { expect } from "chai"; +import { + ConfigurationChangeEvent, + ConfigurationScope, + Disposable, + ExtensionContext, + WorkspaceConfiguration +} from "vscode"; +import { LogLevel } from "@vscode-logging/logger"; +import { IChildLogger, IVSCodeExtLogger } from "@vscode-logging/types"; +import { ConfigureLoggerOpts } from "../api"; +import { listenToLogSettingsChanges } from "../src/settings-changes-handler"; + +describe("The `listenToLogSettingsChanges` utility function", () => { + let logger: IVSCodeExtLogger; + let loggingLevelProp: string; + let sourceLocationProp: string; + let logPath: string; + let getConfiguration: ConfigureLoggerOpts["getConfiguration"]; + let onDidChangeConfiguration: ConfigureLoggerOpts["onDidChangeConfiguration"]; + let subscriptions: ExtensionContext["subscriptions"]; + let currentLogLevel: LogLevel; + let currentSourceLocationTracking: boolean; + + beforeEach(() => { + currentLogLevel = "error"; + currentSourceLocationTracking = false; + }); + + before(() => { + logPath = "c:\\logs"; + + logger = { + changeLevel(newLevel: LogLevel) { + currentLogLevel = newLevel; + }, + changeSourceLocationTracking(newSourceLocation: boolean) { + currentSourceLocationTracking = newSourceLocation; + }, + getChildLogger(): IChildLogger { + return this; + }, + fatal(): void {}, + error(): void {}, + warn(): void {}, + info(): void {}, + debug(): void {}, + trace(): void {} + }; + + loggingLevelProp = "my_vscode_ext.loggingLevel"; + sourceLocationProp = "my_vscode_ext.sourceLocationTracking"; + subscriptions = []; + }); + + context("affects Configuration", () => { + before(() => { + const settingsMap = new Map([ + [loggingLevelProp, "info"], + [sourceLocationProp, true] + ]); + // don't worry be happy (or unknown)... + getConfiguration = _ => + (settingsMap as unknown) as WorkspaceConfiguration; + + onDidChangeConfiguration = function( + cb: (e: ConfigurationChangeEvent) => void + ): Disposable { + const e: ConfigurationChangeEvent = { + affectsConfiguration(): boolean { + return true; + } + }; + cb(e); + return { dispose(): any {} }; + }; + }); + + it("will listen to `logLevel` changes", () => { + expect(currentLogLevel).to.equal("error"); + listenToLogSettingsChanges({ + sourceLocationProp, + loggingLevelProp, + getConfiguration, + logger, + logPath, + onDidChangeConfiguration, + subscriptions + }); + expect(currentLogLevel).to.equal("info"); + }); + + it("will listen to `sourceLocationTracking` changes", () => { + expect(currentSourceLocationTracking).to.be.false; + listenToLogSettingsChanges({ + sourceLocationProp, + loggingLevelProp, + getConfiguration, + logger, + logPath, + onDidChangeConfiguration, + subscriptions + }); + expect(currentSourceLocationTracking).to.be.true; + }); + }); + + context("does **not** affects Configuration", () => { + before(() => { + const settingsMap = new Map([ + ["someOtherProp", "blue"], + ["someOtherProp2", 666] + ]); + // don't worry be happy (or unknown)... + getConfiguration = _ => + (settingsMap as unknown) as WorkspaceConfiguration; + + onDidChangeConfiguration = function( + cb: (e: ConfigurationChangeEvent) => void + ): Disposable { + const e: ConfigurationChangeEvent = { + affectsConfiguration(): boolean { + return false; + } + }; + cb(e); + return { dispose(): any {} }; + }; + }); + + it("will **not** listen to unrelated configuration changes - logLevel", () => { + expect(currentLogLevel).to.equal("error"); + listenToLogSettingsChanges({ + sourceLocationProp, + loggingLevelProp, + getConfiguration, + logger, + logPath, + onDidChangeConfiguration, + subscriptions + }); + expect(currentLogLevel).to.equal("error"); + }); + + it("will **not** listen to unrelated configuration changes - sourceLocationTracking", () => { + expect(currentSourceLocationTracking).to.be.false; + listenToLogSettingsChanges({ + sourceLocationProp, + loggingLevelProp, + getConfiguration, + logger, + logPath, + onDidChangeConfiguration, + subscriptions + }); + expect(currentSourceLocationTracking).to.be.false; + }); + }); +}); diff --git a/packages/wrapper/test/settings-spec.ts b/packages/wrapper/test/settings-spec.ts new file mode 100644 index 0000000..aa4b541 --- /dev/null +++ b/packages/wrapper/test/settings-spec.ts @@ -0,0 +1,51 @@ +import { expect } from "chai"; +import { + getLoggingLevelSetting, + getSourceLocationTrackingSetting +} from "../src/settings"; +import { GetLoggingLevelOpts } from "../src/helper-types"; +import { WorkspaceConfiguration } from "vscode"; + +describe("The settings related utilities", () => { + describe("The function", () => { + let getConfiguration: GetLoggingLevelOpts["getConfiguration"]; + + before(() => { + const settingsMap = new Map([ + ["ext1.loggingLevel", "debug"], + ["otherExt.loggingLevel", "info"] + ]); + getConfiguration = _ => + (settingsMap as unknown) as WorkspaceConfiguration; + }); + + it("Can retrieve the logging level value from a configuration", () => { + const logLevel = getLoggingLevelSetting({ + getConfiguration, + loggingLevelProp: "ext1.loggingLevel" + }); + expect(logLevel).to.equal("debug"); + }); + }); + + describe("The function", () => { + let getConfiguration: GetLoggingLevelOpts["getConfiguration"]; + + before(() => { + const settingsMap = new Map([ + ["ext1.sourceLocationTracking", true], + ["otherExt.sourceLocationTracking", false] + ]); + getConfiguration = _ => + (settingsMap as unknown) as WorkspaceConfiguration; + }); + + it("Can retrieve the source location tracking value from a configuration", () => { + const sourceLocationEnabled = getSourceLocationTrackingSetting({ + getConfiguration, + sourceLocationProp: "otherExt.sourceLocationTracking" + }); + expect(sourceLocationEnabled).to.be.false; + }); + }); +}); diff --git a/packages/wrapper/tsconfig.json b/packages/wrapper/tsconfig.json new file mode 100644 index 0000000..9cfc695 --- /dev/null +++ b/packages/wrapper/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "rootDir": ".", + "outDir": "dist", + "baseUrl": ".", + "module": "commonjs", + "lib": ["es7"], + "target": "es2017", + "declaration": true, + "sourceMap": true, + "forceConsistentCasingInFileNames": true, + "strict": true + }, + "include": ["./src/**/*.ts", "./test/**/*.ts", "./api.d.ts"] +} diff --git a/yarn.lock b/yarn.lock index 5476396..ff9b0f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1182,6 +1182,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/lodash@^4.14.167": + version "4.14.167" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.167.tgz#ce7d78553e3c886d4ea643c37ec7edc20f16765e" + integrity sha512-w7tQPjARrvdeBkX/Rwg95S592JwxqOjmms3zWQ0XZgSyxSLdzWaYH3vErBhdVS/lRBX7F8aBYcYJYTr5TMGOzw== + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -1212,6 +1217,11 @@ resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.51.0.tgz#f2e324d8385db95030b1454fd1f043618e08097d" integrity sha512-C/jZ35OT5k/rsJyAK8mS1kM++vMcm89oSWegkzxRCvHllIq0cToZAkIDs6eCY4SKrvik3nrhELizyLcM0onbQA== +"@types/vscode@1.52.0", "@types/vscode@^1.52.0": + version "1.52.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.52.0.tgz#61917968dd403932127fc4004a21fd8d69e4f61c" + integrity sha512-Kt3bvWzAvvF/WH9YEcrCICDp0Z7aHhJGhLJ1BxeyNP6yRjonWqWnAIh35/pXAjswAnWOABrYlF7SwXR9+1nnLA== + "@types/vscode@^1.32.0": version "1.41.0" resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.41.0.tgz#b0d75920220f84e07093285e59180c0f11d336cd" @@ -4270,7 +4280,7 @@ lodash@4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@4.17.20, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.2.1: +lodash@4.17.20, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.2.1: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -5859,6 +5869,13 @@ rimraf@^3.0.0: dependencies: glob "^7.1.3" +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"