Skip to content

Commit

Permalink
implement colors support
Browse files Browse the repository at this point in the history
  • Loading branch information
hiro5id committed May 22, 2023
1 parent beb81d8 commit a9fe670
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 34 deletions.
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
"emilylilylime.vscode-test-explorer-diagnostics",
"DanielSanMedium.dscodegpt",
"GitHub.vscode-github-actions",
"alexeynobody.javascript-test-runner-reloaded"
"alexeynobody.javascript-test-runner-reloaded",
"yoavbls.pretty-ts-errors"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
Expand Down
14 changes: 13 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
"request": "launch",
"name": "Launch",
"program": "${workspaceFolder}/dist/index.js",
}
},
{
"name": "Debug Node.js programs from command line",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "bash",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"skipFiles": [
"<node_internals>/**"
]
}
]
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ so that it can be easily parsed by tool such as LogDNA.

To suppress some bits of the log to make it less noisy you can set these environment variables:

* CONSOLE_LOG_COLORIZE="true"
* ***This is dasbled by default*** because some logging services such as LogDNA don't understand ANSI color escape codes, so it just introduces noise. But this can make logs easier to read when running locally or in a system that supports ANSI colors.
* CONSOLE_LOG_JSON_DISABLE_AUTO_PARSE="true"
* Disable JSON auto parsing in `@autoParsedJson` and outputs a stringified version of data in the `message` field
* CONSOLE_LOG_JSON_NO_FILE_NAME="true"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"//#### dependencies-documentation": {
"callsites": "this has to stay below v4 because the new one is using ES Module, which would force this package to be changed to an ES module as well"
},
},
"dependencies": {
"app-root-path": "^3.1.0",
"callsites": "^3.1.0",
Expand Down
212 changes: 212 additions & 0 deletions src/colors/colorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import stringify from 'json-stringify-safe';

export interface IDefaultColorMap {
black: string;
red: string;
green: string;
darkGreen: string;
lightGreen: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
teal: string;
lightTeal: string;
darkBlue: string;
darkYellow: string;
lightBlue: string;
purple: string;
pink: string;
lightPink: string;
}

export const defaultColorMap: IDefaultColorMap = {
black: '\x1b[30m',
red: '\x1b[31m',
green: '\x1b[32m',
darkGreen: '\x1b[38;2;36;119;36m',
lightGreen: '\x1b[38;2;0;255;127m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
teal: '\x1b[38;2;26;175;192m',
lightTeal: '\x1b[38;2;31;230;255m',
darkBlue: '\x1b[38;2;54;124;192m',
darkYellow: '\x1b[38;2;159;147;45m',
lightBlue: '\x1b[38;2;120;193;255m',
purple: '\x1b[38;2;135;38;162m',
pink: '\x1b[38;2;168;53;143m',
lightPink: '\x1b[38;2;255;81;216m',
};

export type ColorValue = keyof IDefaultColorMap;

export interface IColorConfiguration {
separator: ColorValue;
string: ColorValue;
number: ColorValue;
boolean: ColorValue;
null: ColorValue;
key: ColorValue;
levelKey: ColorValue;
messageKey: ColorValue;
errorLevel: ColorValue;
nonErrorLevel: ColorValue;
nonErrorMessage: ColorValue;
errorMessage: ColorValue;
warnLevel: ColorValue;
fileNameKey: ColorValue;
fileName: ColorValue;
logCallStackKey: ColorValue;
logCallStack: ColorValue;
packageNameKey: ColorValue;
packageName: ColorValue;
timestampKey: ColorValue;
timestamp: ColorValue;
}

export type ColorItemName = keyof IColorConfiguration;

export const defaultColors: IColorConfiguration = {
separator: 'black',
string: 'white',
number: 'magenta',
boolean: 'cyan',
null: 'red',
key: 'purple',
levelKey: 'teal',
messageKey: 'darkGreen',
errorLevel: 'red',
nonErrorLevel: 'lightTeal',
nonErrorMessage: 'lightGreen',
errorMessage: 'red',
warnLevel: 'yellow',
fileNameKey: 'darkYellow',
fileName: 'yellow',
logCallStackKey: 'blue',
logCallStack: 'lightBlue',
packageNameKey: 'darkYellow',
packageName: 'yellow',
timestampKey: 'pink',
timestamp: 'lightPink',
};

// TODO: this is super beta, consider using Sindre's supports-colors
export function supportsColor() {
const onHeroku = truth(process.env.DYNO) ? true : false;
const forceNoColor = truth(process.env.FORCE_NO_COLOR) ? true : false;
const forceColor = truth(process.env.FORCE_COLOR) ? true : false;
return (!onHeroku && !forceNoColor) || forceColor;
}

// also counts 'false' as false
function truth(it: any) {
return it && it !== 'false' ? true : false;
}

// TODO:colors: support colorizing specific fields like "message"
// TODO:colors: add support for deserializing circual references by incorporating and using 'json-stringify-safe' that i userd here elsewhere but now commented out
// TODO:colors: add support to toggle colors as well as JSON formatting independently

/**
* Given an object, it returns its JSON representation colored using
* ANSI escape characters.
* @param {(Object | string)} json - JSON object to highlighter.
* @param {Colors} [colors] - A map with the ANSI characters for each supported color.
* @param {ColorMap} [colorMap] - An object to configure the coloring.
* @param {number} [spacing=2] - The indentation spaces.
* @returns {string} Stringified JSON colored with ANSI escape characters.
*/
export function colorJson(jsonInput: any, colorsInput: Partial<IColorConfiguration> = defaultColors, colorMap: IDefaultColorMap = defaultColorMap, spacing?: number) {
const colors = { ...defaultColors, ...colorsInput };
let previousMatchedValue: string = '';
let isErrorLevel = false;
let isWarnLevel = false;
let json: string;
if (supportsColor()) {
if (typeof jsonInput !== 'string') json = stringify(jsonInput, null, spacing);
else json = stringify(JSON.parse(jsonInput), null, spacing);
return (
(colorMap as any)[colors.separator] +
json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match: string) => {
let colorCode: ColorItemName = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
colorCode = 'key';
// If key is "level" handle it with special color
if (/\"level\"/i.test(match)) {
colorCode = 'levelKey';
}
// If key is "message" handle it with special color
if (/\"message\"/i.test(match)) {
colorCode = 'messageKey';
}
if (/\"@filename\"/i.test(match)) {
colorCode = 'fileNameKey';
}
if (/\"@logCallStack\"/i.test(match)) {
colorCode = 'logCallStackKey';
}
if (/\"@packageName\"/i.test(match)) {
colorCode = 'packageNameKey';
}
if (/\"@timestamp\"/i.test(match)) {
colorCode = 'timestampKey';
}
} else {
colorCode = 'string';
// If the key is "level" then handle value with special color
if (/\"level\"/i.test(previousMatchedValue)) {
if (/\"error\"/i.test(match)) {
colorCode = 'errorLevel';
isErrorLevel = true;
} else if (/\"warn\"/i.test(match)) {
colorCode = 'warnLevel';
isWarnLevel = true;
} else {
colorCode = 'nonErrorLevel';
}
}
// if the key is "message" then handle value with special color
if (/\"message\"/i.test(previousMatchedValue)) {
if (isErrorLevel) {
colorCode = 'errorMessage';
} else if (isWarnLevel) {
colorCode = 'warnLevel';
} else {
colorCode = 'nonErrorMessage';
}
}
if (/\"@filename\"/i.test(previousMatchedValue)) {
colorCode = 'fileName';
}
if (/\"@logCallStack\"/i.test(previousMatchedValue)) {
colorCode = 'logCallStack';
}
if (/\"@packageName\"/i.test(previousMatchedValue)) {
colorCode = 'packageName';
}
if (/\"@timestamp\"/i.test(previousMatchedValue)) {
colorCode = 'timestamp';
}
}
} else if (/true|false/.test(match)) {
colorCode = 'boolean';
} else if (/null/.test(match)) {
colorCode = 'null';
}
const color = (colorMap as any)[(colors as any)[colorCode]] || '';
previousMatchedValue = match;
return `\x1b[0m${color}${match}${(colorMap as any)[colors.separator]}`;
}) +
'\x1b[0m'
);
} else {
if (typeof jsonInput !== 'string') json = stringify(jsonInput, null, spacing);
else json = stringify(JSON.parse(jsonInput), null, spacing);
return json;
}
}
3 changes: 3 additions & 0 deletions src/colors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// created from 'create-ts-index'

export * from './colorize';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// created from 'create-ts-index'

export * from './colors';
export * from './env';
export * from './capture-nested-stack-trace';
export * from './error-with-context';
Expand Down
36 changes: 23 additions & 13 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* tslint:disable:object-literal-sort-keys */
import appRootPath from 'app-root-path';
import stringify from 'json-stringify-safe';
// import stringify from 'json-stringify-safe';
import * as path from 'path';
import * as w from 'winston';
import { ErrorWithContext } from './error-with-context';
Expand All @@ -12,6 +12,8 @@ import { sortObject } from './sort-object';
import { ToOneLine } from './to-one-line';
import { Env } from './env';
import { NewLineCharacter } from './new-line-character';
import { colorJson } from './colors/colorize';
import stringify from 'json-stringify-safe';

// tslint:disable-next-line:no-var-requires
require('source-map-support').install({
Expand Down Expand Up @@ -160,7 +162,7 @@ export function FormatErrorObject(object: any) {

// Add timestamp
const { CONSOLE_LOG_JSON_NO_TIME_STAMP } = process.env;
if (!CONSOLE_LOG_JSON_NO_TIME_STAMP) {
if (!(CONSOLE_LOG_JSON_NO_TIME_STAMP && CONSOLE_LOG_JSON_NO_TIME_STAMP.toLowerCase() === 'true')) {
returnData['@timestamp'] = new Date().toISOString();
}

Expand Down Expand Up @@ -202,17 +204,25 @@ export function FormatErrorObject(object: any) {
}
}

const jsonString = stringify(returnData);

// strip ansi colors
const colorStripped = jsonString.replace(/\\u001B\[\d*m/gim, '');

// add new line at the end for better local readability
let endOfLogCharacter = '\n';
if (CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS || CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS_EXCEPT_STACK) {
if (
(CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS && CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS.toLowerCase() === 'true') ||
(CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS_EXCEPT_STACK && CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS_EXCEPT_STACK.toLowerCase() === 'true')
) {
endOfLogCharacter = '';
}
return `${colorStripped}${endOfLogCharacter}`;

const { CONSOLE_LOG_COLORIZE } = process.env;

// Return string to be logged on the command line
// TODO: This is where the post processing happens
if (CONSOLE_LOG_COLORIZE && CONSOLE_LOG_COLORIZE.toLowerCase() === 'true') {
return `${colorJson(returnData)}${endOfLogCharacter}`;
} else {
const jsonString = stringify(returnData);
return `${jsonString}${endOfLogCharacter}`;
}
}

const print = w.format.printf((info: any) => {
Expand Down Expand Up @@ -459,19 +469,19 @@ function supressDetailsIfSelected(errorObject: ErrorWithContext | undefined) {
return undefined;
}

if (CONSOLE_LOG_JSON_NO_STACK_FOR_NON_ERROR) {
if (CONSOLE_LOG_JSON_NO_STACK_FOR_NON_ERROR && CONSOLE_LOG_JSON_NO_STACK_FOR_NON_ERROR.toLowerCase() === 'true') {
delete (errorObject as any)['@logCallStack'];
}

if (CONSOLE_LOG_JSON_NO_FILE_NAME) {
if (CONSOLE_LOG_JSON_NO_FILE_NAME && CONSOLE_LOG_JSON_NO_FILE_NAME.toLowerCase() === 'true') {
delete (errorObject as any)['@filename'];
}

if (CONSOLE_LOG_JSON_NO_PACKAGE_NAME) {
if (CONSOLE_LOG_JSON_NO_PACKAGE_NAME && CONSOLE_LOG_JSON_NO_PACKAGE_NAME.toLowerCase() === 'true') {
delete (errorObject as any)['@packageName'];
}

if (CONSOLE_LOG_JSON_NO_LOGGER_DEBUG) {
if (CONSOLE_LOG_JSON_NO_LOGGER_DEBUG && CONSOLE_LOG_JSON_NO_LOGGER_DEBUG.toLowerCase() === 'true') {
delete (errorObject as any)._loggerDebug;
}

Expand Down
2 changes: 1 addition & 1 deletion src/new-line-character.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export function NewLineCharacter() {
const { CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS } = process.env;
if (CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS) {
if (CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS && CONSOLE_LOG_JSON_NO_NEW_LINE_CHARACTERS.toLowerCase() === 'true') {
return ' - ';
} else {
return '\n';
Expand Down
Loading

0 comments on commit a9fe670

Please sign in to comment.