Skip to content

Commit

Permalink
refactor: remove global Logger instance (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
fvsch authored Nov 19, 2024
1 parent 7e29d77 commit aa46177
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 26 deletions.
25 changes: 15 additions & 10 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CLIArgs, parseArgs } from './args.js';
import { CLI_OPTIONS, HOSTS_LOCAL, HOSTS_WILDCARD } from './constants.js';
import { checkDirAccess, readPkgJson } from './fs-utils.js';
import { RequestHandler } from './handler.js';
import { color, logger, requestLogLine } from './logger.js';
import { color, Logger, requestLogLine } from './logger.js';
import { serverOptions } from './options.js';
import { FileResolver } from './resolver.js';
import type { OptionName, ServerOptions } from './types.d.ts';
Expand All @@ -18,6 +18,7 @@ import { clamp, errorList, getRuntime, isPrivateIPv4 } from './utils.js';
Start servitsy with configuration from command line arguments.
*/
export async function run() {
const logger = new Logger(process.stdout, process.stderr);
const args = new CLIArgs(argv.slice(2));

if (args.has('--version')) {
Expand All @@ -43,7 +44,7 @@ export async function run() {
return;
}

const cliServer = new CLIServer(options);
const cliServer = new CLIServer(options, logger);
cliServer.start();
}

Expand All @@ -53,8 +54,10 @@ export class CLIServer {
#portIterator: IterableIterator<number>;
#localNetworkInfo?: NetworkInterfaceInfo;
#server: Server;
#logger: Logger;

constructor(options: Required<ServerOptions>) {
constructor(options: Required<ServerOptions>, logger: Logger) {
this.#logger = logger;
this.#options = options;
this.#portIterator = new Set(options.ports).values();
this.#localNetworkInfo = Object.values(networkInterfaces())
Expand All @@ -65,14 +68,16 @@ export class CLIServer {
const server = createServer(async (req, res) => {
const handler = new RequestHandler({ req, res, resolver, options });
res.on('close', () => {
logger.write('request', requestLogLine(handler.data()));
this.#logger.write('request', requestLogLine(handler.data()));
});
await handler.process();
});
server.on('error', (error) => this.onError(error));
server.on('listening', () => {
const info = this.headerInfo();
if (info) logger.write('header', info, { top: 1, bottom: 1 });
if (info) {
this.#logger.write('header', info, { top: 1, bottom: 1 });
}
});

this.#server = server;
Expand Down Expand Up @@ -138,7 +143,7 @@ export class CLIServer {
this.shutdown();
} else if (!helpShown) {
helpShown = true;
logger.write('info', 'Hit Control+C or Escape to stop the server.');
this.#logger.write('info', 'Hit Control+C or Escape to stop the server.');
}
});
stdin.setRawMode(true);
Expand All @@ -159,7 +164,7 @@ export class CLIServer {
this.#shuttingDown = true;

process.exitCode = 0;
const promise = logger.write('info', 'Gracefully shutting down...');
const promise = this.#logger.write('info', 'Gracefully shutting down...');
this.#server.closeAllConnections();
this.#server.close();
await promise;
Expand All @@ -178,7 +183,7 @@ export class CLIServer {
} else {
const { ports } = this.#options;
const msg = `${ports.length > 1 ? 'ports' : 'port'} already in use: ${ports.join(', ')}`;
logger.error(msg);
this.#logger.error(msg);
exit(1);
}
});
Expand All @@ -187,9 +192,9 @@ export class CLIServer {

// Handle other errors
if (error.code === 'ENOTFOUND') {
logger.error(`host not found: '${error.hostname}'`);
this.#logger.error(`host not found: '${error.hostname}'`);
} else {
logger.error(error);
this.#logger.error(error);
}
exit(1);
}
Expand Down
30 changes: 15 additions & 15 deletions src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { release } from 'node:os';
import { platform } from 'node:process';
import { stderr, stdout } from 'node:process';
import { type Writable } from 'node:stream';
import { inspect } from 'node:util';

import type { ResMetaData } from './types.d.ts';
Expand Down Expand Up @@ -43,9 +43,14 @@ export class ColorUtils {
};
}

class Logger {
#lastout?: LogItem;
#lasterr?: LogItem;
export class Logger {
#out: { stream: Writable; last?: LogItem };
#err: { stream: Writable; last?: LogItem };

constructor(out: Writable, err?: Writable) {
this.#out = { stream: out };
this.#err = { stream: err ?? out };
}

async write(
group: LogItem['group'],
Expand All @@ -62,18 +67,14 @@ class Logger {
}

const { promise, resolve, reject } = withResolvers<void>();
const writeCallback = (err: Error | undefined) => {

const dest = group === 'error' ? this.#err : this.#out;
const text = this.#withPadding(dest.last, item);
dest.last = item;
dest.stream.write(text, (err) => {
if (err) reject(err);
else resolve();
};

if (group === 'error') {
stderr.write(this.#withPadding(this.#lasterr, item), writeCallback);
this.#lasterr = item;
} else {
stdout.write(this.#withPadding(this.#lastout, item), writeCallback);
this.#lastout = item;
}
});

return promise;
}
Expand Down Expand Up @@ -202,4 +203,3 @@ function supportsColor(): boolean {
}

export const color = new ColorUtils(supportsColor());
export const logger = new Logger();
90 changes: 89 additions & 1 deletion test/logger.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { type Buffer } from 'node:buffer';
import { Writable } from 'node:stream';
import { stripVTControlCharacters } from 'node:util';
import { expect, suite, test } from 'vitest';

import { ColorUtils, requestLogLine } from '#src/logger.js';
import { ColorUtils, Logger, requestLogLine } from '#src/logger.js';
import type { ResMetaData } from '#types';

suite('ColorUtils', () => {
Expand Down Expand Up @@ -43,6 +45,92 @@ suite('ColorUtils', () => {
});
});

suite('Logger', () => {
class TestWritable extends Writable {
rawContents = '';
contents = '';
_write(chunk: string | Buffer, encoding: string, cb?: (error?: Error | null) => void) {
const str = chunk.toString();
this.rawContents += str;
this.contents += stripVTControlCharacters(str);
if (typeof cb === 'function') cb();
}
}

const getLogger = () => {
const out = new TestWritable();
const err = new TestWritable();
const logger = new Logger(out, err);
return { out, err, logger };
};

test('Writes logs to their respective out and err streams', async () => {
const { out, err, logger } = getLogger();
await logger.write('info', 'Info 1');
await logger.write('error', 'Error 1');
await logger.write('info', 'Info 2');
await logger.write('error', 'Error 2');
expect(out.contents).toBe(`Info 1\nInfo 2\n`);
expect(err.contents).toBe(`Error 1\nError 2\n`);
});

test('Adds blank lines between logs of different groups', async () => {
const { out, logger } = getLogger();
await logger.write('header', 'Header');
await logger.write('request', 'Request 1');
await logger.write('request', 'Request 2');
await logger.write('info', 'Info 1');
await logger.write('request', 'Request 3');
await logger.write('request', 'Request 4');
await logger.write('request', 'Request 5');
await logger.write('info', 'Info 2');
expect(out.contents).toBe(`Header
Request 1
Request 2
Info 1
Request 3
Request 4
Request 5
Info 2
`);
});

test('accepts custom padding', async () => {
const { out, logger } = getLogger();
await logger.write('info', ['aaa', 'bbb', 'ccc'], {
// should yield two `\n`
top: 2,
// should yield three '\n' (return + two empty lines)
bottom: 2,
});
await logger.write('info', 'final', {
// merged with previous log's bottom padding
top: 2,
// capped at 4, resulting in 5 '\n'
bottom: 12,
});
expect(out.contents).toBe('\n\naaa\nbbb\nccc\n\n\nfinal\n\n\n\n\n');
});

test('error logs strings', async () => {
const { err, logger } = getLogger();
await logger.error('Error 1');
await logger.error('Error 2');
expect(err.contents).toBe(`servitsy: Error 1\nservitsy: Error 2\n`);
});

test('error logs Errors with stack trace', async () => {
const { err, logger } = getLogger();
await logger.error(new Error('Whoops'));
expect(err.contents).toMatch(`Error: Whoops`);
expect(err.contents).toMatch(/[\\\/]test[\\\/]logger\.test\.ts:\d+:\d+/);
});
});

suite('responseLogLine', () => {
const matchLogLine = (data: Omit<ResMetaData, 'url' | 'timing'>, expected: string) => {
const rawLine = requestLogLine({
Expand Down

0 comments on commit aa46177

Please sign in to comment.