diff --git a/.changeset/nervous-bugs-doubt.md b/.changeset/nervous-bugs-doubt.md new file mode 100644 index 0000000000..dd3c6ce557 --- /dev/null +++ b/.changeset/nervous-bugs-doubt.md @@ -0,0 +1,9 @@ +--- +"near-api-js": minor +"@near-js/accounts": patch +"@near-js/providers": patch +"@near-js/utils": patch +"@near-js/wallet-account": patch +--- + +Internal logging library with capabilities for integration with modern logging libraries like Pino, Winston, etc diff --git a/MIGRATION.md b/MIGRATION.md index 5ab6f9e1b0..db45c788c9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -276,6 +276,7 @@ const { format, logWarning, rpc_errors, + Logger } = utils; const { formatNearAmount, @@ -316,5 +317,6 @@ import { parseNearAmount, parseResultError, parseRpcError, + Logger } from '@near-js/utils'; ``` diff --git a/packages/accounts/src/account.ts b/packages/accounts/src/account.ts index 1c462d4202..0ce11cbeda 100644 --- a/packages/accounts/src/account.ts +++ b/packages/accounts/src/account.ts @@ -28,7 +28,7 @@ import { import { baseDecode, baseEncode, - logWarning, + Logger, parseResultError, DEFAULT_FUNCTION_CALL_GAS, printTxOutcomeLogs, @@ -223,12 +223,12 @@ export class Account { return await this.connection.provider.sendTransaction(signedTx); } catch (error) { if (error.type === 'InvalidNonce') { - logWarning(`Retrying transaction ${receiverId}:${baseEncode(txHash)} with new nonce.`); + Logger.warn(`Retrying transaction ${receiverId}:${baseEncode(txHash)} with new nonce.`); delete this.accessKeyByPublicKeyCache[publicKey.toString()]; return null; } if (error.type === 'Expired') { - logWarning(`Retrying transaction ${receiverId}:${baseEncode(txHash)} due to expired block hash`); + Logger.warn(`Retrying transaction ${receiverId}:${baseEncode(txHash)} due to expired block hash`); return null; } @@ -360,9 +360,7 @@ export class Account { * @param beneficiaryId The NEAR account that will receive the remaining Ⓝ balance from the account being deleted */ async deleteAccount(beneficiaryId: string) { - if (!(typeof process === 'object' && process.env['NEAR_NO_LOGS'])) { - console.log('Deleting an account does not automatically transfer NFTs and FTs to the beneficiary address. Ensure to transfer assets before deleting.'); - } + Logger.log('Deleting an account does not automatically transfer NFTs and FTs to the beneficiary address. Ensure to transfer assets before deleting.'); return this.signAndSendTransaction({ receiverId: this.accountId, actions: [deleteAccount(beneficiaryId)] diff --git a/packages/accounts/src/account_2fa.ts b/packages/accounts/src/account_2fa.ts index a45d3ac0a9..6bfec76ef8 100644 --- a/packages/accounts/src/account_2fa.ts +++ b/packages/accounts/src/account_2fa.ts @@ -2,6 +2,7 @@ import { PublicKey } from '@near-js/crypto'; import { FinalExecutionOutcome, TypedError, FunctionCallPermissionView } from '@near-js/types'; import { fetchJson } from '@near-js/providers'; import { actionCreators } from '@near-js/transactions'; +import { Logger } from '@near-js/utils' import BN from 'bn.js'; import { SignAndSendTransactionOptions } from './account'; @@ -84,7 +85,7 @@ export class Account2FA extends AccountMultisig { deployContract(contractBytes), ]; const newFunctionCallActionBatch = actions.concat(functionCall('new', newArgs, MULTISIG_GAS, MULTISIG_DEPOSIT)); - console.log('deploying multisig contract for', accountId); + Logger.log('deploying multisig contract for', accountId); const { stateStatus: multisigStateStatus } = await this.checkMultisigCodeAndStateStatus(contractBytes); switch (multisigStateStatus) { @@ -185,7 +186,7 @@ export class Account2FA extends AccountMultisig { ...(await this.get2faDisableKeyConversionActions()), deployContract(contractBytes), ]; - console.log('disabling 2fa for', this.accountId); + Logger.log('disabling 2fa for', this.accountId); return await this.signAndSendTransaction({ receiverId: this.accountId, actions @@ -217,7 +218,7 @@ export class Account2FA extends AccountMultisig { // TODO: Parse error from result for real (like in normal account.signAndSendTransaction) return result; } catch (e) { - console.warn('Error validating security code:', e); + Logger.warn('Error validating security code:', e); if (e.toString().includes('invalid 2fa code provided') || e.toString().includes('2fa code not valid')) { return await this.promptAndVerify(); } diff --git a/packages/accounts/src/account_multisig.ts b/packages/accounts/src/account_multisig.ts index cfca45354c..c159b4b70c 100644 --- a/packages/accounts/src/account_multisig.ts +++ b/packages/accounts/src/account_multisig.ts @@ -1,5 +1,6 @@ import { Action, actionCreators } from '@near-js/transactions'; import { FinalExecutionOutcome } from '@near-js/types'; +import { Logger } from '@near-js/utils'; import { Account, SignAndSendTransactionOptions } from './account'; import { Connection } from './connection'; @@ -156,7 +157,7 @@ export class AccountMultisig extends Account { actions: [functionCall('delete_request', { request_id: requestIdToDelete }, MULTISIG_GAS, MULTISIG_DEPOSIT)] }); } catch (e) { - console.warn('Attempt to delete an earlier request before 15 minutes failed. Will try again.'); + Logger.warn('Attempt to delete an earlier request before 15 minutes failed. Will try again.'); } } } diff --git a/packages/accounts/src/contract.ts b/packages/accounts/src/contract.ts index 77e7982aaf..1acfb0f2b6 100644 --- a/packages/accounts/src/contract.ts +++ b/packages/accounts/src/contract.ts @@ -1,4 +1,4 @@ -import { getTransactionLastResult } from '@near-js/utils'; +import { getTransactionLastResult, Logger } from '@near-js/utils'; import { ArgumentTypeError, PositionalArgsError } from '@near-js/types'; import { LocalViewExecution } from './local-view-execution'; import Ajv from 'ajv'; @@ -194,8 +194,8 @@ export class Contract { ...options, }); } catch (error) { - console.warn(`Local view execution failed with: "${error.message}"`); - console.warn(`Fallback to normal RPC call`); + Logger.warn(`Local view execution failed with: "${error.message}"`); + Logger.warn(`Fallback to normal RPC call`); } } diff --git a/packages/accounts/test/account.test.js b/packages/accounts/test/account.test.js index 5a6220c079..9b32d4b4d3 100644 --- a/packages/accounts/test/account.test.js +++ b/packages/accounts/test/account.test.js @@ -1,4 +1,4 @@ -const { getTransactionLastResult } = require('@near-js/utils'); +const { getTransactionLastResult, Logger } = require('@near-js/utils'); const { actionCreators } = require('@near-js/transactions'); const { TypedError } = require('@near-js/types'); const BN = require('bn.js'); @@ -90,19 +90,23 @@ test('findAccessKey returns the same access key when fetched simultaneously', as }); describe('errors', () => { - let oldLog; let logs; - beforeEach(async () => { - oldLog = console.log; - logs = []; - console.log = function () { - logs.push(Array.from(arguments).join(' ')); + beforeAll(async () => { + const custom = { + log: (...args) => { + logs.push(args.join(' ')); + }, + warn: () => {}, + error: () => {}, }; + + Logger.overrideLogger(custom); }); - afterEach(async () => { - console.log = oldLog; + beforeEach(async () => { + logs = []; + }); test('create existing account', async() => { @@ -112,7 +116,6 @@ describe('errors', () => { }); describe('with deploy contract', () => { - let oldLog; let logs; let contractId = testUtils.generateUniqueString('test_contract'); let contract; @@ -125,18 +128,20 @@ describe('with deploy contract', () => { viewMethods: ['hello', 'getValue', 'returnHiWithLogs'], changeMethods: ['setValue', 'generateLogs', 'triggerAssert', 'testSetRemove', 'crossContract'] }); - }); - beforeEach(async () => { - oldLog = console.log; - logs = []; - console.log = function () { - logs.push(Array.from(arguments).join(' ')); + const custom = { + log: (...args) => { + logs.push(args.join(' ')); + }, + warn: () => {}, + error: () => {}, }; + + Logger.overrideLogger(custom); }); - afterEach(async () => { - console.log = oldLog; + beforeEach(async () => { + logs = []; }); test('cross-contact assertion and panic', async () => { diff --git a/packages/near-api-js/src/connect.ts b/packages/near-api-js/src/connect.ts index 656450bace..64777da84e 100644 --- a/packages/near-api-js/src/connect.ts +++ b/packages/near-api-js/src/connect.ts @@ -20,12 +20,21 @@ * }) * } * ``` + * @example disable library logs + * ```js + * async function initNear() { + * const near = await connect({ + * networkId: 'testnet', + * nodeUrl: 'https://rpc.testnet.near.org', + * logger: false + * }) + * } * @module connect */ import { readKeyFile } from './key_stores/unencrypted_file_system_keystore'; import { InMemoryKeyStore, MergeKeyStore } from './key_stores'; import { Near, NearConfig } from './near'; -import { logWarning } from './utils'; +import { Logger } from '@near-js/utils'; export interface ConnectConfig extends NearConfig { /** @@ -38,6 +47,13 @@ export interface ConnectConfig extends NearConfig { * Initialize connection to Near network. */ export async function connect(config: ConnectConfig): Promise { + if (config.logger === false) { + // disables logging + Logger.overrideLogger(undefined); + } else if (config.logger !== undefined && config.logger !== null) { + Logger.overrideLogger(config.logger); + } + // Try to find extra key in `KeyPath` if provided. if (config.keyPath && (config.keyStore || config.deps?.keyStore)) { try { @@ -54,12 +70,10 @@ export async function connect(config: ConnectConfig): Promise { keyPathStore, config.keyStore || config.deps?.keyStore ], { writeKeyStoreIndex: 1 }); - if (!(typeof process === 'object' && process.env['NEAR_NO_LOGS'])) { - console.log(`Loaded master account ${accountKeyFile[0]} key from ${config.keyPath} with public key = ${keyPair.getPublicKey()}`); - } + Logger.log(`Loaded master account ${accountKeyFile[0]} key from ${config.keyPath} with public key = ${keyPair.getPublicKey()}`); } } catch (error) { - logWarning(`Failed to load master account key from ${config.keyPath}: ${error}`); + Logger.warn(`Failed to load master account key from ${config.keyPath}: ${error}`); } } return new Near(config); diff --git a/packages/near-api-js/src/utils/index.ts b/packages/near-api-js/src/utils/index.ts index 69b355258f..43f784c3da 100644 --- a/packages/near-api-js/src/utils/index.ts +++ b/packages/near-api-js/src/utils/index.ts @@ -8,6 +8,7 @@ import * as rpc_errors from './rpc_errors'; import { PublicKey, KeyPair, KeyPairEd25519 } from './key_pair'; import { logWarning } from './errors'; +import { Logger } from './logger'; export { key_pair, @@ -20,4 +21,5 @@ export { KeyPairEd25519, rpc_errors, logWarning, + Logger }; diff --git a/packages/near-api-js/src/utils/logger.ts b/packages/near-api-js/src/utils/logger.ts new file mode 100644 index 0000000000..92523889fd --- /dev/null +++ b/packages/near-api-js/src/utils/logger.ts @@ -0,0 +1 @@ +export { Logger } from '@near-js/utils'; diff --git a/packages/providers/src/fetch_json.ts b/packages/providers/src/fetch_json.ts index 57ee7a9766..1e8836f1eb 100644 --- a/packages/providers/src/fetch_json.ts +++ b/packages/providers/src/fetch_json.ts @@ -1,4 +1,5 @@ import { TypedError } from '@near-js/types'; +import { Logger } from '@near-js/utils'; import createError from 'http-errors'; import { exponentialBackoff } from './exponential-backoff'; @@ -16,8 +17,6 @@ export interface ConnectionInfo { headers?: { [key: string]: string | number }; } -const logWarning = (...args) => !(typeof process === 'object' && process.env['NEAR_NO_LOGS']) && console.warn(...args); - export async function fetchJson(connectionInfoOrUrl: string | ConnectionInfo, json?: string): Promise { let connectionInfo: ConnectionInfo = { url: null }; if (typeof (connectionInfoOrUrl) === 'string') { @@ -36,10 +35,10 @@ export async function fetchJson(connectionInfoOrUrl: string | ConnectionInfo, js }); if (!response.ok) { if (response.status === 503) { - logWarning(`Retrying HTTP request for ${connectionInfo.url} as it's not available now`); + Logger.warn(`Retrying HTTP request for ${connectionInfo.url} as it's not available now`); return null; } else if (response.status === 408) { - logWarning(`Retrying HTTP request for ${connectionInfo.url} as the previous connection was unused for some time`); + Logger.warn(`Retrying HTTP request for ${connectionInfo.url} as the previous connection was unused for some time`); return null; } throw createError(response.status, await response.text()); @@ -47,7 +46,7 @@ export async function fetchJson(connectionInfoOrUrl: string | ConnectionInfo, js return response; } catch (error) { if (error.toString().includes('FetchError') || error.toString().includes('Failed to fetch')) { - logWarning(`Retrying HTTP request for ${connectionInfo.url} because of error: ${error}`); + Logger.warn(`Retrying HTTP request for ${connectionInfo.url} because of error: ${error}`); return null; } throw error; diff --git a/packages/providers/src/json-rpc-provider.ts b/packages/providers/src/json-rpc-provider.ts index 0e1cfa2243..22ccd07281 100644 --- a/packages/providers/src/json-rpc-provider.ts +++ b/packages/providers/src/json-rpc-provider.ts @@ -8,6 +8,7 @@ import { baseEncode, getErrorTypeFromErrorMessage, + Logger, parseRpcError, } from '@near-js/utils'; import { @@ -372,9 +373,7 @@ export class JsonRpcProvider extends Provider { return response; } catch (error) { if (error.type === 'TimeoutError') { - if (!(typeof process === 'object' && process.env['NEAR_NO_LOGS'])) { - console.warn(`Retrying request to ${method} as it has timed out`, params); - } + Logger.warn(`Retrying request to ${method} as it has timed out`, params); return null; } diff --git a/packages/utils/src/errors/errors.ts b/packages/utils/src/errors/errors.ts index 8173ea2e65..c13d04657b 100644 --- a/packages/utils/src/errors/errors.ts +++ b/packages/utils/src/errors/errors.ts @@ -1,5 +1,7 @@ +import { Logger } from '../logger'; + +/** @deprecated */ export function logWarning(...args: any[]): void { - if (!(typeof process === 'object' && process.env['NEAR_NO_LOGS'])){ - console.warn(...args); - } + const [message, ...optinalParams] = args; + Logger.warn(message, ...optinalParams); } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index acf0293d22..6fb6401bf4 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,3 +4,4 @@ export * from './format'; export * from './logging'; export * from './provider'; export * from './validators'; +export * from './logger'; diff --git a/packages/utils/src/logger/console.logger.ts b/packages/utils/src/logger/console.logger.ts new file mode 100644 index 0000000000..efc826493b --- /dev/null +++ b/packages/utils/src/logger/console.logger.ts @@ -0,0 +1,64 @@ +import type { LoggerService, LogLevel } from './interface'; + +export class ConsoleLogger implements LoggerService { + public constructor(protected readonly logLevels: LogLevel[]) {} + + public isLevelEnabled = (level: LogLevel): boolean => { + return this.logLevels.includes(level); + }; + + private print( + level: LogLevel, + message: any, + ...optionalParams: any[] + ): void { + switch (level) { + case 'error': + case 'fatal': + return console.error(message, ...optionalParams); + case 'warn': + return console.warn(message, ...optionalParams); + case 'log': + return console.log(message, ...optionalParams); + case 'debug': + case 'verbose': + return console.debug(message, ...optionalParams); + } + } + + verbose(message: any, ...optionalParams: any[]) { + if (!this.isLevelEnabled('verbose')) return; + + this.print('verbose', message, ...optionalParams); + } + + debug(message: any, ...optionalParams: any[]) { + if (!this.isLevelEnabled('debug')) return; + + this.print('debug', message, ...optionalParams); + } + + log(message: any, ...optionalParams: any[]) { + if (!this.isLevelEnabled('log')) return; + + this.print('log', message, ...optionalParams); + } + + warn(message: any, ...optionalParams: any[]) { + if (!this.isLevelEnabled('warn')) return; + + this.print('warn', message, ...optionalParams); + } + + error(message: any, ...optionalParams: any[]) { + if (!this.isLevelEnabled('error')) return; + + this.print('error', message, ...optionalParams); + } + + fatal(message: any, ...optionalParams: any[]) { + if (!this.isLevelEnabled('fatal')) return; + + this.print('fatal', message, ...optionalParams); + } +} diff --git a/packages/utils/src/logger/index.ts b/packages/utils/src/logger/index.ts new file mode 100644 index 0000000000..70a0a1249d --- /dev/null +++ b/packages/utils/src/logger/index.ts @@ -0,0 +1,2 @@ +export { Logger } from './logger'; +export type { LoggerService } from './interface'; diff --git a/packages/utils/src/logger/interface.ts b/packages/utils/src/logger/interface.ts new file mode 100644 index 0000000000..8c1b15429e --- /dev/null +++ b/packages/utils/src/logger/interface.ts @@ -0,0 +1,33 @@ +export type LogLevel = 'log' | 'error' | 'warn' | 'debug' | 'verbose' | 'fatal'; + +export interface LoggerService { + /** + * Write a 'log' level log. + */ + log(message: any, ...optionalParams: any[]): any; + + /** + * Write an 'error' level log. + */ + error(message: any, ...optionalParams: any[]): any; + + /** + * Write a 'warn' level log. + */ + warn(message: any, ...optionalParams: any[]): any; + + /** + * Write a 'debug' level log. + */ + debug?(message: any, ...optionalParams: any[]): any; + + /** + * Write a 'verbose' level log. + */ + verbose?(message: any, ...optionalParams: any[]): any; + + /** + * Write a 'fatal' level log. + */ + fatal?(message: any, ...optionalParams: any[]): any; +} diff --git a/packages/utils/src/logger/logger.ts b/packages/utils/src/logger/logger.ts new file mode 100644 index 0000000000..d528a6792d --- /dev/null +++ b/packages/utils/src/logger/logger.ts @@ -0,0 +1,73 @@ +import { ConsoleLogger } from './console.logger'; +import type { LogLevel, LoggerService } from './interface'; + +const DEFAULT_LOG_LEVELS: LogLevel[] = [ + 'verbose', + 'debug', + 'log', + 'warn', + 'error', + 'fatal', +]; + +const DEFAULT_LOGGER = + typeof process === 'object' && process.env.NEAR_NO_LOGS + ? undefined + : new ConsoleLogger(DEFAULT_LOG_LEVELS); + +/** + * Used to log the library messages + */ +export class Logger { + protected static instanceRef?: LoggerService = DEFAULT_LOGGER; + + public static overrideLogger = (logger?: LoggerService): void => { + this.instanceRef = logger; + }; + + /** + * Write an 'error' level log. + */ + public static error(message: any, stack?: string): void; + public static error(message: any, ...optionalParams: [string, ...any[]]): void; + public static error(message: any, ...optionalParams: any[]) { + this.instanceRef?.error(message, ...optionalParams); + } + + /** + * Write a 'log' level log. + */ + public static log(message: any, ...optionalParams: any[]) { + this.instanceRef?.log(message, ...optionalParams); + } + + /** + * Write a 'warn' level log. + */ + public static warn(message: any, ...optionalParams: any[]) { + this.instanceRef?.warn(message, ...optionalParams); + } + + /** + * Write a 'debug' level log. + */ + public static debug(message: any, ...optionalParams: any[]) { + this.instanceRef?.debug?.(message, ...optionalParams); + } + + /** + * Write a 'verbose' level log. + */ + public static verbose(message: any, ...optionalParams: any[]) { + this.instanceRef?.verbose?.(message, ...optionalParams); + } + + /** + * Write a 'fatal' level log. + */ + public static fatal(message: any, stack?: string): void; + public static fatal(message: any, ...optionalParams: [string, ...any[]]): void; + public static fatal(message: any, ...optionalParams: any[]) { + this.instanceRef?.fatal?.(message, ...optionalParams); + } +} diff --git a/packages/utils/src/logging.ts b/packages/utils/src/logging.ts index 4b134562b7..f80d15096d 100644 --- a/packages/utils/src/logging.ts +++ b/packages/utils/src/logging.ts @@ -1,8 +1,7 @@ import { FinalExecutionOutcome } from '@near-js/types'; import { parseRpcError } from './errors'; - -const SUPPRESS_LOGGING = !!(typeof process === 'object' && process.env.NEAR_NO_LOGS); +import { Logger } from './logger'; /** * Parse and print details from a query execution response @@ -14,10 +13,6 @@ export function printTxOutcomeLogsAndFailures({ contractId, outcome, }: { contractId: string, outcome: FinalExecutionOutcome }) { - if (SUPPRESS_LOGGING) { - return; - } - const flatLogs = [outcome.transaction_outcome, ...outcome.receipts_outcome] .reduce((acc, it) => { const isFailure = typeof it.outcome.status === 'object' && typeof it.outcome.status.Failure === 'object'; @@ -35,7 +30,7 @@ export function printTxOutcomeLogsAndFailures({ }, []); for (const result of flatLogs) { - console.log(`Receipt${result.receiptIds.length > 1 ? 's' : ''}: ${result.receiptIds.join(', ')}`); + Logger.log(`Receipt${result.receiptIds.length > 1 ? 's' : ''}: ${result.receiptIds.join(', ')}`); printTxOutcomeLogs({ contractId, logs: result.logs, @@ -43,7 +38,7 @@ export function printTxOutcomeLogsAndFailures({ }); if (result.failure) { - console.warn(`\tFailure [${contractId}]: ${result.failure}`); + Logger.warn(`\tFailure [${contractId}]: ${result.failure}`); } } } @@ -60,11 +55,7 @@ export function printTxOutcomeLogs({ logs, prefix = '', }: { contractId: string, logs: string[], prefix?: string }) { - if (SUPPRESS_LOGGING) { - return; - } - for (const log of logs) { - console.log(`${prefix}Log [${contractId}]: ${log}`); + Logger.log(`${prefix}Log [${contractId}]: ${log}`); } } diff --git a/packages/utils/test/logger.test.js b/packages/utils/test/logger.test.js new file mode 100644 index 0000000000..d9fbeea0ce --- /dev/null +++ b/packages/utils/test/logger.test.js @@ -0,0 +1,59 @@ +const { Logger } = require('../lib'); + +describe('logger', () => { + let logs; + + beforeEach(async () => { + logs = []; + + const custom = { + verbose: (...args) => { + logs.push(args.join('')); + }, + debug: (...args) => { + logs.push(args.join('')); + }, + log: (...args) => { + logs.push(args.join('')); + }, + warn: (...args) => { + logs.push(args.join('')); + }, + error: (...args) => { + logs.push(args.join('')); + }, + fatal: (...args) => { + logs.push(args.join('')); + }, + }; + + Logger.overrideLogger(custom); + }); + + test('test logger can be overrided', async () => { + Logger.log('111'); + Logger.debug('11s1'); + Logger.warn('1111'); + Logger.error('1131'); + Logger.log('112'); + + expect(logs.length).toBe(5); + }); + + test('test logger can be disabled', async () => { + Logger.overrideLogger(undefined); + + Logger.log('111'); + Logger.log('222'); + + expect(logs.length).toBe(0); + }); + + test('test logged data is accurate', async () => { + Logger.log('lol'); + expect(logs[0]).toEqual('lol'); + + Logger.log('his name is ', 'test'); + expect(logs[1]).toEqual('his name is test'); + }); +}); diff --git a/packages/wallet-account/src/near.ts b/packages/wallet-account/src/near.ts index a67c621af0..9762fa9be1 100644 --- a/packages/wallet-account/src/near.ts +++ b/packages/wallet-account/src/near.ts @@ -17,6 +17,7 @@ import { import { PublicKey } from '@near-js/crypto'; import { KeyStore } from '@near-js/keystores'; import { Signer } from '@near-js/signers'; +import { LoggerService } from '@near-js/utils'; import BN from 'bn.js'; export interface NearConfig { @@ -76,6 +77,10 @@ export interface NearConfig { * Backward-compatibility for older versions */ deps?: { keyStore: KeyStore }; + /** + * Specifies the logger to use. Pass `false` to turn off logging. + */ + logger?: LoggerService | false; } /**