From 5c95c9059e1137218d63d4d27c41d4cfdda45b4d Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Tue, 8 Oct 2024 12:37:05 +0200 Subject: [PATCH] Added pretty json print options to visualise better logs --- src/packages/dumbo/src/core/tracing/index.ts | 113 ++++++++++------ .../dumbo/src/core/tracing/printing/index.ts | 1 + .../dumbo/src/core/tracing/printing/pretty.ts | 76 +++++++++++ .../core/tracing/printing/pretty.unit.spec.ts | 128 ++++++++++++++++++ 4 files changed, 278 insertions(+), 40 deletions(-) create mode 100644 src/packages/dumbo/src/core/tracing/printing/index.ts create mode 100644 src/packages/dumbo/src/core/tracing/printing/pretty.ts create mode 100644 src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts diff --git a/src/packages/dumbo/src/core/tracing/index.ts b/src/packages/dumbo/src/core/tracing/index.ts index d814540..6f2848d 100644 --- a/src/packages/dumbo/src/core/tracing/index.ts +++ b/src/packages/dumbo/src/core/tracing/index.ts @@ -1,8 +1,14 @@ import { JSONSerializer } from '../serializer'; +import { prettyPrintJson } from './printing'; export const tracer = () => {}; export type LogLevel = 'DISABLED' | 'INFO' | 'LOG' | 'WARN' | 'ERROR'; + +export type LogType = 'CONSOLE'; + +export type LogStyle = 'RAW' | 'PRETTY'; + export const LogLevel = { DISABLED: 'DISABLED' as LogLevel, INFO: 'INFO' as LogLevel, @@ -39,52 +45,79 @@ const shouldLog = (logLevel: LogLevel): boolean => { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -tracer.log = (eventName: string, attributes?: Record) => { +type TraceEventRecorder = (message?: any, ...optionalParams: any[]) => void; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TraceEventFormatter = (event: any) => string; + +const nulloTraceEventRecorder: TraceEventRecorder = () => {}; + +const getTraceEventFormatter = + (logStyle: LogStyle): TraceEventFormatter => + (event) => { + switch (logStyle) { + case 'RAW': + return JSONSerializer.serialize(event); + case 'PRETTY': + return prettyPrintJson(event, true); + } + }; + +const getTraceEventRecorder = ( + logLevel: LogLevel, + logStyle: LogStyle, +): TraceEventRecorder => { + const format = getTraceEventFormatter(logStyle); + switch (logLevel) { + case 'DISABLED': + return nulloTraceEventRecorder; + case 'INFO': + return (event) => console.info(format(event)); + case 'LOG': + return (event) => console.log(format(event)); + case 'WARN': + return (event) => console.warn(format(event)); + case 'ERROR': + return (event) => console.error(format(event)); + } +}; + +const recordTraceEvent = ( + logLevel: LogLevel, + eventName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + attributes?: Record, +) => { if (!shouldLog(LogLevel.LOG)) return; - console.log( - JSONSerializer.serialize({ - name: eventName, - timestamp: new Date().getTime(), - ...attributes, - }), + const event = { + name: eventName, + timestamp: new Date().getTime(), + ...attributes, + }; + + const record = getTraceEventRecorder( + logLevel, + (process.env.DUMBO_LOG_STYLE as LogStyle | undefined) ?? 'RAW', ); + + record(event); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -tracer.warn = (eventName: string, attributes?: Record) => { - if (!shouldLog(LogLevel.WARN)) return; - - console.warn( - JSONSerializer.serialize({ - name: eventName, - timestamp: new Date().getTime(), - ...attributes, - }), - ); -}; +tracer.info = (eventName: string, attributes?: Record) => + recordTraceEvent(LogLevel.INFO, eventName, attributes); // eslint-disable-next-line @typescript-eslint/no-explicit-any -tracer.error = (eventName: string, attributes?: Record) => { - if (!shouldLog(LogLevel.ERROR)) return; - - console.error( - JSONSerializer.serialize({ - name: eventName, - timestamp: new Date().getTime(), - ...attributes, - }), - ); -}; +tracer.warn = (eventName: string, attributes?: Record) => + recordTraceEvent(LogLevel.WARN, eventName, attributes); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -tracer.info = (eventName: string, attributes?: Record) => { - if (!shouldLog(LogLevel.INFO)) return; - - console.info( - JSONSerializer.serialize({ - name: eventName, - timestamp: new Date().getTime(), - ...attributes, - }), - ); -}; +tracer.log = (eventName: string, attributes?: Record) => + recordTraceEvent(LogLevel.LOG, eventName, attributes); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +tracer.error = (eventName: string, attributes?: Record) => + recordTraceEvent(LogLevel.ERROR, eventName, attributes); + +export * from './printing'; diff --git a/src/packages/dumbo/src/core/tracing/printing/index.ts b/src/packages/dumbo/src/core/tracing/printing/index.ts new file mode 100644 index 0000000..70b161b --- /dev/null +++ b/src/packages/dumbo/src/core/tracing/printing/index.ts @@ -0,0 +1 @@ +export * from './pretty'; diff --git a/src/packages/dumbo/src/core/tracing/printing/pretty.ts b/src/packages/dumbo/src/core/tracing/printing/pretty.ts new file mode 100644 index 0000000..b3dcfeb --- /dev/null +++ b/src/packages/dumbo/src/core/tracing/printing/pretty.ts @@ -0,0 +1,76 @@ +import chalk from 'chalk'; + +const TWO_SPACES = ' '; + +const COLOR_STRING = chalk.hex('#98c379'); // Soft green for strings +const COLOR_KEY = chalk.hex('#61afef'); // Muted cyan for keys +const COLOR_NUMBER = chalk.hex('#d19a66'); // Light orange for numbers +const COLOR_BOOLEAN = chalk.hex('#c678dd'); // Light purple for booleans +const COLOR_NULL = chalk.hex('#c678dd'); // Light purple for null +const COLOR_BRACKETS = chalk.hex('#abb2bf'); // Soft white for object and array brackets + +const processString = ( + str: string, + indent: string, + handleMultiline: boolean, +): string => { + if (handleMultiline && str.includes('\n')) { + const lines = str.split('\n'); + const indentedLines = lines.map( + (line) => indent + TWO_SPACES + COLOR_STRING(line), + ); + return ( + COLOR_STRING('"') + + '\n' + + indentedLines.join('\n') + + '\n' + + indent + + COLOR_STRING('"') + ); + } + return COLOR_STRING(`"${str}"`); +}; + +// Function to format and colorize JSON by traversing it +const formatJson = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + obj: any, + indentLevel: number = 0, + handleMultiline: boolean = false, +): string => { + const indent = TWO_SPACES.repeat(indentLevel); + + if (obj === null) return COLOR_NULL('null'); + if (typeof obj === 'string') + return processString(obj, indent, handleMultiline); + if (typeof obj === 'number') return COLOR_NUMBER(String(obj)); + if (typeof obj === 'boolean') return COLOR_BOOLEAN(String(obj)); + + // Handle arrays + if (Array.isArray(obj)) { + const arrayItems = obj.map((item) => + formatJson(item, indentLevel + 1, handleMultiline), + ); + return `${COLOR_BRACKETS('[')}\n${indent} ${arrayItems.join( + `,\n${indent} `, + )}\n${indent}${COLOR_BRACKETS(']')}`; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const entries = Object.entries(obj).map( + ([key, value]) => + `${COLOR_KEY(`"${key}"`)}: ${formatJson( + value, + indentLevel + 1, + handleMultiline, + )}`, + ); + return `${COLOR_BRACKETS('{')}\n${indent} ${entries.join( + `,\n${indent} `, + )}\n${indent}${COLOR_BRACKETS('}')}`; +}; + +export const prettyPrintJson = ( + obj: unknown, + handleMultiline: boolean = false, +): string => formatJson(obj, 0, handleMultiline); diff --git a/src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts b/src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts new file mode 100644 index 0000000..a9cd273 --- /dev/null +++ b/src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts @@ -0,0 +1,128 @@ +import assert from 'assert'; +import chalk from 'chalk'; +import { describe, it } from 'node:test'; +import { prettyPrintJson } from './pretty'; + +// Define a basic test suite +void describe('prettyPrintJson', () => { + // Turn off chalk colorization during tests for easy comparison + chalk.level = 0; + + void it('formats a simple object correctly without multiline strings', () => { + const input = { + name: 'John Doe', + age: 30, + }; + + const expectedOutput = `{ + "name": "John Doe", + "age": 30 +}`; + + const output = prettyPrintJson(input, false); // Multiline handling off + assert.strictEqual(output, expectedOutput); + }); + + void it('formats a simple object with multiline string handling', () => { + const input = { + name: 'John Doe', + bio: 'This is line one.\nThis is line two.', + }; + + const expectedOutput = `{ + "name": "John Doe", + "bio": " + This is line one. + This is line two. + " +}`; + + const output = prettyPrintJson(input, true); // Multiline handling on + assert.strictEqual(output, expectedOutput); + }); + + void it('formats nested objects correctly', () => { + const input = { + user: { + name: 'Alice', + age: 25, + location: { + city: 'Wonderland', + country: 'Fiction', + }, + }, + }; + + const expectedOutput = `{ + "user": { + "name": "Alice", + "age": 25, + "location": { + "city": "Wonderland", + "country": "Fiction" + } + } +}`; + + const output = prettyPrintJson(input, false); // Multiline handling off + assert.strictEqual(output, expectedOutput); + }); + + void it('handles arrays and numbers correctly', () => { + const input = { + numbers: [1, 2, 3, 4, 5], + active: true, + }; + + const expectedOutput = `{ + "numbers": [ + 1, + 2, + 3, + 4, + 5 + ], + "active": true +}`; + + const output = prettyPrintJson(input, false); // Multiline handling off + assert.strictEqual(output, expectedOutput); + }); + + void it('formats an object with null values and booleans correctly', () => { + const input = { + name: 'Test', + isActive: false, + tags: null, + }; + + const expectedOutput = `{ + "name": "Test", + "isActive": false, + "tags": null +}`; + + const output = prettyPrintJson(input, false); // Multiline handling off + assert.strictEqual(output, expectedOutput); + }); + + void it('handles multiline SQL-like queries in strings', () => { + const input = { + query: + 'CREATE TABLE users (\n id INT PRIMARY KEY,\n name TEXT NOT NULL\n)', + }; + + const expectedOutput = `{ + "query": " + CREATE TABLE users ( + id INT PRIMARY KEY, + name TEXT NOT NULL + ) + " +}`; + + const output = prettyPrintJson(input, true); // Multiline handling on + console.log(output); + assert.strictEqual(output, expectedOutput); + }); +});