🎯Project goals
This project was brought about by an embarrassing story that other young developers may relate to. When my office space reservation project (Hoosin a.k.a PAA Booking) was still in development I had little concern for robust security. Despite one of my main interests during my undergraduate degree being cybersecurity, I doubted that an administrative application that held boring office data that would be targeted. My main concern was getting the project off the ground.
Long story short, my backend server was cyberattacked. Although my project was still in beta and didn’t contain any sensitive data, it exposed some clear flaws in my system. A big problem I noticed was that I didn’t log enough transactions to catch the attack early. Even when I did log transactions to the console, the logs were ephemeral: they were tied to the terminal’s lifecycle. Obviously, this isn’t ideal. The suitable way to log something is to make a persistent record of that transaction. I decided to create a module for my backend to easily log to a file using node:fs
. When I found myself copying and pasting this module across multiple projects I realized:
“I should just turn this into a Node module.”
So I created a dead simple, lightweight Node module to conveniently persist terminal logs that comes in at 1.3kb
. Something to dictate for the console.
⚙️How it works
Logging
The main use case for console-dictation is to write to a file. There are three initial log categories a user can log to:
function | log path |
---|---|
system | <CWD>/logs/sys/sys.log |
error | <CWD>/logs/error/err.log |
misc | <CWD>/logs/misc/misc.log |
Folder paths are only created as the log functions are used, so if you use all of the functions you’ll find yourself with a file structure like:
Each function has the same signature:
An example of usage would be:
path
You can use some of the optional parameters to customize behaviour like which path
the log is written to:
The above code would create the file structure:
This technique of changing the log path should be used for one-off redirects. Let’s say you decide you want the system
log to be in a file called system.log
instead of the default sys.log
. You could hypothetically pass a new path
on each system
call, but making sure the path
argument is properly passed on each system
call would get pretty annoying. For that reason, you should rely on using the config
function instead of using the optional path
parameter if you want to change the log path for the majority of logs (discussed later).
isDependency
isDependency
is an internal parameter for dependency writing. It’s accessible through the function, but it should be used with caution: it’s not intended to be used in external calls.
Configuring
The config
function should be used to configure module logs at a global level. config
has the signature:
paths
You can customize the file paths while preserving the file names by passing an argument that correlates to the target log function:
log_names
You can customize the file names while preserving the file paths by passing an argument that correlates to the target log function:
indexing_config
By default, logs take the format of:
console-dictation also supports an indexing system that takes the form of:
You can optionally opt-in to an indexing system:
If you opt-in to console-dictation’s indexing system, by default you also opt-in to a dependency logging system (i.e., X makes a log whenever a log is made in Y):
By default, the dependencies are:
System log | Error log | Miscellaneous log |
---|---|---|
[] | [system] | [system] |
You can change the log dependencies:
🔍Taking a look under the hood
State
Module scope can get a little confusing, but here’s the basics:
- Node.js uses a wrapper function to encapsulate module code (source)
- when the wrapper function is called, the encapsulated variables are assigned, including instantiating objects and stored on the heap
When console-dictation is imported
or required
into a project for the first time, all objects, including the dictator
wrapper object, tracker
object, and config
objects are instantiated once and stored on the heap.
Because object variables point to their object by reference, every subsequent import
or require
in any module in the same project will reference the same objects at runtime.
For some developers, this behaviour is an obstacle, but for the purposes of this project, this behaviour means that the configuration and tracking will be consistent throughout the project’s lifecycle! (source)
Writing
There’s an internal module called write
used to write all messages to the given file path with signature
The logic is very simple, using the node:fs
library:
- check whether the provided file path exists - if yes, append to file - if no, make folder directory, open file, write to file
The log functions are essentially wrapper functions for the internal write
function with the following logic:
- construct a message with a timestamp
- add an index to the message if
1.
indexing = true
is stored in memory 2.isDependency
is not passed - check whether a valid path passed as an argument
- if yes,
write
with the passed path - if no,write
with the path stored in memory for that log type - parse through the log type’s dependency array and write in the format “[log type] issued: issue #“
Time Stamping
Timestamps are created with a JavaScript Date
object that instantiated on function call in UTC-5 time zone in the format of:
Indexing
Indexing configuration indexing
and dependencies
are stored as an object in a config
module.
The indexing functionality comes in two parts: the tracker and the logging
The tracker is an object that stores the current index for each log function and an increment function to increase any log type’s index:
As described in writing, each log function checks to see if it isDependency
. If isDependency = false
then the tracker
object’s increment
function is called and returns the current index value for the log.
Validating
It’s important to validate that custom configurations are correctly passed to log or config functions.
An example where this could cause a system error would be if a custom path was passed into a log function such as:
In this case, the internal write function would try writing to a folder ./logs/test
and resolve as an error similar to:
To ensure the correct arguments were passed to a function, each argument is passed through an isRequestValid
internal function of signature:
where the incoming request is the first parameter and the keys to validate against are the second parameter.
The logic checks:
- whether the request keys are of the same length as the validator keys
- whether each request key exists in the validator keys array
⚒️How to start using console-dictation
console-dictation is a TypeScript-compatible Node.js module. console-dictation is compatible with both CommonJS and ES Module JavaScript.
Installation
Quick Start (CommonJS)
-
Require the package
-
Use package methods to start logging
Quick Start (ESM JavaScript)
-
Require the package
-
Use package methods to start logging
Usage
Example 1: simple express logging
Example 2: utilize destructuring
Demos
This module has two demos prepared: one for an ES Module JavaScript/MJS environment and one for a CommonJS environment.
ES Module JavaScript
ESM demo code can be found here.
-
Clone this repo
-
CD into the demo folder
-
Run start script
-
Inspect logs
CommonJS
CJS demo code can be found here.
-
Clone this repo
-
CD into the demo folder
-
Run start script
-
Inspect logs