Skip to content

Commit

Permalink
Added pretty json print options to visualise better logs
Browse files Browse the repository at this point in the history
  • Loading branch information
oskardudycz committed Oct 8, 2024
1 parent 7ca0e8d commit 5c95c90
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 40 deletions.
113 changes: 73 additions & 40 deletions src/packages/dumbo/src/core/tracing/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -39,52 +45,79 @@ const shouldLog = (logLevel: LogLevel): boolean => {
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
tracer.log = (eventName: string, attributes?: Record<string, any>) => {
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<string, any>,
) => {
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<string, any>) => {
if (!shouldLog(LogLevel.WARN)) return;

console.warn(
JSONSerializer.serialize({
name: eventName,
timestamp: new Date().getTime(),
...attributes,
}),
);
};
tracer.info = (eventName: string, attributes?: Record<string, any>) =>
recordTraceEvent(LogLevel.INFO, eventName, attributes);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
tracer.error = (eventName: string, attributes?: Record<string, any>) => {
if (!shouldLog(LogLevel.ERROR)) return;

console.error(
JSONSerializer.serialize({
name: eventName,
timestamp: new Date().getTime(),
...attributes,
}),
);
};
tracer.warn = (eventName: string, attributes?: Record<string, any>) =>
recordTraceEvent(LogLevel.WARN, eventName, attributes);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
tracer.info = (eventName: string, attributes?: Record<string, any>) => {
if (!shouldLog(LogLevel.INFO)) return;

console.info(
JSONSerializer.serialize({
name: eventName,
timestamp: new Date().getTime(),
...attributes,
}),
);
};
tracer.log = (eventName: string, attributes?: Record<string, any>) =>
recordTraceEvent(LogLevel.LOG, eventName, attributes);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
tracer.error = (eventName: string, attributes?: Record<string, any>) =>
recordTraceEvent(LogLevel.ERROR, eventName, attributes);

export * from './printing';
1 change: 1 addition & 0 deletions src/packages/dumbo/src/core/tracing/printing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './pretty';
76 changes: 76 additions & 0 deletions src/packages/dumbo/src/core/tracing/printing/pretty.ts
Original file line number Diff line number Diff line change
@@ -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);
128 changes: 128 additions & 0 deletions src/packages/dumbo/src/core/tracing/printing/pretty.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 5c95c90

Please sign in to comment.