Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added pretty json print options to visualise better logs #92

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Added pretty json print options to visualise better logs
oskardudycz committed Oct 8, 2024
commit 02eea1aff4c728ba346fdaecdb518ab0253ae3b3
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,
@@ -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);
});
});