UNPKG

fix-log-decorate

Version:

Decorates FIX protocol messages in console to make them easier to read

151 lines (127 loc) 4.2 kB
#!/usr/bin/env node import { pipeline, Transform } from 'stream' import chalk from 'chalk' import minimist from 'minimist' import path from 'path' import fs from 'fs' import { fileURLToPath } from 'url' import { msgTypeLU, tagLU } from './fixlu.mjs' const map = fn => new Transform({ transform(chunk, encoding, callback) { try { callback(null, fn(chunk)) } catch(e){ callback(e) } } }) const { usenumber, usename, usevalue, uselookup, usenewline, skipseen, skip, keep, highlight, delim, help, version, } = minimist(process.argv.slice(2), { default: { usenumber: 1, usename: 1, usevalue: 1, uselookup: 1, usenewline: 0, skipseen: 1, skip: '', keep: '', highlight: '', delim: '|', help: false, version: false, } }) if (help || version) { const __dirname = path.dirname(fileURLToPath(import.meta.url)) const readFile = f => fs.readFileSync(path.resolve(__dirname,f)).toString() console.error( help ? readFile('README.md') + "\n" : '', 'fix-log-decorate version:', JSON.parse(readFile('package.json')).version ) process.exit() } const splitBySpace = a => (''+a).split(' ').filter(x => x !== '') const [skipArr, keepArr, highlightArr] = [skip, keep, highlight].map(splitBySpace) const pipeAsDelim = x => ('' + x).replace(/\x01/g, '|').replace(/\\x01/g, '|') const headerFields = ["BeginString","BodyLength","MsgType","SenderCompID","TargetCompID","MsgSeqNum","SendingTime","CheckSum"] const decorate = { fieldNo: ({fieldSeen}) => chalk[fieldSeen ? 'grey':'white'], fieldName: ({fieldName}) => chalk[headerFields.includes(fieldName)? "green" : "greenBright"], value: ({fieldName}) => fieldName === "SenderCompID" ? chalk.inverse.magenta : fieldName === "TargetCompID" ? chalk.magenta : // cyanBright fieldName === "MsgSeqNum" ? chalk.yellowBright : chalk.white, lookup: ({lookup, fieldName}) => chalk [fieldName ==="MsgType" ? 'inverse':'visible'] [ (fieldName ==="MsgType" && lookup === "HEARTBEAT") ? "grey" : (fieldName ==="MsgType") ? "red" : "blueBright" ], } const includes = (arr, vals) => vals.some(x=>arr.includes(x)) const fieldReplacer = (alreadySeen = {}) => (match, fieldNo, value) => { //alreadySeen is a closure state object for tracking items already seen per messsage if (fieldNo == 8 || fieldNo == 10){alreadySeen={}} // reset seen records on new message (in case more than one per line) const valueSeenKey = `${fieldNo}_${value}` const newLine = usenewline && fieldNo == 10 ? '\n' : '' const fieldName = tagLU?.[fieldNo]?.desc const lookup = tagLU?.[fieldNo]?.enum?.[value] const valuesByType = type => /^field/.test(type) ? [fieldNo, fieldName] : [value, lookup] const skipField = includes(skipArr, valuesByType("field")) || (keepArr.length && !includes(keepArr, valuesByType("field"))) const highlighter = ({type, chalk}) => includes(highlightArr, valuesByType(type)) ? chalk.bgYellow.black : chalk const valueLookup = { fieldNo, fieldName, value, lookup, fieldSeen: alreadySeen[fieldNo] } const outputField = (type, condition) => !condition ? '' : highlighter({type, chalk: decorate[type](valueLookup)})(valueLookup[type]) const notYetSeen = x => !skipseen || !alreadySeen[x] const output = skipField ? newLine : [ outputField('fieldNo', usenumber ), outputField('fieldName', usename && fieldName && notYetSeen(fieldNo) ), '=', outputField('value', usevalue || !lookup ), outputField('lookup', uselookup && lookup && notYetSeen(valueSeenKey) ), delim, newLine ].join('') // update state alreadySeen[fieldNo] = true alreadySeen[valueSeenKey] = true return output } const highlightFieldNames = row => { try { return ('' + row).replace(/(\d+)=([^|\n]*)\|/g, fieldReplacer()) } catch (e){ console.error("fix-log-decorate highFieldNames error:",e) return row; } } pipeline( process.stdin, map(pipeAsDelim), map(highlightFieldNames), process.stdout, err => err && console.error('fix-log-decorate error:', err) )