Skip to content

Commit

Permalink
Browser transport
Browse files Browse the repository at this point in the history
  • Loading branch information
shuritch committed Mar 21, 2024
1 parent 518ab8b commit f60f806
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 4 deletions.
20 changes: 20 additions & 0 deletions modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ Transport logs to the console.
| **pretty** | Pretty function | **log=>log+'\n'** |
| **stdout** | Output interface with method write | **process.stdout** |

### Browser API

Transport logs to browser console.

> [!TIP]
>
> By default, in the browser, transport uses corresponding
> [Log4j](https://en.wikipedia.org/wiki/Log4j) console methods (`error`, `warn`, `info`, `debug`,
> `trace`) and uses console.error for any fatal level logs. Other levels will be translated to `log`
> by default. You can change this behavior by passing `pipe` option.
#### Options

| Option | Description | Default |
| :--------: | ----------------------------------------------------------------------- | :----------------------------------------------------: |
| **locale** | Intl time formatter locale | **undefined** |
| **colors** | Css for methods | **{ ..., info: 'color: blue' }** |
| **pipe** | Pipes logs from Logger to destination, `(log: string, logObj) => void` | **(log, lvl) => console[lvl](format)** |
| **format** | Pipes logs from Logger to destination, `(log: string, logObj) => any[]` | **({lvl,msg,time})=>[`[${time}]%c${lvl}:`,color,msg]** |

## Custom transport

To create custom transport you only need to follow contract:
Expand Down
24 changes: 24 additions & 0 deletions modules/browser/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

const DEFAULT_COLOR = '#6666ff';
const DEFAULT_METHOD = console.log;
const DEFAULT_BIND = { method: DEFAULT_METHOD, color: DEFAULT_COLOR };
const { format: timeFormat } = new Intl.DateTimeFormat(); //? Bro language
module.exports = Browser;

// prettier-ignore
const BINDINGS = {
info: { method: console.info ?? DEFAULT_METHOD, color: '#659AD2' },
warn: { method: console.warn ?? DEFAULT_METHOD, color: '#F9C749' },
debug: { method: console.debug ?? DEFAULT_METHOD, color: '#9AA2AA' },
error: { method: console.error ?? DEFAULT_METHOD, color: '#EC3D47' },
trace: { method: console.trace ?? DEFAULT_METHOD, color: '#F9C749' },
fatal: { method: console.error ?? DEFAULT_METHOD, color: '#EC3D47' },
};

function Browser() {}
Browser.prototype.write = function (_, { lvl, time, msg = '', ...other }) {
const { color, method } = BINDINGS[lvl] ?? DEFAULT_BIND;
const format = `[${timeFormat(time)}] %c${lvl.toUpperCase()}:`;
method(format, color, msg, JSON.stringify(other, null, 2).slice(1, -1));
};
2 changes: 2 additions & 0 deletions modules/console/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

module.exports = Console;

const INVALID_OPTIONS = 'Invalid type of options parameter, must be an object';
const kOptions = Symbol('nalogy:console:options');
const OPTIONS = {
pretty: log => log + '\n',
stdout: process.stdout,
};

function Console(options = {}) {
if (options === null || typeof options !== 'object') throw new Error(INVALID_OPTIONS);
this[kOptions] = { ...OPTIONS, ...options };
}

Expand Down
23 changes: 23 additions & 0 deletions modules/filesystem/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

const { join } = require('path');

module.exports = {
INVALID_OPTIONS: 'Invalid type of options parameter, must be an object',
INVALID_DIRECTORY: 'Can not create directory: ',
ROTATION_ERROR: 'Error wile rotating the file: ',
STREAM_ERROR: 'Log file is not writable: ',

OPTIONS: {
path: join(__dirname, './logs'),
bufferSize: 64 * 1_024,
writeInterval: 3_000,
silence: false,
locale: 'af',
keep: 3,
},

STREAM_OPTIONS: { flags: 'a' },
DAY_IN_MS: 1000 * 60 * 60 * 24,
STAB_FUNCTION: () => false,
};
119 changes: 119 additions & 0 deletions modules/filesystem/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use strict';

const { INVALID_OPTIONS, INVALID_DIRECTORY, STREAM_ERROR, ROTATION_ERROR } = require('./config');
const { OPTIONS, STREAM_OPTIONS, DAY_IN_MS, STAB_FUNCTION } = require('./config');

const { createWriteStream, promises: fsp } = require('node:fs');
const { EventEmitter } = require('node:events');
const { stat, mkdir, readdir, unlink } = fsp;
const { join } = require('node:path');

const PROMISE_TO_BOOL = [() => true, () => false];

module.exports = class FSLogger extends EventEmitter {
#buffer = { length: 0, store: [] };
#active = false;
#options = null;
#stream = null;
#lock = null;
#location = '';
#file = '';

#switchTimer = null;
#flushTimer = null;

//? Only this method can throw exceptions
constructor(options = {}) {
super();
if (options === null || typeof options !== 'object') throw new Error(INVALID_OPTIONS);
this.#options = { ...OPTIONS, ...options };
if (!this.#options.silence) return;
const emit = this.emit.bind(this);
this.emit = (event, ...args) => {
if (event === 'error') return this;
return emit(event, ...args);
};
}

async start() {
if (this.#active) return this;
const path = this.#options.path;
var isExist = await stat(path)
.then(v => v.isDirectory())
.catch(STAB_FUNCTION);

if (!isExist) isExist = await mkdir(path).then(...PROMISE_TO_BOOL);
if (!isExist) throw new Error(INVALID_DIRECTORY + path);
await this.#open();
return this;
}

async finish() {
if (!(this.#active && this.#stream)) return;
if (this.#stream.destroyed || this.#stream.closed) return;
await this.#flush();
this.#active = false;
this.#switchTimer = (clearTimeout(this.#switchTimer), null);
this.#flushTimer = (clearTimeout(this.#flushTimer), null);
await stat(this.#location)
.then(({ size }) => !size && unlink(this.#location).catch(STAB_FUNCTION))
.catch(STAB_FUNCTION);
}

write(log) {
this.#buffer.store.push(Buffer.from(log + '\n'));
this.#buffer.length += log.length;
this.#buffer.length > this.#options.bufferSize && this.flush();
}

async #open() {
this.#file = new Date().toLocaleDateString(this.#options.locale) + '.log';
this.#location = join(this.#options.path, this.#file);
await this.#rotate();

const error = await new Promise(resolve => {
const signal = () => resolve(STREAM_ERROR + this.#file);
this.#stream = createWriteStream(this.#location, STREAM_OPTIONS);
this.#stream.on('error', () => this.emit(STREAM_ERROR + this.#file));
this.#stream.once('open', () => {
this.#stream.off('error', signal);
resolve();
});
});

if (error) return void this.#stream.destroy();
const today = new Date();
const tm = -today.getTime() + today.setUTCHours(0, 0, 0, 0) + DAY_IN_MS;

this.#active = true;
this.#flushTimer = setInterval(() => this.#flush(), this.#options.writeInterval);
this.#switchTimer = setTimeout(() => this.finish().then(() => this.#open()), tm);
}

async #flush() {
if (this.#lock) await this.#lock;
if (!this.#buffer.length) return Promise.resolve();
const buffer = Buffer.concat(this.#buffer.store);
this.#buffer.store.length = this.#buffer.length = 0;
this.#lock = new Promise(res => void this.#stream.write(buffer, res));
return this.#lock;
}

async #rotate() {
if (!this.#options.keep) return;
const promises = [];
try {
var today = new Date().getTime();
var files = await readdir(this.#options.path);
for (var name of files) {
if (!name.endsWith('.log')) continue;
var date = new Date(name.substring(0, name.lastIndexOf('.'))).getTime();
if ((today - date) / DAY_IN_MS < this.#options.keep) continue;
promises.push(unlink(join(this.#options.path, name)));
}
await Promise.all(promises);
} catch (err) {
this.emit('error', ROTATION_ERROR + err);
}
}
};
5 changes: 3 additions & 2 deletions modules/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

module.exports = {
FSLogger: require('./fslogger'),
Console: require('./console'),
'nalogy/filesystem': require('./filesystem'),
'nalogy/console': require('./console'),
'nalogy/browser': require('./browser'),
};
11 changes: 10 additions & 1 deletion modules/tests/console.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const Console = require('../console');
const Console = require('..')['nalogy/console'];
// const Browser = require('..')['nalogy/browser'];
const assert = require('node:assert');
const test = require('node:test');

Expand All @@ -19,3 +20,11 @@ test('Console transport', () => {
assert.strictEqual(typeof transport.write, 'function');
transport.write('hello world');
});

// test('Browser transport', () => {
// const transport = new Browser();
// assert.strictEqual(typeof transport.write, 'function');
// transport.write(undefined, { time: new Date(), msg: 'hello world', lvl: 'info' });
// transport.write(undefined, { time: new Date(), msg: 'hello world', lvl: 'debug' });
// transport.write(undefined, { time: new Date(), msg: 'hello world', lvl: 'warn' });
// });
2 changes: 1 addition & 1 deletion modules/tests/fslogger.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const { access, constants, stat, rm, readFile } = require('node:fs').promises;
const FSLogger = require('../fslogger');
const FSLogger = require('..')['nalogy/filesystem'];
const assert = require('node:assert');
const { join } = require('node:path');
const test = require('node:test');
Expand Down

0 comments on commit f60f806

Please sign in to comment.