Skip to content

Commit

Permalink
[NayNay] Debug Log (#147)
Browse files Browse the repository at this point in the history
* [NayNay] Debug Log

- install and integrated winston logging library into cli
- allowing us to be able to log any and everything to the respective files saved in the ~/path/to/syslogs/entropy-cryptography/entropy-cli.*.log
- included example use of new logger in the tui flow for balance

* Update logger.ts

Co-authored-by: mix irving <[email protected]>

* Update logger.ts

Co-authored-by: mix irving <[email protected]>

* removed debug function and package and updated use of debug logs throughout cli to use new logger; added masking to the payload logged to obfuscate secret information

* removed use of env vars and mvoed to options arg in logger constructor

* updated changelog

---------

Co-authored-by: Nayyir Jutha <[email protected]>
Co-authored-by: mixmix <[email protected]>
  • Loading branch information
3 people authored Jul 9, 2024
1 parent a9b4b0b commit 614f9ec
Show file tree
Hide file tree
Showing 24 changed files with 443 additions and 70 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Version header format: `[version] Name - year-month-day (entropy-core compatibil
### Added
- new: './src/flows/balance/balance.ts' - service file separated out of main flow containing the pure functions to perform balance requests for one or multiple addresses
- new: './tests/balance.test.ts' - new unit tests file for balance pure functions
- new: './src/common/logger.ts' - utility file consisting of the logger used throughout the entropy cli
- new: './src/common/masking.ts' - utility helper file for EntropyLogger, used to mask private data in the payload (message) of the logging method
### Fixed
- keyring retrieval method was incorrectly returning the default keyring when no keyring was found, which is not the intended flow
### Changed
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,13 @@
"ansi-colors": "^4.1.3",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"debug": "^4.3.4",
"env-paths": "^3.0.0",
"inquirer": "8.0.0",
"lodash.clonedeep": "^4.5.0",
"mkdirp": "^3.0.1",
"typescript": "^4.8.4",
"viem": "^2.7.8",
"winston": "^3.13.0",
"x25519": "^0.1.0"
},
"devDependencies": {
Expand Down
2 changes: 0 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { cliGetBalance } from './flows/balance/cli'
import { cliListAccounts } from './flows/manage-accounts/cli'
import { cliEntropyTransfer } from './flows/entropyTransfer/cli'
import { cliSign } from './flows/sign/cli'
// import { debug } from './common/utils'

const program = new Command()

Expand Down Expand Up @@ -68,7 +67,6 @@ program.command('list')
.description('List all accounts. Output is JSON of form [{ name, address, data }]')
.action(async () => {
// TODO: test if it's an encrypted account, if no password provided, throw because later on there's no protection from a prompt coming up

const accounts = await cliListAccounts()
writeOut(accounts)
process.exit(0)
Expand Down
6 changes: 4 additions & 2 deletions src/common/initializeEntropy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import Entropy, { wasmGlobalsReady } from "@entropyxyz/sdk"
import Keyring from "@entropyxyz/sdk/keys"
import inquirer from "inquirer"
import { decrypt, encrypt } from "../flows/password"
import { debug } from "../common/utils"
import * as config from "../config"
import { EntropyAccountData } from "../config/types"
import { EntropyLogger } from "./logger"

// TODO: unused
// let defaultAccount // have a main account to use
Expand Down Expand Up @@ -36,6 +36,7 @@ type MaybeKeyMaterial = EntropyAccountData | string
// WARNING: in programatic cli mode this function should NEVER prompt users, but it will if no password was provided
// This is currently caught earlier in the code
export const initializeEntropy = async ({ keyMaterial, password, endpoint, configPath }: InitializeEntropyOpts): Promise<Entropy> => {
const logger = new EntropyLogger('initializeEntropy', endpoint)
try {
await wasmGlobalsReady()

Expand Down Expand Up @@ -90,7 +91,7 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint, confi

})
keyrings.default = keyring
debug(keyring)
logger.debug(keyring)

// TO-DO: fix in sdk: admin should be on kering.accounts by default
// /*WANT*/ keyrings[keyring.admin.address] = keyring
Expand All @@ -111,6 +112,7 @@ export const initializeEntropy = async ({ keyMaterial, password, endpoint, confi

return entropy
} catch (error) {
logger.error('Error while initializing entropy', error)
console.error(error.message)
if (error.message.includes('TimeError')) {
process.exit(1)
Expand Down
126 changes: 126 additions & 0 deletions src/common/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import envPaths from 'env-paths'
import { join } from 'path'
import * as winston from 'winston'
import { maskPayload } from './masking'
import { EntropyLoggerOptions } from 'src/types'

/**
* Winston Base Log Levels for NPM
* {
* error: 0,
* warn: 1,
* info: 2,
* http: 3,
* verbose: 4,
* debug: 5,
* silly: 6
* }
*/

export class EntropyLogger {
protected context: string
protected endpoint: string
private winstonLogger: winston.Logger
// TO-DO: update commander with debug, testing, and level options for both programmatic and textual cli
constructor (context: string, endpoint: string, { debug, isTesting, level }: EntropyLoggerOptions = {}) {
this.context = context
this.endpoint = endpoint

let format = winston.format.combine(
// Add timestamp key: { timestamp: 'YYYY-MM-DD HH:mm:ss.SSS' }
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss.SSS',
}),
// If message is instanceof Error, log Error's message property and stack
winston.format.errors({ stack: true }),
// Allows for string interpolation tokens '%s' in message with splat key values
// Ex. { message: 'my message %s', splat: ['test'] } -> { message: 'my message test' }
winston.format.splat(),
// Uses safe-stable-stringify to finalize full object message as string
// (prevents circular references from crashing)
winston.format.json(),
);

if (isTesting) {
format = winston.format.combine(
format,
winston.format.colorize({ level: true }),
winston.format.printf(info => {
let message = typeof info.message === 'object' ? JSON.stringify(info.message, null, 2) : info.message;
if (info.stack) {
message = `${message}\n${info.stack}`;
}
return `${info.level}: ${message}`;
}),
);
}
const paths = envPaths('entropy-cryptography', { suffix: '' })
const DEBUG_PATH = join(paths.log, 'entropy-cli.debug.log')
const ERROR_PATH = join(paths.log, 'entropy-cli.error.log')
const INFO_PATH = join(paths.log, 'entropy-cli.info.log')

this.winstonLogger = winston.createLogger({
level: level || 'info',
format,
defaultMeta: { service: 'Entropy CLI' },
transports: [
new winston.transports.File({
level: 'error',
filename: ERROR_PATH
}),
new winston.transports.File({
level: 'info',
filename: INFO_PATH
}),
new winston.transports.File({
level: 'debug',
filename: DEBUG_PATH,
}),
],
})

// If env var is set then stream logs to console as well as a file
if (debug) {
this.winstonLogger.add(new winston.transports.Console({
format: winston.format.cli()
}))
}
}

// maps to winston:error
public error (description: string, error: Error): void {
this.writeLogMsg('error', error?.message || error, this.context, description, error.stack);
}

// maps to winston:info
public log (message: any, context?: string): void {
this.writeLogMsg('info', message, context);
}

// maps to winston:warn
public warn (message: any, context?: string): void {
this.writeLogMsg('warn', message, context);
}

// maps to winston:debug
public debug (message: any, context?: string): void {
this.writeLogMsg('debug', message, context);
}

// maps to winston:verbose
public verbose (message: any, context?: string): void {
this.writeLogMsg('verbose', message, context);
}

protected writeLogMsg (level: string, message: any, context?: string, description?: string, stack?: string) {
this.winstonLogger.log({
level,
message: maskPayload(message),
context: context || this.context,
endpoint: this.endpoint,
description,
stack,
});
}

}
37 changes: 37 additions & 0 deletions src/common/masking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import cloneDeep from 'lodash.clonedeep'

const DEFAULT_MASKED_FIELDS = [
'seed',
'secretKey',
'addressRaw',
];

export function maskPayload (payload: any): any {
const clonedPayload = cloneDeep(payload);
const maskedPayload = {}

if (!clonedPayload) {
return clonedPayload;
}

// maskJSONFields doesn't handle nested objects very well so we'll
// need to recursively walk to object and mask them one by one
for (const [property, value] of Object.entries(clonedPayload)) {
console.log(property, typeof value);

if (value && typeof value === 'object') {
if (Object.keys(clonedPayload[property]).filter(key => isNaN(parseInt(key))).length === 0) {
const reconstructedUintArr: number[] = Object.values(clonedPayload[property])
maskedPayload[property] = "base64:" + Buffer.from(reconstructedUintArr).toString("base64");
} else {
maskedPayload[property] = maskPayload(value);
}
} else if (value && typeof value === 'string' && DEFAULT_MASKED_FIELDS.includes(property)) {
maskedPayload[property] = "*".repeat(clonedPayload[property].length)
} else {
maskedPayload[property] = value
}
}

return maskedPayload;
}
12 changes: 1 addition & 11 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import { decodeAddress, encodeAddress } from "@polkadot/keyring"
import { hexToU8a, isHex } from "@polkadot/util"
import { Buffer } from 'buffer'
import Debug from 'debug'
import { EntropyAccountConfig } from "../config/types"

const _debug = Debug('@entropyxyz/cli')

export function stripHexPrefix (str: string): string {
if (str.startsWith('0x')) return str.slice(2)
return str
}

export function debug (...args: any[]) {
_debug(...args.map(arg => {
return typeof arg === 'object'
? JSON.stringify(arg, replacer, 2)
: arg
}))
}
function replacer (key, value) {
export function replacer (key, value) {
if(value instanceof Uint8Array ){
return Buffer.from(value).toString('base64')
}
Expand Down
3 changes: 2 additions & 1 deletion src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import envPaths from 'env-paths'


import allMigrations from './migrations'
import { replacer } from 'src/common/utils'

const paths = envPaths('entropy-cryptography', { suffix: '' })
const CONFIG_PATH = join(paths.config, 'entropy-cli.json')
Expand Down Expand Up @@ -67,5 +68,5 @@ export function getSync (configPath = CONFIG_PATH) {

export async function set (config = {}, configPath = CONFIG_PATH) {
await mkdirp(dirname(configPath))
await writeFile(configPath, JSON.stringify(config))
await writeFile(configPath, JSON.stringify(config, replacer))
}
6 changes: 3 additions & 3 deletions src/flows/DeployPrograms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import * as util from "@polkadot/util"
import inquirer from "inquirer"
import { readFileSync } from "fs"
import { initializeEntropy } from "../../common/initializeEntropy"
import { debug, print, getSelectedAccount } from "../../common/utils"
import { print, getSelectedAccount } from "../../common/utils"
import { EntropyTuiOptions } from "src/types"

export async function devPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options) {
export async function devPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options: EntropyTuiOptions) {
const { endpoint } = options
const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress)

Expand Down Expand Up @@ -84,7 +85,6 @@ async function deployProgram (entropy: Entropy, account: any) {

async function getOwnedPrograms (entropy: Entropy, account: any) {
const userAddress = account.address
debug('Account address:',userAddress)
if (!userAddress) return

try {
Expand Down
8 changes: 5 additions & 3 deletions src/flows/UserPrograms/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import inquirer from "inquirer"
import * as util from "@polkadot/util"
import { initializeEntropy } from "../../common/initializeEntropy"
import { debug, getSelectedAccount, print } from "../../common/utils"
import { getSelectedAccount, print } from "../../common/utils"
import { EntropyLogger } from "src/common/logger";

let verifyingKey: string;

export async function userPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options) {
export async function userPrograms ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) {
const FLOW_CONTEXT = 'USER_PROGRAMS'
const { endpoint } = options
const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress)

Expand Down Expand Up @@ -76,7 +78,7 @@ export async function userPrograms ({ accounts, selectedAccount: selectedAccount
message: "Enter the program pointer you wish to check:",
validate: (input) => (input ? true : "Program pointer is required!"),
}])
debug('program pointer', programPointer);
logger.debug(`program pointer: ${programPointer}`, `${FLOW_CONTEXT}::PROGRAM_PRESENCE_CHECK`);
const program = await entropy.programs.dev.get(programPointer);
print(program);
} catch (error) {
Expand Down
5 changes: 3 additions & 2 deletions src/flows/balance/cli.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { initializeEntropy } from '../../common/initializeEntropy'
import * as config from '../../config'
import { debug } from '../../common/utils'
import { getBalance } from './balance'
import { EntropyLogger } from 'src/common/logger'

export async function cliGetBalance ({ address, password, endpoint }) {
const logger = new EntropyLogger('CLI::CHECK_BALANCE', endpoint)
const storedConfig = await config.get()
const account = storedConfig.accounts.find(account => account.address === address)
if (!account) throw Error(`No account with address ${address}`)
// QUESTION: is throwing the right response?
debug('account', account)
logger.debug('account', account)

// check if data is encrypted + we have a password
if (typeof account.data === 'string' && !password) {
Expand Down
9 changes: 6 additions & 3 deletions src/flows/balance/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { EntropyLogger } from "src/common/logger";
import { initializeEntropy } from "../../common/initializeEntropy"
import { print, debug, getSelectedAccount } from "../../common/utils"
import { print, getSelectedAccount } from "../../common/utils"
import { getBalance } from "./balance";

// TO-DO setup flow method to provide options to allow users to select account,
// use external address, or get balances for all accounts in config
export async function checkBalance ({ accounts, selectedAccount: selectedAccountAddress }, options) {
export async function checkBalance ({ accounts, selectedAccount: selectedAccountAddress }, options, logger: EntropyLogger) {
const FLOW_CONTEXT = 'CHECK_BALANCE'
const { endpoint } = options
debug('endpoint', endpoint);
logger.debug(`endpoint: ${endpoint}`, FLOW_CONTEXT)

const selectedAccount = getSelectedAccount(accounts, selectedAccountAddress)
logger.log(selectedAccount, FLOW_CONTEXT)
const entropy = await initializeEntropy({ keyMaterial: selectedAccount.data, endpoint });
const accountAddress = selectedAccountAddress
const freeBalance = await getBalance(entropy, accountAddress)
Expand Down
3 changes: 1 addition & 2 deletions src/flows/entropyFaucet/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import inquirer from "inquirer"
import { print, debug, accountChoices } from "../../common/utils"
import { print, accountChoices } from "../../common/utils"
import { initializeEntropy } from "../../common/initializeEntropy"

export async function entropyFaucet ({ accounts }, options) {
Expand All @@ -14,7 +14,6 @@ export async function entropyFaucet ({ accounts }, options) {

const answers = await inquirer.prompt([accountQuestion])
const selectedAccount = answers.selectedAccount
debug('selectedAccount', selectedAccount)

const recipientAddress = selectedAccount.address
const aliceAccount = {
Expand Down
Loading

0 comments on commit 614f9ec

Please sign in to comment.