@stonyx/logs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,278 @@
1
+ <h1 align="center">
2
+ <br>
3
+ <br>
4
+ <img width="560" src="https://github.com/abofs/chronicle/raw/master/media/chronicle-logo.svg" alt="Chronicle">
5
+ <br>
6
+ <br>
7
+ <br>
8
+ </h1>
9
+
10
+ > Simplified logging for node applications
11
+
12
+ ![](https://github.com/abofs/chronicle/raw/master/media/screenshot.jpg)
13
+
14
+ <br>
15
+
16
+ ---
17
+
18
+ <div align="center">
19
+ <p>
20
+ <p>
21
+ <sup>
22
+ If our projects are useful to you, please consider becoming a <a href="https://github.com/sponsors/abofs">GitHub Sponsor</a>
23
+ </sup>
24
+ </p>
25
+ </p>
26
+ </div>
27
+
28
+ ---
29
+
30
+ **Chronicle** is built on top of all the great work done by "Sindre Sorhus" and other collaborators of the [chalk](https://www.npmjs.com/package/chalk) project.
31
+ This project is not directly associated with chalk other than chalk being a core dependency of **Chronicle**.
32
+
33
+ **IMPORTANT**: Please note that although **Chronicle** can be configured to any color through chalk, your output is subject to your terminal's color limitations.
34
+
35
+ ## Highlights
36
+
37
+ - Fully configurable
38
+ - Simple and Expressive API
39
+ - Highly performant
40
+
41
+ ## Install
42
+
43
+ ```sh
44
+ npm install node-chronicle
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ```js
50
+ import Chronicle from 'node-chronicle';
51
+
52
+ const chronicle = new Chronicle();
53
+
54
+ chronicle.info('Info: sample application has started');
55
+ chronicle.warn('Warning: this is just a sample');
56
+ chronicle.error('Error: no application logic detected', true); // logs to logs/error.log file
57
+ ```
58
+
59
+ Easily define your own logging mechanism and color-coding preference:
60
+
61
+ ```js
62
+ import Chronicle from 'node-chronicle';
63
+
64
+ const chronicle = new Chronicle({
65
+ systemLogs: {
66
+ blue: '#007cae', // indigo blue
67
+ yellow: '#ae8f00', // bright orange
68
+ red: 'red',
69
+ },
70
+ });
71
+
72
+ chronicle.blue('Info: using custom method blue, sample application has started');
73
+ chronicle.yellow('Warning: using custom method yellow, this is just a sample');
74
+ chronicle.red('Error: using custom method red, no application logic detected', false);
75
+ ```
76
+
77
+ Customize logging options to best suit your project
78
+
79
+ ```js
80
+ import Chronicle from 'node-chronicle';
81
+
82
+ const chronicle = new Chronicle({
83
+ logToFileByDefault: true,
84
+ logTimestamp: true,
85
+ path: 'custom-logs', // <project root>/custom-logs/*.log
86
+ prefix: '--------------------------------------------------------------- \n',
87
+ suffix: '\n=============================================================== \n',
88
+ });
89
+
90
+ chronicle.info('Info: sample application has started');
91
+ ```
92
+ ![](https://github.com/abofs/chronicle/raw/master/media/examples/custom-options.jpg)
93
+
94
+
95
+ Add additional log types extending the default options of "info", "warn", "error" and "debug"
96
+
97
+ ```js
98
+ import Chronicle from '../source/index.js';
99
+
100
+ const chronicle = new Chronicle({ additionalLogs: { question: 'green' } });
101
+
102
+ // create additional log with direct chalk configuration
103
+ chronicle.defineType('query', chronicle.chalk().black.bgGreen);
104
+
105
+ chronicle.question('What will a fully custom chalk color function look like?');
106
+ await chronicle.query('This is what a custom chalk color setting looks like', true);
107
+ ```
108
+ ![](https://github.com/abofs/chronicle/raw/master/media/examples/additional-logs.jpg)
109
+
110
+ ## API
111
+
112
+ ### Defining Logs & Colors
113
+
114
+ By default, **Chronicle** is instantiated with the following options:
115
+
116
+ ```js
117
+ additionalLogs: {},
118
+ systemLogs: {
119
+ info: 'cyan',
120
+ warn: 'yellow',
121
+ error: 'red',
122
+ },
123
+ ```
124
+
125
+ You can add to a new log/color setting by passing the `additionalLogs` option to the **Chronicle** constructor. Any setting that already exists in `systemLogs` will be replaced, otherwise they will be added.
126
+
127
+ ```js
128
+ const chronicle = new Chronicle({ additionalLogs: { info: 'green', custom: 'cyan' } });
129
+
130
+ // output configuration:
131
+ {
132
+ info: 'green',
133
+ warn: 'yellow',
134
+ error: 'red',
135
+ custom: 'cyan'
136
+ }
137
+ ```
138
+
139
+ **Chronicle** will generate convenience methods for all keys provided, with the corresponding color settings. The example above would create the following convenience methods, for logging:
140
+
141
+ ```js
142
+ chronicle.info() // green output
143
+ chronicle.warn() // yellow output
144
+ chronicle.error() // red output
145
+ chronicle.custom() // cyan output
146
+ ```
147
+
148
+ These methods can then be called in your application with [logging parameters](#logging-parameters).
149
+
150
+ Color settings are handled by determining whether your input is a color name or a hex value (prefixed with **#**). For example, passing `red` as a color setting will utilize `chalk.red`, while passing `#ff0000` would use `chalk.hex('#ff0000')` instead. A [list of available colors](https://github.com/chalk/chalk#colors) can be found in chalks' documentation.
151
+
152
+ Additionally, these methods return a promise when `logToFile` is true, allowing you use them with `await` in an async method, or append `then(), catch(), or finally()` for more advanced callback usage.
153
+
154
+ ```js
155
+ async method() {
156
+ await chronicle.error('error message', true);
157
+
158
+ // do something after logs/error.log (default) is created
159
+ }
160
+ ```
161
+
162
+ ### The Debug Method
163
+
164
+ **Chronicle** allows for the `chronicle.debug()` method to be overridden by a color setting. However, by default we do not define a color for debug and debug is handled differently. For console logging, all **debug** does is output the following:
165
+
166
+ ```js
167
+ // For logging to console:
168
+ console.dir(content);
169
+
170
+ // For writing to file:
171
+ JSON.stringify(content, null, 2);
172
+ ```
173
+
174
+ We believe that when wanting to output complicated objects or debug **typescript** applications, there are better methods than utilizing this **Chronicle** package. But for anyone who's fully incorporated **Chronicle** into their project, this function offers some convenience.
175
+
176
+ ### Logging Parameters
177
+
178
+ ```js
179
+ chronicle.error('error message', true, false); // content, logToFile, overwrite
180
+ ```
181
+
182
+ | Parameter | Type | Default | Description |
183
+ | :---: | :---: | :---: | :--- |
184
+ | `content` | **String** | | Content of log that will output on your console. |
185
+ | `logToFile` | **Boolean** | *false* | Option to log content to file. |
186
+ | `overwrite` | **Boolean** | *false <br> (true on debug())* | Option to overwrite log file, rather than append to it. This option is redundant if logToFile is false. |
187
+
188
+ **logToFile** will log to *<project-root>/logs* unless [configured](#configuration) differently during instantiation. <br>
189
+ ### Configuration
190
+
191
+ When instantiating **Chronicle**, you can pass an object to customize your settings. Below is the default configuration:
192
+
193
+ ```js
194
+ const chronicle = new Chronicle({
195
+ logToFileByDefault: false,
196
+ logTimestamp: false,
197
+ path: 'logs/',
198
+ prefix: '',
199
+ suffix: '',
200
+ additionalLogs: {},
201
+ systemLogs: {
202
+ info: 'cyan',
203
+ warn: 'yellow',
204
+ error: 'red',
205
+ }
206
+ });
207
+ ```
208
+
209
+ | Option | Type | Default | Description |
210
+ | :---: | :---: | :---: | :--- |
211
+ | `logToFileByDefault` | **Boolean** | *false* | Option to change default setting for `logToFile` parameter of logging functions. |
212
+ | `logTimestamp` | **Boolean** | *false* | Option to include timestamp in console logging. Timestamps are automatically included in file logs. |
213
+ | `path` | **String** | *'logs/'* | Path in which to store log files. This setting is relative to your project's root directory. |
214
+ | `prefix` | **String** | *''* | Prefix string to prepend all log messages for all log types with the exception of *debug*. |
215
+ | `suffix` | **String** | *''* | Suffix string to tack on to all log messages for all log types with the exception of *debug*. |
216
+ | `additionalLogs` | **Object** | | Key value pair object containing log type to color setting for logs that will be merged with `systemLogs` |
217
+ | `systemLogs` | **Object** | | Key value pair object containing log type to color setting for main **Chronicle** logs available in application |
218
+
219
+ `additionalLogs` and `systemLogs` are explained with more detail in the [defining logs and colors](#defining-logs) section.
220
+
221
+ ### Advanced Configuration
222
+
223
+ You may want to do more than just pick a basic color for your output. **chalk** offers a variety of different options, and can be configured via `defineType()`. **Chronicle** exposes the chalk instance via `chalk()` so that you don't have to import **chalk** directly into your project. Here is an example of how you can use this method to fully customize your log color setting:
224
+
225
+ ```js
226
+ const chronicle = new Chronicle();
227
+
228
+ chronicle.defineType('critical', chronicle.chalk().bold.red);
229
+ chronicle.critical('This is a critical error');
230
+ ```
231
+
232
+ Additionally, any [configuration](#configuration) that can be set during instantiation, can also be applied exclusively to any given type by passing in a third **options** parameter.
233
+
234
+ ```js
235
+ // params: type, setting, options
236
+ chronicle.definetype('notice', '#c0c0c0', {
237
+ prefix: '--------------------------------------------------------------- \n',
238
+ suffix: '\n=============================================================== \n'
239
+ });
240
+ ```
241
+
242
+ ### defineType() params
243
+ | Parameter | Type | Description |
244
+ | :---: | :---: | :--- |
245
+ | `type` | **String** | Create or overwrites a logging function for the given type. |
246
+ | `setting` | **String or Function** | Color setting or chalk function |
247
+ | `options` | **Object** | Configure any setting only to the given type rather than globally. See [configuration](#configuration) for list of options |
248
+
249
+
250
+ ```js
251
+ const chronicle = new Chronicle();
252
+
253
+ chronicle.defineType('info', chronicle.chalk().black.bgCyan);
254
+ chronicle.defineType('critical', chronicle.chalk().bold.red);
255
+ chronicle.defineType('dialog', 'magentaBright');
256
+ chronicle.definetype('notice', '#c0c0c0', {
257
+ prefix: '--------------------------------------------------------------- \n',
258
+ suffix: '\n=============================================================== \n'
259
+ });
260
+
261
+ chronicle.info('This pre-existing log now has a cyan background and black foreground');
262
+ chronicle.critical('This new log is bold and red');
263
+ chronicle.dialog('This new dialog is bright magenta');
264
+ chronicle.notice('This new log is the hex "#c0c0c0" share of gray');
265
+ ```
266
+
267
+ `defineType()` can also be used as an alternative to populating the `additionalLogs` setting in the constructor, as if the setting doesn't already exist, it will then be created.
268
+
269
+ ## Origin
270
+
271
+ As a team of developers who are constantly working on side projects, we often litter our codebase with TODOs to refactor convenience utils such as **chronicle** into classes of their own, or projects of their own. This usually turns into internal tech debt that never gets addressed. Furthermore, we also often find ourselves going the *copy -> paste -> modify* route of previously written useful logic, which saves us time in new projects, but not as much as it would if all we had to do was run an `npm install` instead.
272
+
273
+ With that in mind, we are proud to release **chronicle** as an open source package, in hopes others will find this just as useful as we do in their own projects.
274
+
275
+ ## Maintainers
276
+
277
+ - [Stone Costa](https://github.com/mstonepc)
278
+ - [Daniel DeLima](https://github.com/danieldtech)
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@stonyx/logs",
3
+ "version": "1.0.0",
4
+ "description": "Simplified logging for node applications",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./color": "./src/color.js"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/abofs/stonyx-logs.git"
17
+ },
18
+ "keywords": [
19
+ "log",
20
+ "logging",
21
+ "color-coding",
22
+ "chronicle",
23
+ "history",
24
+ "documentation",
25
+ "document",
26
+ "chalk",
27
+ "clean-output",
28
+ "terminal",
29
+ "debug"
30
+ ],
31
+ "author": "Stone Costa",
32
+ "license": "ISC",
33
+ "bugs": {
34
+ "url": "https://github.com/abofs/stonyx-logs/issues"
35
+ },
36
+ "homepage": "https://github.com/abofs/stonyx-logs#readme",
37
+ "dependencies": {
38
+ "chalk": "^5.3.0"
39
+ },
40
+ "devDependencies": {
41
+ "eslint": "^8.27.0",
42
+ "eslint-plugin-node": "^11.1.0",
43
+ "qunit": "^2.19.3",
44
+ "sinon": "^17.0.0"
45
+ },
46
+ "scripts": {
47
+ "test": "qunit 'test/unit/**/*-test.js'",
48
+ "lint": "eslint . --fix"
49
+ }
50
+ }
package/src/color.js ADDED
@@ -0,0 +1,50 @@
1
+ import chalk from 'chalk';
2
+
3
+ export default class Color {
4
+ constructor() {
5
+ this.types = [];
6
+ }
7
+
8
+ // retrieves configured color function for log type
9
+ getLogColor(type) {
10
+ return this.types[type];
11
+ }
12
+
13
+ getChalkInstance() {
14
+ return chalk;
15
+ }
16
+
17
+ setLogColor(type, setting) {
18
+ const chalkColorFunction = this.settingToChalkColorFunction(setting);
19
+ this.types[type] = chalkColorFunction;
20
+ }
21
+
22
+ // retrieves chalk color function, and fully validates output
23
+ settingToChalkColorFunction(setting) {
24
+ const errorMessage = 'Invalid chalk color function.'
25
+ + 'For help with color settings, see https://github.com/abofs/chronicle#defining-logs--colors';
26
+
27
+ switch (typeof setting) {
28
+ case 'string':
29
+ const chalkColorFunction = (setting[0] === '#') ? chalk.hex(setting) : chalk[setting];
30
+ if (!chalkColorFunction
31
+ || typeof chalkColorFunction !== 'function'
32
+ || typeof chalkColorFunction('') !== 'string') {
33
+ throw errorMessage;
34
+ }
35
+
36
+ return chalkColorFunction;
37
+
38
+ case 'function':
39
+ // validate that given function returns a string
40
+ if (typeof setting('') !== 'string') {
41
+ throw errorMessage;
42
+ }
43
+
44
+ return setting;
45
+
46
+ default:
47
+ throw errorMessage;
48
+ }
49
+ }
50
+ }
package/src/index.js ADDED
@@ -0,0 +1,172 @@
1
+ import { mkdirSync, promises as fsp } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import projectPath from 'path';
4
+ import Color from './color.js';
5
+
6
+ const defaultOptions = {
7
+ logToFileByDefault: false, // default setting (overridable by logToFile param)
8
+ logTimestamp: false, // option to include timestamp in console logs
9
+ path: 'logs/', // default log directory (relative to main project root directory)
10
+ prefix: '',
11
+ suffix: '',
12
+
13
+ // log types with corresponding color settings
14
+ additionalLogs: {},
15
+ systemLogs: {
16
+ info: 'cyan',
17
+ warn: 'yellow',
18
+ error: 'red',
19
+ },
20
+ };
21
+
22
+ // used to sanitize defineType() options input
23
+ const optionKeys = Object.keys(defaultOptions);
24
+
25
+ export default class Chronicle {
26
+ constructor(options = defaultOptions) {
27
+ options = {
28
+ ...defaultOptions,
29
+ ...options,
30
+ };
31
+ this.options = options;
32
+ this.options.path = this.sanitizePath(this.options.path);
33
+
34
+ const { additionalLogs, systemLogs } = options;
35
+ const logs = {
36
+ ...systemLogs,
37
+ ...additionalLogs,
38
+ };
39
+
40
+ this.color = new Color();
41
+ this.typeOptions = [];
42
+
43
+ // create direct convenience methods for logging
44
+ for (const type of Object.keys(logs)) {
45
+ this.defineType(type, logs[type]);
46
+ }
47
+ }
48
+
49
+ // records setting and options for log type, and crates convenience method ie: chronicle.info()
50
+ defineType(type, setting, options = null) {
51
+ this.color.setLogColor(type, setting);
52
+
53
+ // create convenience method if it doesn't exist
54
+ if (!this[type]) this.createConvenienceMethod(type);
55
+
56
+ if (!options) return;
57
+ if (typeof options !== 'object') throw 'The options param must be an object.';
58
+
59
+ for (let option of Object.keys(options)) {
60
+ if (!optionKeys.includes(option)) {
61
+ throw `${option} is not a valid configuration object.`
62
+ + '\n For a list of available options, see https://github.com/abofs/chronicle#configuration';
63
+ }
64
+
65
+ // sanitize path input
66
+ if (option === 'path') options[option] = this.sanitizePath(options[option]);
67
+ }
68
+
69
+ this.typeOptions[type] = options;
70
+ }
71
+
72
+ // proxy through `logAction` method in order to set defaults based on argument presence
73
+ createConvenienceMethod(type) {
74
+ this[type] = (content, logToFile, overwrite = false) =>
75
+ this.logAction(type, content, logToFile, overwrite);
76
+ }
77
+
78
+ // validates params and sets configuration-based defaults for logging
79
+ logAction(type, content, logToFile, overwrite) {
80
+ // set logToFile default based on class options when not set
81
+ if (arguments[2] === undefined) logToFile = this.getOptionForType(type, 'logToFileByDefault');
82
+
83
+ // treat overwrite default as true for log type "debug"
84
+ if (type === 'debug' && arguments[3] === undefined) overwrite = true;
85
+
86
+ return this.log(content, type, logToFile, overwrite);
87
+ }
88
+
89
+ // retrieves option setting for given type, default to global
90
+ getOptionForType(type, option) {
91
+ const options = this.typeOptions[type];
92
+ if (!options || !options[option]) return this.options[option];
93
+
94
+ return options[option];
95
+ }
96
+
97
+ // exposes chalk for custom color options via defineType
98
+ chalk() {
99
+ return this.color.getChalkInstance();
100
+ }
101
+
102
+ // logs to console, and conditionally to file
103
+ async log(content, type, logToFile, overwrite) {
104
+ const logTimestamp = this.getOptionForType(type, 'logTimestamp');
105
+ const timestamp = `[${new Date().toLocaleString('en-US')}]`;
106
+ const chalkColorFunction = this.color.getLogColor(type);
107
+ let prefix = this.getOptionForType(type, 'prefix');
108
+ let suffix = this.getOptionForType(type, 'suffix');
109
+ if (logTimestamp) prefix += `${timestamp} `;
110
+ if (prefix) prefix = chalkColorFunction(prefix);
111
+ if (suffix) suffix = chalkColorFunction(suffix);
112
+ const coloredLog = chalkColorFunction(content);
113
+
114
+ console.log(`${prefix}${coloredLog}${suffix}`); // eslint-disable-line no-console
115
+
116
+ if (!logToFile) return;
117
+
118
+ return this.writeToFile(type, `${timestamp} ${content}\n`, overwrite);
119
+ }
120
+
121
+ // direct hardcoded debug method (log to file functionality is limited)
122
+ async debug(content, logToFile = false, overwrite = true) {
123
+ console.dir(content, { depth: 6 }); // eslint-disable-line no-console
124
+
125
+ if (!logToFile) return;
126
+
127
+ return this.writeToFile('debug', JSON.stringify(content, null, 2), overwrite);
128
+ }
129
+
130
+ async writeToFile(type, content, overwrite) {
131
+ const path = this.getOptionForType(type, 'path');
132
+ const targetLog = `${path}${type}.log`;
133
+ await this.validateFileAndDirectory(path, targetLog);
134
+
135
+ const fileAction = overwrite ? fsp.writeFile : fsp.appendFile;
136
+
137
+ return fileAction(targetLog, content);
138
+ }
139
+
140
+ // attempts to create file and/or directory if they don't already exist
141
+ async validateFileAndDirectory(path, targetLog) {
142
+ const errorMethod = this.error || console.error; // prefer native method unless removed by user
143
+
144
+ mkdirSync(path, { recursive: true });
145
+
146
+ await fsp.access(targetLog).catch(() => {
147
+ fsp.writeFile(targetLog, '').catch(() => {
148
+ errorMethod(`Failed to create log file: ${targetLog}.`
149
+ + '\n Verify that the application runner has write permissions');
150
+ });
151
+ });
152
+ }
153
+
154
+ // method to conditionally sanitize user configuration input
155
+ sanitizePath(path) {
156
+ const moduleDir = projectPath.dirname(fileURLToPath(import.meta.url));
157
+ const delim = moduleDir.includes('node_modules') ? 'node_modules' : 'src';
158
+ const splitDir = moduleDir.split(delim);
159
+
160
+ if (splitDir.length < 2) throw ('Failed to locate your project\'s root directory.');
161
+
162
+ // use project root directory behind path
163
+ path = projectPath.resolve(splitDir[0], path);
164
+
165
+ // force path property to contain a trailing "/"
166
+ if (path[path.length - 1] !== '/') {
167
+ path += '/';
168
+ }
169
+
170
+ return path;
171
+ }
172
+ }