Skip to content
Merged
Show file tree
Hide file tree
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
5 changes: 4 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
{
"extends": "@aptoma/eslint-config",
"parserOptions": {
"ecmaVersion": 10
"ecmaVersion": 2022
},
"env": {
"node": true,
"mocha": true,
"es6": true
},
"rules": {
"valid-jsdoc": "off"
}
}
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on: [push]

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [22, 24]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ coverage
.nyc_output
config/config.json
config/config.test.json
package-lock.json
8 changes: 0 additions & 8 deletions .travis.yml

This file was deleted.

5 changes: 5 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ const internals = {
instances: {}
};

/**
* @param {string} [name]
* @param {import('./lib/logger').LoggerOptions} [opts]
* @returns {Logger}
*/
module.exports = function (name, opts) {
name = name || '_default';
if (typeof (name) === 'object') {
Expand Down
106 changes: 65 additions & 41 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use strict';

const Hoek = require('@hapi/hoek');
const printf = require('util').format;
const stringify = require('fast-safe-stringify');
const inspect = require('util').inspect;
Expand All @@ -17,8 +16,8 @@ const internals = {

/**
* Default meta data to append to logs
* @param {Object} request
* @return {Object|undefined}
* @param {HapiRequest} [request]
* @return {Record<string, unknown>|undefined}
*/
function defaultMeta(request) {
if (!request) {
Expand All @@ -29,37 +28,68 @@ function defaultMeta(request) {

/**
* defaultHandler for outputing the logs
* @return {Object}
* @return {object}
*/
function defaultHandler() {
return {
/**
* @param {string} str
*/
log(str) {
process.stdout.write(str + '\n');
}
};
}

/**
* @typedef {Object} RequestInfo
* @property {String} remoteAddress
* @property {String} host
* @property {String} path
* @property {String} method
* @property {Object} query
* @property {Number} statusCode
* @property {Number} responseTime
* @property {String} userAgent
* @property {String} [contentLength]
* @property {String} [referer]
* @typedef {object} RequestInfo
* @property {string} remoteAddress
* @property {string} host
* @property {string} path
* @property {string} method
* @property {object} query
* @property {number} statusCode
* @property {number} responseTime
* @property {string} [userAgent]
* @property {string} [contentLength]
* @property {string} [referer]
*/

/**
* @typedef {Object} LogData
* @property {Integer} timestamp when the log event occured
* @property {String[]} tags
* @property {Object|String} [log] the log message or object
* @typedef {object} HapiRequest - Hapi request object
* @property {object} request.info - Request information
* @property {string} request.info.id - Unique request identifier
* @property {string} request.info.remoteAddress - Client IP address
* @property {string} request.info.hostname - Request hostname
* @property {number} request.info.received - Timestamp when request was received
* @property {string} request.path - Request path
* @property {string} request.method - HTTP method
* @property {Record<string, unknown>} request.query - Query propertyeters
* @property {Record<string, string>} request.headers - Request headers
* @property {object} request.raw - Raw request/response objects
* @property {object} request.raw.req - Raw Node.js request object
* @property {Record<string, string>} request.raw.req.headers - Raw request headers
* @property {object} request.raw.res - Raw Node.js response object
* @property {number} request.raw.res.statusCode - HTTP status code
* @property {object} [request.response] - Hapi response object
* @property {Record<string, string>} [request.response.headers] - Response headers
*/

/**
* @typedef {object} LogData
* @property {number} timestamp when the log event occured
* @property {string[]} tags
* @property {object|string} [log] the log message or object
* @property {RequestInfo} [requestInfo]
* @property {Object} [data] optional meta data
* @property {object} [data] optional meta data
*/

/**
* @typedef {object} LoggerOptions
* @property {{log: (message: string) => void}} [handler] Handler implementing `log(message)` method, defaults to console logging
* @property {(request?: HapiRequest) => Record<string, unknown>} [meta] Should return meta data as an Object that is appended to logs. Default returns {requestId:request.info.id} This function doesnt always recieve request object.
* @property {(timestamp: number) => string} [formatTime]
* @property {boolean} [jsonOutput] output all data as stringified json
*/

class Logger {
Expand All @@ -68,23 +98,19 @@ class Logger {
* Creates a new Logger
*
* @class Logger
* @param {Object} [options]
* @param {Function} [options.formatTimestamp] function to format timestamp
* @param {Boolean} [options.jsonOutput] output all data as stringified json
* @param {Object} [options.handler] Handler implementing `log(message)` method, defaults to console logging
* @param {Function} [options.meta] Should return meta data as an Object that is appended to logs. Default returns {requestId:request.info.id} This function doesnt always recieve request object.
* @param {LoggerOptions} [options]
*/
constructor(options) {
this.opts = Hoek.applyToDefaults(internals.defaults, options || {});
this.opts = {...internals.defaults, ...options};
this.handler = this.opts.handler || defaultHandler();
this.meta = this.opts.meta || defaultMeta;
this.formatTime = this.opts.formatTimestamp;
}

/**
* Handle Error
* @param {Object} request
* @param {Object} data
* @param {HapiRequest} request
* @param {object} data
*/
handleError(request, data) {
this.write({
Expand All @@ -97,8 +123,8 @@ class Logger {

/**
* Handle 'request' event
* @param {Object} request
* @param {Object} event
* @param {HapiRequest} request
* @param {object} event
*/
handleRequest(request, event) {
this.write({
Expand All @@ -111,11 +137,11 @@ class Logger {

/**
* Handle 'response' event
* @param {Object} request
* @param {HapiRequest} request
*/
handleResponse(request) {
const referer = request.raw.req.headers.referer;
const contentLength = Hoek.reach(request, 'response.headers.content-length');
const contentLength = request.response?.headers?.['content-length'];

let remoteAddress = request.info.remoteAddress;
const xFF = request.headers['x-forwarded-for'];
Expand All @@ -124,6 +150,7 @@ class Logger {
remoteAddress = xFF.split(',')[0];
}

/** @type {RequestInfo} */
const requestInfo = {
remoteAddress,
host: request.info.hostname,
Expand Down Expand Up @@ -154,7 +181,7 @@ class Logger {
/**
* Format RequestInfo to a human friendly string
* @param {RequestInfo} data
* @return {String}
* @return {string}
*/
stringifyRequestInfo(data) {
return printf(
Expand All @@ -173,7 +200,7 @@ class Logger {

/**
* Handle 'log' event
* @param {Object} event
* @param {object} event
*/
handleLog(event) {
this.write({
Expand All @@ -186,7 +213,7 @@ class Logger {
/**
* Format log data to human readable string
* @param {LogData} obj
* @return {String}
* @return {string}
*/
humanReadableFormatter(obj) {
let log;
Expand Down Expand Up @@ -273,17 +300,14 @@ class Logger {
messages = messages[0];
}

const meta = this.meta();
const payload = {
timestamp: Date.now(),
tags: Array.isArray(tags) ? tags : [tags],
log: messages
log: messages,
...(meta && {data: meta})
};

const meta = this.meta();
if (meta) {
payload.data = meta;
}

this.write(payload);
}
}
Expand Down
56 changes: 38 additions & 18 deletions lib/pretty.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
#!/usr/bin/env node
'use strict';

const chalk = require('chalk');
const readline = require('readline');
const moment = require('moment');
const format = require('util').format;
const readline = require('node:readline');
const {styleText, format} = require('node:util');

const timeFormatter = new Intl.DateTimeFormat(undefined, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});

// eslint-disable-next-line complexity
rl.on('line', (line) => {
if (!line) {
return console.log();
Expand All @@ -32,12 +38,17 @@ rl.on('line', (line) => {
return console.log(line);
}

const time = moment(json._time).format('HH:mm:ss');
const time = timeFormatter.format(new Date(json._time));

const tags = json._tags || [];
const tagsStr = tags.length > 0 ? colorizeTags(tags) : '';
const msg = format(
`%s ${chalk.gray('[')}%s${chalk.gray(']')} ${chalk.gray('-')} %s %s`,
chalk.gray(time),
colorizeTags(json._tags),
'%s %s%s%s%s %s %s',
styleText('gray', time),
styleText('gray', '['),
tagsStr,
tagsStr ? '' : '', // no extra space if no tags
styleText('gray', '] -'),
json.msg || '',
props(json, ['msg', '_time', '_tags'])
);
Expand All @@ -62,27 +73,36 @@ function stringify(data) {
}

function colorizeTags(tags) {
let colors = [chalk.blue, chalk.cyan];
/** @type {'blue' | 'red' | 'yellow'} */
let color1 = 'blue';
/** @type {'cyan' | 'red' | 'yellow'} */
let color2 = 'cyan';

if (tags.find((tag) => tag === 'error')) {
colors = [chalk.red.bold, chalk.red];
color1 = 'red';
color2 = 'red';
} else if (tags.find((tag) => tag.startsWith('warn'))) {
colors = [chalk.yellow.bold, chalk.yellow];
color1 = 'yellow';
color2 = 'yellow';
}
return tags.map((t, idx) => idx % 2 === 0 ? colors[0](t) : colors[1](t)).join(', ');

return tags.map((t, idx) =>
idx % 2 === 0 ? styleText(color1, t) : styleText(color2, t)
).join(', ');
}

function formatResponse(json) {
const time = moment(json._time).format('HH:mm:ss');
const time = timeFormatter.format(new Date(json._time));

const statusColor = (json.statusCode / 100 | 0) > 3 ? chalk.red : chalk.green;
const statusColor = (json.statusCode / 100 | 0) > 3 ? 'red' : 'green';

const msg = format(
'%s %s %s %s %s %s ms',
chalk.gray(time),
statusColor(json.method),
styleText('gray', time),
styleText(statusColor, json.method),
json.path,
chalk.cyan(JSON.stringify(json.query)),
statusColor(json.statusCode),
styleText('cyan', JSON.stringify(json.query)),
styleText(statusColor, String(json.statusCode)),
json.responseTime
);

Expand Down
Loading