Skip to content

Commit

Permalink
SNOW-856233 Easy logging (#642)
Browse files Browse the repository at this point in the history
SNOW-856233 easy logging
  • Loading branch information
sfc-gh-knozderko authored Oct 10, 2023
1 parent 21f884c commit bd44c97
Show file tree
Hide file tree
Showing 20 changed files with 1,621 additions and 325 deletions.
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 {
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();
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

0 comments on commit bd44c97

Please sign in to comment.