Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-856233 Easy logging #642

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions lib/configuration/client_configuration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
*/
const os = require('os');
const path = require('path');
const fs = require('fs');
const {isString} = require('../util');
const clientConfigFileName = 'sf_client_config.json';

const Levels = Object.freeze({
Off: 'OFF',
Error: 'ERROR',
Warn: 'WARN',
Info: 'INFO',
Debug: 'DEBUG',
Trace: 'TRACE'
});

const allLevels = Object.values(Levels);

class ClientConfig {
sfc-gh-knozderko marked this conversation as resolved.
Show resolved Hide resolved
constructor (loggingConfig) {
this.loggingConfig = loggingConfig;
}
}

class ClientLoggingConfig {
constructor (logLevel, logPath) {
this.logLevel = logLevel;
this.logPath = logPath;
}
}

class ConfigurationError extends Error {
name = 'ConfigurationError';

constructor(message, cause) {
super(message);
this.cause = cause;
Error.captureStackTrace(this, this.constructor);
}

toString() {
return this.message + ': ' + this.cause.toString();
}
}

/**
* @param value {String} Log level.
* @return {String} normalized log level value.
* @throws {Error} Error for unknown value.
*/
function levelFromString (value) {
const level = value.toUpperCase();
sfc-gh-knozderko marked this conversation as resolved.
Show resolved Hide resolved
if (!allLevels.includes(level)) {
throw new Error('Unknown log level: ' + value);
}
return level;
}

/**
* @param fsPromisesModule {module} filestream module
* @param processModule {processModule} process module
*/
function ConfigurationUtil(fsPromisesModule, processModule) {

const fsPromises = typeof fsPromisesModule !== 'undefined' ? fsPromisesModule : require('fs/promises');
const process = typeof processModule !== 'undefined' ? processModule : require('process');

/**
* @param configFilePath {String} A path to a client config file.
* @return {Promise<ClientConfig>} Client configuration.
*/
this.getClientConfig = async function (configFilePath) {
const path = await findConfig(configFilePath);
if (path == null) {
return null;
}
const configFileContents = await readFileConfig(path);
return configFileContents == null ? null : parseConfigFile(configFileContents);
};

function readFileConfig (filePath) {
if (!filePath) {
return Promise.resolve(null);
}
return fsPromises.readFile(filePath, { encoding: 'utf8' })
.catch(err => {
throw new ConfigurationError('Finding client configuration failed', err);
});
}

function parseConfigFile (configurationJson) {
try {
const parsedConfiguration = JSON.parse(configurationJson);
validate(parsedConfiguration);
return new ClientConfig(
new ClientLoggingConfig(
getLogLevel(parsedConfiguration),
getLogPath(parsedConfiguration)
)
);
} catch (err) {
throw new ConfigurationError('Parsing client configuration failed', err);
}
}

function validate (configuration) {
validateLogLevel(configuration);
validateLogPath(configuration);
}

function validateLogLevel(configuration) {
const logLevel = getLogLevel(configuration);
if (logLevel == null) {
return;
}
if (!isString(logLevel)) {
throw new Error('Log level is not a string');
}
levelFromString(logLevel);
}

function validateLogPath(configuration) {
const logPath = getLogPath(configuration);
if (logPath == null) {
return;
}
if (!isString(logPath)) {
throw new Error('Log path is not a string');
}
}

function getLogLevel (configuration) {
return configuration.common.log_level;
}

function getLogPath (configuration) {
return configuration.common.log_path;
}

function findConfig (filePathFromConnectionString) {
return verifyNotEmpty(filePathFromConnectionString)
.then((filePath) => filePath ?? getFilePathFromEnvironmentVariable())
.then((filePath) => filePath ?? searchForConfigInDictionary('.'))
.then((filePath) => filePath ?? searchForConfigInDictionary(os.homedir()))
.then((filePath) => filePath ?? searchForConfigInDictionary(os.tmpdir()));
}

async function verifyNotEmpty (filePath) {
return filePath ? filePath : null;
}

function getFilePathFromEnvironmentVariable () {
return verifyNotEmpty(process.env.SF_CLIENT_CONFIG_FILE);
}

async function searchForConfigInDictionary (dictionary) {
const filePath = path.join(dictionary, clientConfigFileName);
return onlyIfFileExists(filePath);
}

async function onlyIfFileExists (filePath) {
return await fsPromises.access(filePath, fs.constants.F_OK)
.then(() => filePath)
.catch(() => null);
}
}

exports.Levels = Levels;
exports.levelFromString = levelFromString;
exports.ConfigurationUtil = ConfigurationUtil;
26 changes: 19 additions & 7 deletions lib/connection/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var Statement = require('./statement');
var Parameters = require('../parameters');
var Authenticator = require('../authentication/authentication');
var Logger = require('../logger');
const {init: initEasyLogging} = require('../logger/easy_logging_starter')


const PRIVATELINK_URL_SUFFIX = ".privatelink.snowflakecomputing.com";
Expand Down Expand Up @@ -261,13 +262,19 @@ function Connection(context)
throw authErr;
}

// Request connection
services.sf.connect({
callback: connectCallback(self, callback),
json: body
});

// return the connection to facilitate chaining
initEasyLogging(connectionConfig.clientConfigFile)
.then(() => {
try {
services.sf.connect({
callback: connectCallback(self, callback),
json: body
});
} catch (e) {
// we don't expect an error here since callback method should be called
Logger.getInstance().error('Unexpected error from calling callback function', e);
}
})
.catch(() => callback(Errors.createClientError(ErrorCodes.ERR_CONN_CONNECT_INVALID_CLIENT_CONFIG)));
return this;
};

Expand Down Expand Up @@ -301,6 +308,11 @@ function Connection(context)

try
{
try {
await initEasyLogging(connectionConfig.clientConfigFile);
} catch (err) {
throw Errors.createClientError(ErrorCodes.ERR_CONN_CONNECT_INVALID_CLIENT_CONFIG);
}
await auth.authenticate(connectionConfig.getAuthenticator(),
connectionConfig.getServiceName(),
connectionConfig.account,
Expand Down
23 changes: 19 additions & 4 deletions lib/connection/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,11 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
ErrorCodes.ERR_CONN_CREATE_INVALID_GCS_USE_DOWNSCOPED_CREDENTIAL);
}

var clientConfigFile = options.clientConfigFile;
if (Util.exists(clientConfigFile)) {
Errors.checkArgumentValid(Util.isString(clientConfigFile), ErrorCodes.ERR_CONN_CREATE_INVALID_CLIENT_CONFIG_FILE);
}

// remember if we're in qa mode
this._qaMode = qaMode;

Expand Down Expand Up @@ -474,7 +479,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
if (Util.exists(options.disableQueryContextCache)) {
Errors.checkArgumentValid(Util.isBoolean(options.disableQueryContextCache),
ErrorCodes.ERR_CONN_CREATE_INVALID_DISABLED_QUERY_CONTEXT_CACHE);

disableQueryContextCache = options.disableQueryContextCache;
}

Expand All @@ -494,7 +499,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
if (Util.exists(options.includeRetryReason)) {
Errors.checkArgumentValid(Util.isBoolean(options.includeRetryReason),
ErrorCodes.ERR_CONN_CREATE_INVALID_INCLUDE_RETRY_REASON);

includeRetryReason = options.includeRetryReason;
}

Expand Down Expand Up @@ -763,14 +768,14 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)

/**
* Returns whether the Retry reason is included or not in the retry url
*
*
* @returns {Boolean}
*/
this.getIncludeRetryReason = function () {
return includeRetryReason;
}

/**
/**
* Returns whether the Query Context Cache is enabled or not by the configuration
*
* @returns {Boolean}
Expand All @@ -779,6 +784,15 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
return disableQueryContextCache;
}

/**
* Returns the client config file
*
* @returns {String}
*/
this.getClientConfigFile = function () {
return clientConfigFile;
};

// save config options
this.username = options.username;
this.password = options.password;
Expand All @@ -790,6 +804,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
this.masterToken = options.masterToken;
this.masterTokenExpirationTime = options.masterTokenExpirationTime;
this.sessionTokenExpirationTime = options.sessionTokenExpirationTime;
this.clientConfigFile = options.clientConfigFile;

// create the parameters array
var parameters = createParameters();
Expand Down
2 changes: 2 additions & 0 deletions lib/constants/error_messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ exports[404039] = 'Invalid forceStageBindError. The specified value must be a nu
exports[404040] = 'Invalid browser timeout value. The specified value must be a positive number.';
exports[404041] = 'Invalid disablQueryContextCache. The specified value must be a boolean.';
exports[404042] = 'Invalid includeRetryReason. The specified value must be a boolean.'
exports[404043] = 'Invalid clientConfigFile value. The specified value must be a string.';

// 405001
exports[405001] = 'Invalid callback. The specified value must be a function.';
Expand All @@ -76,6 +77,7 @@ exports[405501] = 'Connection already in progress.';
exports[405502] = 'Already connected.';
exports[405503] = 'Connection already terminated. Cannot connect again.';
exports[405504] = 'connect() does not work with external browser or okta authenticators, call connectAsync() instead';
exports[405505] = 'Configuration from client config file failed';

// 406001
exports[406001] = 'Invalid callback. The specified value must be a function.';
Expand Down
24 changes: 16 additions & 8 deletions lib/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,16 +166,13 @@ function Core(options)
*/
configure: function (options)
{
var logTag = options.logLevel;
if (Util.exists(logTag))
{
// check that the specified value is a valid tag
Errors.checkArgumentValid(LoggerCore.isValidLogTag(logTag),
ErrorCodes.ERR_GLOBAL_CONFIGURE_INVALID_LOG_LEVEL);

const logLevel = extractLogLevel(options);
const logFilePath = options.logFilePath;
if (logLevel != null || logFilePath) {
Logger.getInstance().configure(
{
level: LoggerCore.logTagToLevel(logTag)
level: logLevel,
filePath: logFilePath
});
}

Expand Down Expand Up @@ -228,6 +225,17 @@ function Core(options)
}
};

function extractLogLevel(options) {
const logTag = options.logLevel;
if (Util.exists(logTag)) {
Errors.checkArgumentValid(LoggerCore.isValidLogTag(logTag),
ErrorCodes.ERR_GLOBAL_CONFIGURE_INVALID_LOG_LEVEL);

return LoggerCore.logTagToLevel(logTag)
}
return null;
}

// add some read-only constants
var nativeTypeValues = DataTypes.NativeTypes.values;
Object.defineProperties(instance,
Expand Down
2 changes: 2 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ codes.ERR_CONN_CREATE_INVALID_FORCE_STAGE_BIND_ERROR = 404039;
codes.ERR_CONN_CREATE_INVALID_BROWSER_TIMEOUT = 404040;
codes.ERR_CONN_CREATE_INVALID_DISABLED_QUERY_CONTEXT_CACHE = 404041
codes.ERR_CONN_CREATE_INVALID_INCLUDE_RETRY_REASON =404042
codes.ERR_CONN_CREATE_INVALID_CLIENT_CONFIG_FILE = 404043;

// 405001
codes.ERR_CONN_CONNECT_INVALID_CALLBACK = 405001;
Expand All @@ -81,6 +82,7 @@ codes.ERR_CONN_CONNECT_STATUS_CONNECTING = 405501; // sql state: 08002
codes.ERR_CONN_CONNECT_STATUS_CONNECTED = 405502; // sql state: 08002
codes.ERR_CONN_CONNECT_STATUS_DISCONNECTED = 405503; // sql state: 08002
codes.ERR_CONN_CREATE_INVALID_AUTH_CONNECT = 405504;
codes.ERR_CONN_CONNECT_INVALID_CLIENT_CONFIG = 405505;

// 406001
codes.ERR_CONN_DESTROY_INVALID_CALLBACK = 406001;
Expand Down
2 changes: 1 addition & 1 deletion lib/logger/browser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2019 Snowflake Computing Inc. All rights reserved.
* Copyright (c) 2015-2023 Snowflake Computing Inc. All rights reserved.
*/

var Util = require('../util');
Expand Down
Loading
Loading