Improve Logger + add support for PII Logging (#17549)
This commit is contained in:
Родитель
d66ac99e51
Коммит
c323c5e65e
|
@ -125,6 +125,9 @@
|
|||
<trans-unit id="azureAuthTypeDeviceCode">
|
||||
<source xml:lang="en">Azure Device Code</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="azureLogChannelName">
|
||||
<source xml:lang="en">Azure Logs</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="encryptPrompt">
|
||||
<source xml:lang="en">Encrypt</source>
|
||||
</trans-unit>
|
||||
|
|
|
@ -1022,6 +1022,11 @@
|
|||
"Verbose"
|
||||
]
|
||||
},
|
||||
"mssql.piiLogging": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%mssql.piiLogging%"
|
||||
},
|
||||
"mssql.logRetentionMinutes": {
|
||||
"type": "number",
|
||||
"default": 10080,
|
||||
|
|
|
@ -99,6 +99,7 @@
|
|||
"mssql.createAzureFunction":"Create Azure Function with SQL binding",
|
||||
"mssql.query.maxXmlCharsToStore":"Number of XML characters to store after running a query",
|
||||
"mssql.tracingLevel":"[Optional] Log level for backend services. Azure Data Studio generates a file name every time it starts and if the file already exists the logs entries are appended to that file. For cleanup of old log files see logRetentionMinutes and logFilesRemovalLimit settings. The default tracingLevel does not log much. Changing verbosity could lead to extensive logging and disk space requirements for the logs. Error includes Critical, Warning includes Error, Information includes Warning and Verbose includes Information",
|
||||
"mssql.piiLogging": "Should Personally Identifiable Information (PII) be logged in the Azure Logs output channel and the output channel log file.",
|
||||
"mssql.logRetentionMinutes":"Number of minutes to retain log files for backend services. Default is 1 week.",
|
||||
"mssql.logFilesRemovalLimit":"Maximum number of old files to remove upon startup that have expired mssql.logRetentionMinutes. Files that do not get cleaned up due to this limitation get cleaned up next time Azure Data Studio starts up.",
|
||||
"mssql.query.setRowCount":"Maximum number of rows to return before the server stops processing your query.",
|
||||
|
|
|
@ -6,14 +6,17 @@
|
|||
import * as vscode from 'vscode';
|
||||
import { IAccount } from 'vscode-mssql';
|
||||
import * as Constants from '../constants/constants';
|
||||
import { Logger } from '../models/logger';
|
||||
|
||||
export class AccountStore {
|
||||
constructor(
|
||||
private _context: vscode.ExtensionContext
|
||||
private _context: vscode.ExtensionContext,
|
||||
private _logger: Logger
|
||||
) { }
|
||||
|
||||
public getAccounts(): IAccount[] {
|
||||
let configValues = this._context.globalState.get<IAccount[]>(Constants.configAzureAccount) ?? [];
|
||||
this._logger.verbose(`Retreived ${configValues?.length} Azure accounts from account store.`);
|
||||
return configValues;
|
||||
}
|
||||
|
||||
|
@ -21,7 +24,7 @@ export class AccountStore {
|
|||
let account: IAccount;
|
||||
let configValues = this._context.globalState.get<IAccount[]>(Constants.configAzureAccount);
|
||||
if (!configValues) {
|
||||
throw new Error('no accounts stored');
|
||||
throw new Error('No Azure accounts stored');
|
||||
}
|
||||
for (let value of configValues) {
|
||||
if (value.key.id === key) {
|
||||
|
@ -37,6 +40,9 @@ export class AccountStore {
|
|||
}
|
||||
|
||||
public removeAccount(key: string): void {
|
||||
if (!key) {
|
||||
this._logger.error('Azure Account key not received for removal request.');
|
||||
}
|
||||
let configValues = this.getAccounts();
|
||||
configValues = configValues.filter(val => val.key.id !== key);
|
||||
this._context.globalState.update(Constants.configAzureAccount, configValues);
|
||||
|
@ -50,22 +56,24 @@ export class AccountStore {
|
|||
* @returns {Promise<void>} a Promise that returns when the account was saved
|
||||
*/
|
||||
public async addAccount(account: IAccount): Promise<void> {
|
||||
let configValues = this.getAccounts();
|
||||
// remove element if already present in map
|
||||
if (configValues.length > 0) {
|
||||
configValues = configValues.filter(val => val.key.id !== account.key.id);
|
||||
if (account) {
|
||||
let configValues = this.getAccounts();
|
||||
// remove element if already present in map
|
||||
if (configValues.length > 0) {
|
||||
configValues = configValues.filter(val => val.key.id !== account.key.id);
|
||||
} else {
|
||||
configValues = [];
|
||||
}
|
||||
configValues.unshift(account);
|
||||
await this._context.globalState.update(Constants.configAzureAccount, configValues);
|
||||
} else {
|
||||
configValues = [];
|
||||
this._logger.error('Empty Azure Account cannot be added to account store.');
|
||||
}
|
||||
configValues.unshift(account);
|
||||
await this._context.globalState.update(Constants.configAzureAccount, configValues);
|
||||
}
|
||||
|
||||
public async clearAccounts(): Promise<void> {
|
||||
let configValues = [];
|
||||
await this._context.globalState.update(Constants.configAzureAccount, configValues);
|
||||
|
||||
this._logger.verbose('Cleared all saved Azure accounts');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -11,19 +11,19 @@ import * as http from 'http';
|
|||
import * as path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as vscode from 'vscode';
|
||||
import { AzureLogger } from '../azure/azureLogger';
|
||||
import VscodeWrapper from '../controllers/vscodeWrapper';
|
||||
import { Logger } from '../models/logger';
|
||||
|
||||
export class AzureAuthRequest implements AuthRequest {
|
||||
simpleWebServer: SimpleWebServer;
|
||||
serverPort: string;
|
||||
nonce: string;
|
||||
context: vscode.ExtensionContext;
|
||||
logger: AzureLogger;
|
||||
logger: Logger;
|
||||
_vscodeWrapper: VscodeWrapper;
|
||||
|
||||
|
||||
constructor(context: vscode.ExtensionContext, logger: AzureLogger) {
|
||||
constructor(context: vscode.ExtensionContext, logger: Logger) {
|
||||
this.simpleWebServer = new SimpleWebServer();
|
||||
this.serverPort = undefined;
|
||||
this.nonce = crypto.randomBytes(16).toString('base64');
|
||||
|
|
|
@ -9,7 +9,6 @@ import { AzureStringLookup } from '../azure/azureStringLookup';
|
|||
import { AzureUserInteraction } from '../azure/azureUserInteraction';
|
||||
import { AzureErrorLookup } from '../azure/azureErrorLookup';
|
||||
import { AzureMessageDisplayer } from './azureMessageDisplayer';
|
||||
import { AzureLogger } from '../azure/azureLogger';
|
||||
import { AzureAuthRequest } from './azureAuthRequest';
|
||||
import { SimpleTokenCache } from './cacheService';
|
||||
import * as path from 'path';
|
||||
|
@ -30,47 +29,8 @@ import { AzureAccount } from '../../lib/ads-adal-library/src';
|
|||
import { Subscription } from '@azure/arm-subscriptions';
|
||||
import * as mssql from 'vscode-mssql';
|
||||
import * as azureUtils from './utils';
|
||||
|
||||
function getAppDataPath(): string {
|
||||
let platform = process.platform;
|
||||
switch (platform) {
|
||||
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming');
|
||||
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support');
|
||||
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
||||
default: throw new Error('Platform not supported');
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultLogLocation(): string {
|
||||
return path.join(getAppDataPath(), 'vscode-mssql');
|
||||
}
|
||||
|
||||
async function findOrMakeStoragePath(): Promise<string | undefined> {
|
||||
let defaultLogLocation = getDefaultLogLocation();
|
||||
let storagePath = path.join(defaultLogLocation, 'AAD');
|
||||
|
||||
try {
|
||||
await fs.mkdir(defaultLogLocation, { recursive: true });
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
console.log(`Creating the base directory failed... ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdir(storagePath, { recursive: true });
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
console.error(`Initialization of vscode-mssql storage failed: ${e}`);
|
||||
console.error('Azure accounts will not be available');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Initialized vscode-mssql storage.');
|
||||
return storagePath;
|
||||
}
|
||||
import * as Constants from '../constants/constants';
|
||||
import { Logger, LogLevel } from '../models/logger';
|
||||
|
||||
export class AzureController {
|
||||
|
||||
|
@ -82,7 +42,7 @@ export class AzureController {
|
|||
private cacheService: SimpleTokenCache;
|
||||
private storageService: StorageService;
|
||||
private context: vscode.ExtensionContext;
|
||||
private logger: AzureLogger;
|
||||
private logger: Logger;
|
||||
private prompter: IPrompter;
|
||||
private _vscodeWrapper: VscodeWrapper;
|
||||
private credentialStoreInitialized = false;
|
||||
|
@ -90,17 +50,23 @@ export class AzureController {
|
|||
constructor(
|
||||
context: vscode.ExtensionContext,
|
||||
prompter: IPrompter,
|
||||
logger?: AzureLogger,
|
||||
logger?: Logger,
|
||||
private _subscriptionClientFactory: azureUtils.SubscriptionClientFactory = azureUtils.defaultSubscriptionClientFactory) {
|
||||
this.context = context;
|
||||
this.prompter = prompter;
|
||||
if (!this.logger) {
|
||||
this.logger = new AzureLogger();
|
||||
if (!logger) {
|
||||
let logLevel: LogLevel = LogLevel[utils.getConfigTracingLevel() as keyof typeof LogLevel];
|
||||
let pii = utils.getConfigPiiLogging();
|
||||
let _channel = vscode.window.createOutputChannel(LocalizedConstants.azureLogChannelName);
|
||||
this.logger = new Logger(text => _channel.append(text), logLevel, pii);
|
||||
} else {
|
||||
this.logger = logger;
|
||||
}
|
||||
if (!this._vscodeWrapper) {
|
||||
this._vscodeWrapper = new VscodeWrapper();
|
||||
}
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
this.authRequest = new AzureAuthRequest(this.context, this.logger);
|
||||
await this.authRequest.startServer();
|
||||
|
@ -131,7 +97,7 @@ export class AzureController {
|
|||
|
||||
public async addAccount(accountStore: AccountStore): Promise<IAccount> {
|
||||
let account: IAccount;
|
||||
let config = vscode.workspace.getConfiguration('mssql').get('azureActiveDirectory');
|
||||
let config = azureUtils.getAzureActiveDirectoryConfig();
|
||||
if (config === utils.azureAuthTypeToString(AzureAuthType.AuthCodeGrant)) {
|
||||
let azureCodeGrant = await this.createAuthCodeGrant();
|
||||
account = await azureCodeGrant.startLogin();
|
||||
|
@ -142,12 +108,13 @@ export class AzureController {
|
|||
await accountStore.addAccount(account);
|
||||
}
|
||||
|
||||
this.logger.verbose('Account added successfully.');
|
||||
return account;
|
||||
}
|
||||
|
||||
public async getAccountSecurityToken(account: IAccount, tenantId: string | undefined, settings: AADResource): Promise<Token> {
|
||||
let token: Token;
|
||||
let config = vscode.workspace.getConfiguration('mssql').get('azureActiveDirectory');
|
||||
let config = azureUtils.getAzureActiveDirectoryConfig();
|
||||
if (config === utils.azureAuthTypeToString(AzureAuthType.AuthCodeGrant)) {
|
||||
let azureCodeGrant = await this.createAuthCodeGrant();
|
||||
tenantId = tenantId ? tenantId : azureCodeGrant.getHomeTenant(account).id;
|
||||
|
@ -161,6 +128,7 @@ export class AzureController {
|
|||
account, tenantId, settings
|
||||
);
|
||||
}
|
||||
this.logger.verbose('Access token retreived successfully.');
|
||||
return token;
|
||||
}
|
||||
|
||||
|
@ -180,6 +148,7 @@ export class AzureController {
|
|||
|
||||
if (!token) {
|
||||
let errorMessage = LocalizedConstants.msgGetTokenFail;
|
||||
this.logger.error(errorMessage);
|
||||
this._vscodeWrapper.showErrorMessage(errorMessage);
|
||||
} else {
|
||||
profile.azureAccountToken = token.token;
|
||||
|
@ -276,28 +245,27 @@ export class AzureController {
|
|||
}
|
||||
|
||||
private async createAuthCodeGrant(): Promise<AzureCodeGrant> {
|
||||
let azureLogger = new AzureLogger();
|
||||
await this.initializeCredentialStore();
|
||||
return new AzureCodeGrant(
|
||||
providerSettings, this.storageService, this.cacheService, azureLogger,
|
||||
providerSettings, this.storageService, this.cacheService, this.logger,
|
||||
this.azureMessageDisplayer, this.azureErrorLookup, this.azureUserInteraction,
|
||||
this.azureStringLookup, this.authRequest
|
||||
);
|
||||
}
|
||||
|
||||
private async createDeviceCode(): Promise<AzureDeviceCode> {
|
||||
let azureLogger = new AzureLogger();
|
||||
await this.initializeCredentialStore();
|
||||
return new AzureDeviceCode(
|
||||
providerSettings, this.storageService, this.cacheService, azureLogger,
|
||||
providerSettings, this.storageService, this.cacheService, this.logger,
|
||||
this.azureMessageDisplayer, this.azureErrorLookup, this.azureUserInteraction,
|
||||
this.azureStringLookup, this.authRequest
|
||||
);
|
||||
}
|
||||
|
||||
public async removeToken(account): Promise<void> {
|
||||
public async removeToken(account: AzureAccount): Promise<void> {
|
||||
let azureAuth = await this.createAuthCodeGrant();
|
||||
await azureAuth.deleteAccountCache(account.key);
|
||||
this.logger.verbose(`Account deleted from cache successfully: ${account.key.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -306,15 +274,57 @@ export class AzureController {
|
|||
*/
|
||||
private async initializeCredentialStore(): Promise<void> {
|
||||
if (!this.credentialStoreInitialized) {
|
||||
let storagePath = await findOrMakeStoragePath();
|
||||
let storagePath = await this.findOrMakeStoragePath();
|
||||
let credentialStore = new CredentialStore(this.context);
|
||||
this.cacheService = new SimpleTokenCache('aad', storagePath, true, credentialStore);
|
||||
this.cacheService = new SimpleTokenCache(Constants.adalCacheFileName, storagePath, true, credentialStore);
|
||||
await this.cacheService.init();
|
||||
this.storageService = this.cacheService.db;
|
||||
this.credentialStoreInitialized = true;
|
||||
this.logger.verbose(`Credential store initialized.`);
|
||||
}
|
||||
}
|
||||
|
||||
private getAppDataPath(): string {
|
||||
let platform = process.platform;
|
||||
switch (platform) {
|
||||
case 'win32': return process.env['APPDATA'] || path.join(process.env['USERPROFILE'], 'AppData', 'Roaming');
|
||||
case 'darwin': return path.join(os.homedir(), 'Library', 'Application Support');
|
||||
case 'linux': return process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');
|
||||
default: throw new Error('Platform not supported');
|
||||
}
|
||||
}
|
||||
|
||||
private getDefaultOutputLocation(): string {
|
||||
return path.join(this.getAppDataPath(), Constants.vscodeAppName);
|
||||
}
|
||||
|
||||
// Generates storage path for Azure Account cache, e.g C:\users\<>\AppData\Roaming\Code\Azure Accounts\
|
||||
private async findOrMakeStoragePath(): Promise<string | undefined> {
|
||||
let defaultOutputLocation = this.getDefaultOutputLocation();
|
||||
let storagePath = path.join(defaultOutputLocation, Constants.azureAccountDirectory);
|
||||
|
||||
try {
|
||||
await fs.mkdir(defaultOutputLocation, { recursive: true });
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
this.logger.error(`Creating the base directory failed... ${e}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.mkdir(storagePath, { recursive: true });
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
this.logger.error(`Initialization of vscode-mssql storage failed: ${e}`);
|
||||
this.logger.error('Azure accounts will not be available');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('Initialized vscode-mssql storage.');
|
||||
return storagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the token still valid, refreshes the token for given account
|
||||
|
@ -323,10 +333,13 @@ export class AzureController {
|
|||
public async checkAndRefreshToken(
|
||||
session: mssql.IAzureAccountSession,
|
||||
accountStore: AccountStore): Promise<void> {
|
||||
if (session.account && AzureController.isTokenInValid(session.token?.token, session.token.expiresOn)) {
|
||||
if (session?.account && AzureController.isTokenInValid(session.token?.token, session.token.expiresOn)) {
|
||||
const token = await this.refreshToken(session.account, accountStore,
|
||||
providerSettings.resources.azureManagementResource);
|
||||
session.token = token;
|
||||
this.logger.verbose(`Access Token refreshed for account: ${session?.account?.key.id}`);
|
||||
} else {
|
||||
this.logger.verbose(`Access Token not refreshed for account: ${session?.account?.key.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { Logger } from '@microsoft/ads-adal-library';
|
||||
|
||||
export class AzureLogger implements Logger {
|
||||
public log(msg: any, ...vals: any[]): void {
|
||||
const fullMessage = `${msg} - ${vals.map(v => JSON.stringify(v)).join(' - ')}`;
|
||||
console.log(fullMessage);
|
||||
}
|
||||
public error(msg: any, ...vals: any[]): void {
|
||||
const fullMessage = `${msg} - ${vals.map(v => JSON.stringify(v)).join(' - ')}`;
|
||||
console.error(fullMessage);
|
||||
}
|
||||
public pii(msg: any, ...vals: any[]): void {
|
||||
const fullMessage = `${msg} - ${vals.map(v => JSON.stringify(v)).join(' - ')}`;
|
||||
console.log(fullMessage);
|
||||
}
|
||||
}
|
|
@ -8,8 +8,12 @@ import { SqlManagementClient } from '@azure/arm-sql';
|
|||
import { SubscriptionClient } from '@azure/arm-subscriptions';
|
||||
import { PagedAsyncIterableIterator } from '@azure/core-paging';
|
||||
import { Token } from 'vscode-mssql';
|
||||
import * as vscode from 'vscode';
|
||||
import * as Constants from '../constants/constants';
|
||||
import { TokenCredentialWrapper } from './credentialWrapper';
|
||||
|
||||
const configAzureAD = 'azureActiveDirectory';
|
||||
|
||||
/**
|
||||
* Helper method to convert azure results that comes as pages to an array
|
||||
* @param pages azure resources as pages
|
||||
|
@ -40,3 +44,17 @@ export type SqlManagementClientFactory = (token: Token, subscriptionId: string)
|
|||
export function defaultSqlManagementClientFactory(token: Token, subscriptionId: string): SqlManagementClient {
|
||||
return new SqlManagementClient(new TokenCredentialWrapper(token), subscriptionId);
|
||||
}
|
||||
|
||||
|
||||
function getConfiguration(): vscode.WorkspaceConfiguration {
|
||||
return vscode.workspace.getConfiguration(Constants.extensionConfigSectionName);
|
||||
}
|
||||
|
||||
export function getAzureActiveDirectoryConfig(): string {
|
||||
let config = getConfiguration();
|
||||
if (config) {
|
||||
return config.get(configAzureAD);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Collection of Non-localizable Constants
|
||||
export const vscodeAppName = 'Code';
|
||||
export const languageId = 'sql';
|
||||
export const extensionName = 'mssql';
|
||||
export const extensionConfigSectionName = 'mssql';
|
||||
|
@ -18,6 +19,7 @@ export const connectionsArrayName = 'connections';
|
|||
export const disconnectedServerLabel = 'disconnectedServer';
|
||||
export const serverLabel = 'Server';
|
||||
export const folderLabel = 'Folder';
|
||||
export const azureAccountDirectory = 'Azure Accounts';
|
||||
export const cmdRunQuery = 'mssql.runQuery';
|
||||
export const cmdRunCurrentStatement = 'mssql.runCurrentStatement';
|
||||
export const cmdCancelQuery = 'mssql.cancelQuery';
|
||||
|
@ -63,6 +65,8 @@ export const cmdAzureSignIn = 'azure-account.login';
|
|||
export const cmdAzureSignInWithDeviceCode = 'azure-account.loginWithDeviceCode';
|
||||
export const cmdAzureSignInToCloud = 'azure-account.loginToCloud';
|
||||
export const cmdAadRemoveAccount = 'mssql.removeAadAccount';
|
||||
export const piiLogging = 'piiLogging';
|
||||
export const mssqlPiiLogging = 'mssql.piiLogging';
|
||||
export const sqlDbPrefix = '.database.windows.net';
|
||||
export const defaultConnectionTimeout = 15;
|
||||
export const azureSqlDbConnectionTimeout = 30;
|
||||
|
@ -108,6 +112,7 @@ export const databaseString = 'Database';
|
|||
export const localizedTexts = 'localizedTexts';
|
||||
export const ipAddressRegex = /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/;
|
||||
export const configAzureAccount = 'azureAccount';
|
||||
export const adalCacheFileName = 'azureTokenCache_azure_publicCloud';
|
||||
|
||||
// Configuration Constants
|
||||
export const copyIncludeHeaders = 'copyIncludeHeaders';
|
||||
|
|
|
@ -116,7 +116,7 @@ export default class ConnectionManager {
|
|||
}
|
||||
|
||||
if (!this._accountStore) {
|
||||
this._accountStore = new AccountStore(context);
|
||||
this._accountStore = new AccountStore(context, this.client?.logger);
|
||||
}
|
||||
|
||||
if (!this._connectionUI) {
|
||||
|
|
|
@ -1205,9 +1205,20 @@ export default class MainController implements vscode.Disposable {
|
|||
if (needsRefresh) {
|
||||
this._objectExplorerProvider.refresh(undefined);
|
||||
}
|
||||
if (e.affectsConfiguration(Constants.mssqlPiiLogging)) {
|
||||
this.updatePiiLoggingLevel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates Pii Logging configuration for Logger.
|
||||
*/
|
||||
private updatePiiLoggingLevel(): void {
|
||||
const piiLogging: boolean = vscode.workspace.getConfiguration(Constants.extensionName).get(Constants.piiLogging, false);
|
||||
SqlToolsServerClient.instance.logger.piiLogging = piiLogging;
|
||||
}
|
||||
|
||||
public removeAadAccount(prompter: IPrompter): void {
|
||||
this.connectionManager.removeAccount(prompter);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import * as path from 'path';
|
|||
import VscodeWrapper from '../controllers/vscodeWrapper';
|
||||
import * as Utils from '../models/utils';
|
||||
import { VersionRequest } from '../models/contracts';
|
||||
import { Logger } from '../models/logger';
|
||||
import { Logger, LogLevel } from '../models/logger';
|
||||
import * as Constants from '../constants/constants';
|
||||
import ServerProvider from './server';
|
||||
import ServiceDownloadProvider from './serviceDownloadProvider';
|
||||
|
@ -138,6 +138,10 @@ export default class SqlToolsServiceClient {
|
|||
return this._client.diagnostics;
|
||||
}
|
||||
|
||||
public get logger(): Logger {
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _config: IConfig,
|
||||
private _server: ServerProvider,
|
||||
|
@ -150,8 +154,10 @@ export default class SqlToolsServiceClient {
|
|||
public static get instance(): SqlToolsServiceClient {
|
||||
if (this._instance === undefined) {
|
||||
let config = new ExtConfig();
|
||||
let logLevel: LogLevel = LogLevel[Utils.getConfigTracingLevel() as keyof typeof LogLevel];
|
||||
let pii = Utils.getConfigPiiLogging();
|
||||
_channel = vscode.window.createOutputChannel(Constants.serviceInitializingOutputChannelName);
|
||||
let logger = new Logger(text => _channel.append(text));
|
||||
let logger = new Logger(text => _channel.append(text), logLevel, pii);
|
||||
let serverStatusView = new ServerStatusView();
|
||||
let httpClient = new HttpClient();
|
||||
let decompressProvider = new DecompressProvider();
|
||||
|
|
|
@ -4,28 +4,109 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as os from 'os';
|
||||
import { ILogger } from './interfaces';
|
||||
import { Logger as AzureLogger } from '@microsoft/ads-adal-library';
|
||||
import * as Utils from './utils';
|
||||
|
||||
export enum LogLevel {
|
||||
'Pii',
|
||||
'Off',
|
||||
'Critical',
|
||||
'Error',
|
||||
'Warning',
|
||||
'Information',
|
||||
'Verbose',
|
||||
'All'
|
||||
}
|
||||
|
||||
/*
|
||||
* Logger class handles logging messages using the Util functions.
|
||||
*/
|
||||
export class Logger implements ILogger {
|
||||
export class Logger implements ILogger, AzureLogger {
|
||||
private _writer: (message: string) => void;
|
||||
private _piiLogging: boolean = false;
|
||||
private _prefix: string;
|
||||
private _logLevel: LogLevel;
|
||||
|
||||
private _indentLevel: number = 0;
|
||||
private _indentSize: number = 4;
|
||||
private _atLineStart: boolean = false;
|
||||
|
||||
constructor(writer: (message: string) => void, prefix?: string) {
|
||||
constructor(writer: (message: string) => void, logLevel: LogLevel, piiLogging: boolean, prefix?: string) {
|
||||
this._writer = writer;
|
||||
this._logLevel = logLevel;
|
||||
this._piiLogging = piiLogging;
|
||||
this._prefix = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message containing PII (when enabled). Provides the ability to sanitize or shorten values to hide information or reduce the amount logged.
|
||||
* @param msg The initial message to log
|
||||
* @param objsToSanitize Set of objects we want to sanitize
|
||||
* @param stringsToShorten Set of strings to shorten
|
||||
* @param vals Any other values to add on to the end of the log message
|
||||
*/
|
||||
public piiSantized(msg: any, objsToSanitize: { name: string, objOrArray: any | any[] }[],
|
||||
stringsToShorten: { name: string, value: string }[], ...vals: any[]): void {
|
||||
if (this.piiLogging) {
|
||||
msg = [
|
||||
msg,
|
||||
...objsToSanitize?.map(obj => `${obj.name}=${sanitize(obj.objOrArray)}`),
|
||||
...stringsToShorten.map(str => `${str.name}=${shorten(str.value)}`)
|
||||
].join(' ');
|
||||
this.write(LogLevel.Pii, msg, vals);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message containing PII (when enabled).
|
||||
* @param msg The initial message to log
|
||||
* @param vals Any other values to add on to the end of the log message
|
||||
*/
|
||||
public pii(msg: any, ...vals: any[]): void {
|
||||
if (this.piiLogging) {
|
||||
this.write(LogLevel.Pii, msg, vals);
|
||||
}
|
||||
}
|
||||
|
||||
public set piiLogging(val: boolean) {
|
||||
this._piiLogging = val;
|
||||
}
|
||||
|
||||
public get piiLogging(): boolean {
|
||||
return this._piiLogging;
|
||||
}
|
||||
|
||||
public shouldLog(logLevel: LogLevel): Boolean {
|
||||
return logLevel <= this._logLevel;
|
||||
}
|
||||
|
||||
private write(logLevel: LogLevel, msg: any, ...vals: any[]): void {
|
||||
if (this.shouldLog(logLevel) || logLevel === LogLevel.Pii) {
|
||||
const fullMessage = `[${LogLevel[logLevel]}]: ${msg} - ${vals.map(v => JSON.stringify(v)).join(' - ')}`;
|
||||
this.appendLine(fullMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public logDebug(message: string): void {
|
||||
Utils.logDebug(message);
|
||||
}
|
||||
|
||||
public log(msg: any, ...vals: any[]): void {
|
||||
this.write(LogLevel.All, msg, vals);
|
||||
}
|
||||
|
||||
public error(msg: any, ...vals: any[]): void {
|
||||
this.write(LogLevel.Error, msg, vals);
|
||||
}
|
||||
|
||||
public info(msg: any, ...vals: any[]): void {
|
||||
this.write(LogLevel.Information, msg, vals);
|
||||
}
|
||||
|
||||
public verbose(msg: any, ...vals: any[]): void {
|
||||
this.write(LogLevel.Verbose, msg, vals);
|
||||
}
|
||||
|
||||
private appendCore(message: string): void {
|
||||
if (this._atLineStart) {
|
||||
if (this._indentLevel > 0) {
|
||||
|
@ -64,3 +145,54 @@ export class Logger implements ILogger {
|
|||
this._atLineStart = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a given object for logging to the output window, removing/shortening any PII or unneeded values
|
||||
* @param objOrArray The object to sanitize for output logging
|
||||
* @returns The stringified version of the sanitized object
|
||||
*/
|
||||
function sanitize(objOrArray: any): string {
|
||||
if (Array.isArray(objOrArray)) {
|
||||
return JSON.stringify(objOrArray.map(o => sanitizeImpl(o)));
|
||||
} else {
|
||||
return sanitizeImpl(objOrArray);
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeImpl(obj: any): string {
|
||||
obj = Object.assign({}, obj);
|
||||
delete obj.domains; // very long and not really useful
|
||||
// shorten all tokens since we don't usually need the exact values and there's security concerns if they leaked
|
||||
shortenIfExists(obj, 'token');
|
||||
shortenIfExists(obj, 'refresh_token');
|
||||
shortenIfExists(obj, 'access_token');
|
||||
shortenIfExists(obj, 'code');
|
||||
shortenIfExists(obj, 'id_token');
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortens the given string property on an object if it exists, otherwise does nothing
|
||||
* @param obj The object possibly containing the property
|
||||
* @param property The name of the property to shorten - if it exists
|
||||
*/
|
||||
function shortenIfExists(obj: any, property: string): void {
|
||||
if (obj[property]) {
|
||||
obj[property] = shorten(obj[property]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortens a given string - if it's longer than 6 characters will return the first 3 characters
|
||||
* followed by a ... followed by the last 3 characters. Returns the original string if 6 characters
|
||||
* or less.
|
||||
* @param str The string to shorten
|
||||
* @returns Shortened string in the form 'xxx...xxx'
|
||||
*/
|
||||
function shorten(str?: string): string | undefined {
|
||||
// Don't shorten if adding the ... wouldn't make the string shorter
|
||||
if (!str || str.length < 10) {
|
||||
return str;
|
||||
}
|
||||
return `${str.substr(0, 3)}...${str.slice(-3)}`;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const msInM = 60000;
|
|||
const msInS = 1000;
|
||||
|
||||
const configTracingLevel = 'tracingLevel';
|
||||
const configPiiLogging = 'piiLogging';
|
||||
const configLogRetentionMinutes = 'logRetentionMinutes';
|
||||
const configLogFilesRemovalLimit = 'logFilesRemovalLimit';
|
||||
|
||||
|
@ -407,6 +408,15 @@ export function getConfigTracingLevel(): string {
|
|||
}
|
||||
}
|
||||
|
||||
export function getConfigPiiLogging(): boolean {
|
||||
let config = getConfiguration();
|
||||
if (config) {
|
||||
return config.get(configPiiLogging);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfigLogFilesRemovalLimit(): number {
|
||||
let config = getConfiguration();
|
||||
if (config) {
|
||||
|
@ -442,6 +452,9 @@ export function getCommonLaunchArgsAndCleanupOldLogFiles(logPath: string, fileNa
|
|||
console.log(`Old log files deletion report: ${JSON.stringify(deletedLogFiles)}`);
|
||||
launchArgs.push('--tracing-level');
|
||||
launchArgs.push(getConfigTracingLevel());
|
||||
if (getConfigPiiLogging()) {
|
||||
launchArgs.push('--pii-logging');
|
||||
}
|
||||
return launchArgs;
|
||||
}
|
||||
|
||||
|
|
|
@ -463,6 +463,7 @@ export class ObjectExplorerService {
|
|||
let azureAccountToken = await azureController.refreshToken(
|
||||
account, this._connectionManager.accountStore, providerSettings.resources.databaseResource, connectionCredentials.tenantId);
|
||||
if (!azureAccountToken) {
|
||||
this._client.logger.verbose('Access token could not be refreshed for connection profile.');
|
||||
let errorMessage = LocalizedConstants.msgAccountRefreshFailed;
|
||||
await this._connectionManager.vscodeWrapper.showErrorMessage(
|
||||
errorMessage, LocalizedConstants.refreshTokenLabel).then(async result => {
|
||||
|
@ -472,6 +473,7 @@ export class ObjectExplorerService {
|
|||
connectionCredentials.azureAccountToken = updatedProfile.azureAccountToken;
|
||||
connectionCredentials.expiresOn = updatedProfile.expiresOn;
|
||||
} else {
|
||||
this._client.logger.error('Credentials not refreshed by user.');
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
@ -488,8 +490,11 @@ export class ObjectExplorerService {
|
|||
this._sessionIdToConnectionCredentialsMap.set(response.sessionId, connectionCredentials);
|
||||
this._sessionIdToPromiseMap.set(response.sessionId, promise);
|
||||
return response.sessionId;
|
||||
} else {
|
||||
this._client.logger.error('No response received for session creation request');
|
||||
}
|
||||
} else {
|
||||
this._client.logger.error('Connection could not be made, as credentials not available.');
|
||||
// no connection was made
|
||||
promise.resolve(undefined);
|
||||
return undefined;
|
||||
|
|
|
@ -19,6 +19,7 @@ import * as assert from 'assert';
|
|||
import { AccountStore } from '../src/azure/accountStore';
|
||||
import { IConnectionInfo } from 'vscode-mssql';
|
||||
import { AzureController } from '../src/azure/azureController';
|
||||
import { Logger } from '../src/models/logger';
|
||||
|
||||
function createTestCredentials(): IConnectionInfo {
|
||||
const creds: IConnectionInfo = {
|
||||
|
@ -67,6 +68,7 @@ suite('Connection Profile tests', () => {
|
|||
let mockAzureController: AzureController;
|
||||
let mockContext: TypeMoq.IMock<vscode.ExtensionContext>;
|
||||
let mockPrompter: TypeMoq.IMock<IPrompter>;
|
||||
let mockLogger: TypeMoq.IMock<Logger>;
|
||||
let globalstate: TypeMoq.IMock<vscode.Memento & { setKeysForSync(keys: readonly string[]): void; }>;
|
||||
|
||||
setup(() => {
|
||||
|
@ -74,9 +76,10 @@ suite('Connection Profile tests', () => {
|
|||
globalstate = TypeMoq.Mock.ofType<vscode.Memento & { setKeysForSync(keys: readonly string[]): void; }>();
|
||||
mockContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockPrompter = TypeMoq.Mock.ofType<IPrompter>();
|
||||
mockLogger = TypeMoq.Mock.ofType<Logger>();
|
||||
mockContext.setup(c => c.globalState).returns(() => globalstate.object);
|
||||
mockAzureController = new AzureController(mockContext.object, mockPrompter.object);
|
||||
mockAccountStore = new AccountStore(mockContext.object);
|
||||
mockAzureController = new AzureController(mockContext.object, mockPrompter.object, mockLogger.object);
|
||||
mockAccountStore = new AccountStore(mockContext.object, mockLogger.object);
|
||||
});
|
||||
|
||||
test('CreateProfile should ask questions in correct order', async () => {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { ConnectionProfile } from '../src/models/connectionProfile';
|
|||
import { ConnectionCredentials } from '../src/models/connectionCredentials';
|
||||
import * as LocalizedConstants from '../src/constants/localizedConstants';
|
||||
import { AccountStore } from '../src/azure/accountStore';
|
||||
import { Logger } from '../src/models/logger';
|
||||
|
||||
suite('Connection UI tests', () => {
|
||||
|
||||
|
@ -28,6 +29,7 @@ suite('Connection UI tests', () => {
|
|||
let connectionStore: TypeMoq.IMock<ConnectionStore>;
|
||||
let connectionManager: TypeMoq.IMock<ConnectionManager>;
|
||||
let mockAccountStore: AccountStore;
|
||||
let mockLogger: TypeMoq.IMock<Logger>;
|
||||
let mockContext: TypeMoq.IMock<vscode.ExtensionContext>;
|
||||
let globalstate: TypeMoq.IMock<vscode.Memento & { setKeysForSync(keys: readonly string[]): void; }>;
|
||||
let quickPickMock: TypeMoq.IMock<vscode.QuickPick<IConnectionCredentialsQuickPickItem>>;
|
||||
|
@ -47,12 +49,13 @@ suite('Connection UI tests', () => {
|
|||
vscodeWrapper.setup(v => v.createQuickPick()).returns(() => quickPickMock.object);
|
||||
prompter = TypeMoq.Mock.ofType<IPrompter>();
|
||||
mockContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockContext = TypeMoq.Mock.ofType<vscode.ExtensionContext>();
|
||||
mockContext.setup(c => c.globalState).returns(() => globalstate.object);
|
||||
connectionStore = TypeMoq.Mock.ofType(ConnectionStore, TypeMoq.MockBehavior.Loose, mockContext.object);
|
||||
connectionManager = TypeMoq.Mock.ofType(ConnectionManager, TypeMoq.MockBehavior.Loose, mockContext.object);
|
||||
globalstate = TypeMoq.Mock.ofType<vscode.Memento & { setKeysForSync(keys: readonly string[]): void; }>();
|
||||
|
||||
mockAccountStore = new AccountStore(mockContext.object);
|
||||
mockLogger = TypeMoq.Mock.ofType<Logger>();
|
||||
mockAccountStore = new AccountStore(mockContext.object, mockLogger.object);
|
||||
connectionUI = new ConnectionUI(connectionManager.object, mockContext.object,
|
||||
connectionStore.object, mockAccountStore, prompter.object, vscodeWrapper.object);
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as TypeMoq from 'typemoq';
|
|||
import * as assert from 'assert';
|
||||
import ServerProvider from '../src/languageservice/server';
|
||||
import SqlToolsServiceClient from '../src/languageservice/serviceclient';
|
||||
import { Logger } from '../src/models/logger';
|
||||
import { Logger, LogLevel } from '../src/models/logger';
|
||||
import { PlatformInformation } from '../src/models/platform';
|
||||
import StatusView from './../src/views/statusView';
|
||||
import * as LanguageServiceContracts from '../src/models/contracts/languageService';
|
||||
|
@ -25,7 +25,7 @@ suite('Service Client tests', () => {
|
|||
|
||||
let testConfig: TypeMoq.IMock<IConfig>;
|
||||
let testServiceProvider: TypeMoq.IMock<ServerProvider>;
|
||||
let logger = new Logger(text => console.log(text));
|
||||
let logger = new Logger(text => console.log(text), LogLevel.Verbose, false);
|
||||
let testStatusView: TypeMoq.IMock<StatusView>;
|
||||
let vscodeWrapper: TypeMoq.IMock<VscodeWrapper>;
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче