Skip to content

Commit 337279c

Browse files
authored
feat: add request logging (#716)
1 parent 0616ce3 commit 337279c

File tree

8 files changed

+89
-10
lines changed

8 files changed

+89
-10
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
"source/**/*.ts": [
8787
"eslint --max-warnings 0 --fix",
8888
"vitest related --run"
89+
],
90+
"tests": [
91+
"vitest --run"
8992
]
9093
}
9194
}

source/main.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,11 @@ for (const endpoint of args['--listen']) {
114114
let message = chalk.green('Serving!');
115115
if (local) {
116116
const prefix = network ? '- ' : '';
117-
const space = network ? ' ' : ' ';
117+
const space = network ? ' ' : ' ';
118118

119119
message += `\n\n${chalk.bold(`${prefix}Local:`)}${space}${local}`;
120120
}
121-
if (network) message += `\n${chalk.bold('- On Your Network:')} ${network}`;
121+
if (network) message += `\n${chalk.bold('- Network:')} ${network}`;
122122
if (previous)
123123
message += chalk.red(
124124
`\n\nThis port was picked because ${chalk.underline(

source/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export declare interface Options {
7575
'--single': boolean;
7676
'--debug': boolean;
7777
'--config': Path;
78+
'--no-request-logging': boolean;
7879
'--no-clipboard': boolean;
7980
'--no-compression': boolean;
8081
'--no-etag': boolean;

source/utilities/cli.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ const helpText = chalkTemplate`
3838
3939
-p Specify custom port
4040
41-
-d, --debug Show debugging information
42-
4341
-s, --single Rewrite all not-found requests to \`index.html\`
4442
43+
-d, --debug Show debugging information
44+
4545
-c, --config Specify custom path to \`serve.json\`
4646
47+
-L, --no-request-logging Do not log any request information to the console.
48+
4749
-C, --cors Enable CORS, sets \`Access-Control-Allow-Origin\` to \`*\`
4850
4951
-n, --no-clipboard Do not copy the local address to the clipboard

source/utilities/logger.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
import chalk from 'chalk';
77

8+
const http = (...message: string[]) =>
9+
console.info(chalk.bgBlue.bold(' HTTP '), ...message);
810
const info = (...message: string[]) =>
9-
console.error(chalk.bgMagenta.bold(' INFO '), ...message);
11+
console.info(chalk.bgMagenta.bold(' INFO '), ...message);
1012
const warn = (...message: string[]) =>
1113
console.error(chalk.bgYellow.bold(' WARN '), ...message);
1214
const error = (...message: string[]) =>
1315
console.error(chalk.bgRed.bold(' ERROR '), ...message);
1416
const log = console.log;
1517

16-
export const logger = { info, warn, error, log };
18+
export const logger = { http, info, warn, error, log };

source/utilities/server.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import { readFile } from 'node:fs/promises';
77
import handler from 'serve-handler';
88
import compression from 'compression';
99
import isPortReachable from 'is-port-reachable';
10+
import chalk from 'chalk';
1011
import { getNetworkAddress, registerCloseListener } from './http.js';
1112
import { promisify } from './promise.js';
13+
import { logger } from './logger.js';
1214
import type { IncomingMessage, ServerResponse } from 'node:http';
1315
import type { AddressInfo } from 'node:net';
1416
import type {
@@ -46,13 +48,37 @@ export const startServer = async (
4648
type ExpressRequest = Parameters<typeof compress>[0];
4749
type ExpressResponse = Parameters<typeof compress>[1];
4850

51+
// Log the request.
52+
const requestTime = new Date();
53+
const formattedTime = `${requestTime.toLocaleDateString()} ${requestTime.toLocaleTimeString()}`;
54+
const ipAddress =
55+
request.socket.remoteAddress?.replace('::ffff:', '') ?? 'unknown';
56+
const requestUrl = `${request.method ?? 'GET'} ${request.url ?? '/'}`;
57+
if (!args['--no-request-logging'])
58+
logger.http(
59+
chalk.dim(formattedTime),
60+
chalk.yellow(ipAddress),
61+
chalk.cyan(requestUrl),
62+
);
63+
4964
if (args['--cors'])
5065
response.setHeader('Access-Control-Allow-Origin', '*');
5166
if (!args['--no-compression'])
5267
await compress(request as ExpressRequest, response as ExpressResponse);
5368

5469
// Let the `serve-handler` module do the rest.
5570
await handler(request, response, config);
71+
72+
// Before returning the response, log the status code and time taken.
73+
const responseTime = Date.now() - requestTime.getTime();
74+
if (!args['--no-request-logging'])
75+
logger.http(
76+
chalk.dim(formattedTime),
77+
chalk.yellow(ipAddress),
78+
chalk[response.statusCode < 400 ? 'green' : 'red'](
79+
`Returned ${response.statusCode} in ${responseTime} ms`,
80+
),
81+
);
5682
};
5783

5884
// Then we run the async function, and re-throw any errors.

tests/__snapshots__/cli.test.ts.snap

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ exports[`utilities/cli > render help text 1`] = `
2727
2828
-p Specify custom port
2929
30-
-d, --debug Show debugging information
31-
3230
-s, --single Rewrite all not-found requests to \`index.html\`
3331
32+
-d, --debug Show debugging information
33+
3434
-c, --config Specify custom path to \`serve.json\`
3535
36+
-L, --no-request-logging Do not log any request information to the console.
37+
3638
-C, --cors Enable CORS, sets \`Access-Control-Allow-Origin\` to \`*\`
3739
3840
-n, --no-clipboard Do not copy the local address to the clipboard

tests/server.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// tests/config.test.ts
2-
// Tests for the configuration loader.
1+
// tests/server.test.ts
2+
// Tests for the server creating function.
33

44
import { afterEach, describe, test, expect, vi } from 'vitest';
55
import { extend as createFetch } from 'got';
66

77
import { loadConfiguration } from '../source/utilities/config.js';
88
import { startServer } from '../source/utilities/server.js';
9+
import { logger } from '../source/utilities/logger.js';
910

1011
// The path to the fixtures for this test file.
1112
const fixture = 'tests/__fixtures__/server/';
@@ -54,4 +55,46 @@ describe('utilities/server', () => {
5455
const response = await fetch(address.local!);
5556
expect(response.ok);
5657
});
58+
59+
// Make sure the server logs requests by default.
60+
test('log requests to the server by default', async () => {
61+
const consoleSpy = vi.spyOn(logger, 'http');
62+
const address = await startServer({ port: 3003, host: '::1' }, config, {});
63+
64+
const response = await fetch(address.local!);
65+
expect(response.ok);
66+
67+
expect(consoleSpy).toBeCalledTimes(2);
68+
69+
const requestLog = consoleSpy.mock.calls[0].join(' ');
70+
const responseLog = consoleSpy.mock.calls[1].join(' ');
71+
72+
const time = new Date();
73+
const formattedTime = `${time.toLocaleDateString()} ${time.toLocaleTimeString()}`;
74+
const ip = '::1';
75+
const requestString = 'GET /';
76+
const status = 200;
77+
78+
expect(requestLog).toMatch(
79+
new RegExp(`${formattedTime}.*${ip}.*${requestString}`),
80+
);
81+
expect(responseLog).toMatch(
82+
new RegExp(
83+
`${formattedTime}.*${ip}.*Returned ${status} in [0-9][0-9]? ms`,
84+
),
85+
);
86+
});
87+
88+
// Make sure the server logs requests by default.
89+
test('log requests to the server by default', async () => {
90+
const consoleSpy = vi.spyOn(logger, 'http');
91+
const address = await startServer({ port: 3004 }, config, {
92+
'--no-request-logging': true,
93+
});
94+
95+
const response = await fetch(address.local!);
96+
expect(response.ok);
97+
98+
expect(consoleSpy).not.toHaveBeenCalled();
99+
});
57100
});

0 commit comments

Comments
 (0)