Skip to content

Commit 5259264

Browse files
committed
fixes #19 - adds further explanation about the introduced regex - [\.:]+
1 parent 5dffe01 commit 5259264

File tree

1 file changed

+71
-60
lines changed

1 file changed

+71
-60
lines changed

chapters/ch04.2-writing-logs.md

Lines changed: 71 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,23 @@ When logging is performed synchronously, each log entry is written immediately t
3030
3131
By decoupling the logging process from the main application logic, we can achieve several advantages:
3232

33-
- **Improved Performance:** Asynchronous logging allows the main application thread to continue executing without waiting for log writes to complete. This can be crucial for applications that require high responsiveness and throughput.
33+
- **Improved Performance:** Asynchronous logging allows the main application thread to continue executing without waiting for log writes to complete. This can be crucial for applications that require high responsiveness and throughput.
3434

35-
- **Reduced I/O Overhead:** Writing log messages to disk can be an I/O-intensive operation. By batching multiple log messages together and writing them in one go, you reduce the frequency of disk I/O operations, which can improve efficiency.
35+
- **Reduced I/O Overhead:** Writing log messages to disk can be an I/O-intensive operation. By batching multiple log messages together and writing them in one go, you reduce the frequency of disk I/O operations, which can improve efficiency.
3636

37-
- **Better Resource Utilization:** Asynchronous logging allows you to optimize resource utilization, such as managing concurrent log writes, handling errors without disrupting the main flow, and efficiently managing file handles.
37+
- **Better Resource Utilization:** Asynchronous logging allows you to optimize resource utilization, such as managing concurrent log writes, handling errors without disrupting the main flow, and efficiently managing file handles.
3838

39-
- **Enhanced Scalability:** Applications with multiple threads or processes benefit from asynchronous logging because it minimizes contention for resources like the log file. This is particularly valuable in scenarios where multiple components are concurrently generating log messages
39+
- **Enhanced Scalability:** Applications with multiple threads or processes benefit from asynchronous logging because it minimizes contention for resources like the log file. This is particularly valuable in scenarios where multiple components are concurrently generating log messages
4040

4141
### 4. Getting Caller Information (Module and Line Number)
4242

4343
Including caller information, such as the file name and line number from which a log message originated, can significantly enhance the effectiveness of our logging library. This feature provides contextual insight into where specific events occurred in the codebase, making it easier to identify the source of issues and troubleshoot them.
4444

4545
When an application encounters an error or unexpected behavior, having access to the module and line number associated with the log message allows developers to:
4646

47-
- Quickly locate the exact location in the code where the event occurred.
48-
- Understand the sequence of events leading up to the issue.
49-
- Make precise code adjustments to fix problems.
47+
- Quickly locate the exact location in the code where the event occurred.
48+
- Understand the sequence of events leading up to the issue.
49+
- Make precise code adjustments to fix problems.
5050

5151
Implementing this feature might involve using techniques from the programming language's stack trace or introspection mechanisms. Here's a high-level overview of how you could achieve this:
5252

@@ -67,9 +67,9 @@ To do the testing, we'll create a new file `test.js` and a `config.json`. `test.
6767
```js
6868
// test.js
6969

70-
const { Logger, LogConfig } = require('./index')
70+
const { Logger, LogConfig } = require("./index");
7171

72-
const logger = Logger.with_config(LogConfig.from_file('./config.json'));
72+
const logger = Logger.with_config(LogConfig.from_file("./config.json"));
7373
```
7474

7575
The `config.json` file has the following contents. You may try to tweak the values as well. Try putting in the values that aren't supported by us, and see whether the `assert` methods that we created actually crash the program or not?
@@ -117,7 +117,7 @@ class Logger {
117117
get size_threshold() {
118118
return this.#config.rolling_config.size_threshold;
119119
}
120-
...
120+
...
121121
}
122122
```
123123

@@ -143,7 +143,7 @@ Error: time_option must be an instance of RollingConfig. Unsupported param 400
143143
at Function.assert (/Users/ishtmeet/Code/logtard/lib/utils/rolling-options.js:36:19)
144144
```
145145

146-
Why? Let's take a quick look at our `RollingTimeOptions` utility class
146+
Why? Let's take a quick look at our `RollingTimeOptions` utility class
147147

148148
```js
149149
// file: lib/util/rolling-options.js
@@ -229,9 +229,9 @@ The "Don't Repeat Yourself" (DRY) principle is a basic concept in software devel
229229

230230
DRY encourages developers to write clean, efficient, and modular code by:
231231

232-
- Reducing the chances of errors: Duplicated code increases the chances of mistakes or bugs when changes are made in one place but not in others.
233-
- Simplifying maintenance: When a change is required, you only need to update the code in one place, making it easier to keep your codebase up-to-date and consistent.
234-
- Enhancing readability: Code that is free from unnecessary duplication is easier to understand and follow, making it more accessible to other developers.
232+
- Reducing the chances of errors: Duplicated code increases the chances of mistakes or bugs when changes are made in one place but not in others.
233+
- Simplifying maintenance: When a change is required, you only need to update the code in one place, making it easier to keep your codebase up-to-date and consistent.
234+
- Enhancing readability: Code that is free from unnecessary duplication is easier to understand and follow, making it more accessible to other developers.
235235

236236
Although following the DRY principle is generally beneficial, there can be situations where duplication might not necessarily be a bad thing. Not every instance of code repetition needs to be eliminated, and striving for absolute **DRY**ness in all cases might lead to overcomplicated solutions or premature abstraction.
237237

@@ -357,7 +357,7 @@ class Logger {
357357

358358
        console.log('%s: %s', message, LogLevel.to_string(log_level))
359359
}
360-
...
360+
...
361361
}
362362
```
363363

@@ -372,17 +372,17 @@ We only write logs based off the current logger's `log_level`
372372

373373
### Writing to a file
374374

375-
We've been using `console.log()` to print the log messages to the standard output, or your terminal. However, there are many drawbacks to this approach.
375+
We've been using `console.log()` to print the log messages to the standard output, or your terminal. However, there are many drawbacks to this approach.
376376

377377
1. **Lack of Persistence:** Console logs are ephemeral and disappear once the application session ends (when you close the terminal or exit out of an ssh connection). This makes it challenging to review logs for past sessions.
378378
2. **Performance Impact:** Continuous console output can impact application performance, especially when generating a high volume of logs. It can slow down the application and interfere with its responsiveness. There are certain ways to get mitigate this, we'll talk about this in a later chapter.
379379

380380
Create a private `log_file_handle` member
381381

382382
```js
383-
// file: lib/logger.js
383+
// file: lib/logger.js
384384

385-
const fs = require('node:fs/promises')
385+
const fs = require('node:fs/promises')
386386

387387
class Logger {
388388
...
@@ -402,7 +402,7 @@ We'll expose a public method on the `Logger` called `init` and make that `async`
402402

403403
```js
404404
// file: lib/logger.js
405-
const fs = require('node:fs/promises')
405+
const fs = require('node:fs/promises')
406406

407407
class Logger {
408408
...
@@ -426,29 +426,38 @@ There's a lot going on in this method. Let's break it down.
426426

427427
3. `new Date().toISOString()` generates a string representation of the current date and time in the ISO 8601 format, such as "2023-08-18T15:30:00.000Z".
428428

429-
4. `.replace(/\..+/, "")` is a regular expression operation. Let's break down the regex:
430-
431-
- `\.` matches a literal dot character. Since the dot (`.`) is a special character in regular expression, known as a wildcard. It matches any single character except for a newline character (`\n`). This is useful when you want to match any character, which is often used in pattern matching.
432-
433-
However, in this case, we want to match a literal dot character in the string (the dot in the date-time format "00.000Z"). To achieve this, we need to escape the dot character by preceding it with a backslash (`\`). When you place a backslash before a special character, it indicates that you want to match the literal character itself, rather than its special meaning.
434-
435-
- `.+` matches one or more of any character except newline. We match for all the characters following the dot.
436-
437-
- `/g` is the global flag, indicating that the replacement should be applied to all occurrences of the pattern.
438-
439-
- So, the regex `\..+` matches the dot and all characters after it in the string.
440-
441-
- The replacement `""` removes the dot and all characters after it.
429+
4. `.replace(/[\.:]+/, "")` is a regular expression operation. Let's break down the regex:
430+
431+
- the square brackets [] are used to define a character class. A character class is a way to specify a set of characters from which a single character can match. For example:
432+
- [abc] matches any single character that is either 'a', 'b', or 'c'.
433+
- [0-9] matches any single digit from 0 to 9.
434+
- [a-zA-Z] matches any single alphabetical character, regardless of case.
435+
436+
You can also use special characters within character classes to match certain character types, like `\d` for digits, `\w` for word characters, `\s` for whitespace, etc.
437+
438+
In this case we are looking for all the dot (`.`) characters and colon (`:`) characters in the string.
439+
440+
- `\.` matches a literal dot character. Since the dot (`.`) is a special character in regular expression, known as a wildcard. It matches any single character except for a newline character (`\n`). This is useful when you want to match any character, which is often used in pattern matching.
441+
442+
However, in this case, we want to match a literal dot character in the string (the dot in the date-time format "00.000Z"). To achieve this, we need to escape the dot character by preceding it with a backslash (`\`). When you place a backslash before a special character, it indicates that you want to match the literal character itself, rather than its special meaning.
443+
444+
- `+` matches one or more of any character except newline. We match for all the characters following the dot (.) and the (:) character.
445+
446+
- `/g` is the global flag, indicating that the replacement should be applied to all occurrences of the pattern.
447+
448+
- So, the regex `\..+` matches the dot and all characters after it in the string.
449+
450+
- The replacement `""` removes the dot and all characters after it.
442451

443452
5. The result of the `replace` operation is a modified version of the ISO string, which now includes only the date and time portion, without the fractional seconds.
444453

445454
6. `.log` is appended to the modified date and time string to form the final log file name.
446455

447456
7. `await fs.open(file_name, "a+")` opens or creates the log file using the `fs.open` function with "a+" flag. We talked about the modes in a [previous chapter](https://github.com/ishtms/learn-nodejs-hard-way/blob/master/chapters/ch03-working-with-files.md#flag-argument)
448-
449-
- If the file doesn't exist, it will be created.
450-
- If the file exists, data can be appended to it.
451-
- The `"a+"` mode allows both reading and appending. Appending means, we begin writing to the end of the file instead of from the 1st line. However, if the file is empty, it starts from the beginning.
457+
458+
- If the file doesn't exist, it will be created.
459+
- If the file exists, data can be appended to it.
460+
- The `"a+"` mode allows both reading and appending. Appending means, we begin writing to the end of the file instead of from the 1st line. However, if the file is empty, it starts from the beginning.
452461

453462
This code initializes the logger by creating or opening a log file with a name based on the current date. The regular expression is used to remove the fractional seconds from the ISO date string, and the resulting string is used as part of the log file name. This allows for creating a new log file every time the `init` method is called, typically representing logs for a specific time period.
454463

@@ -480,11 +489,11 @@ And then call the `init` method from the `test.js`
480489
```js
481490
// file: test.js
482491

483-
const { Logger, LogConfig } = require('./index')
492+
const { Logger, LogConfig } = require("./index");
484493

485-
const logger = Logger.with_config(LogConfig.from_file('./config.json'));
494+
const logger = Logger.with_config(LogConfig.from_file("./config.json"));
486495
await logger.init();
487-
console.log("Program exits.")
496+
console.log("Program exits.");
488497
```
489498

490499
However the output is strange
@@ -545,11 +554,11 @@ Let's fix this by using the `__dirname` global variable that we used earlier.
545554
```js
546555
// file: test.js
547556

548-
const path = require('node:path')
557+
const path = require("node:path");
549558
const { Logger, LogConfig } = require("./index");
550559

551560
async function main() {
552-
const logger = Logger.with_config(LogConfig.from_file(path.join(__dirname, 'config.json')));
561+
const logger = Logger.with_config(LogConfig.from_file(path.join(__dirname, "config.json")));
553562
await logger.init();
554563
console.log("End of the file");
555564
}
@@ -586,7 +595,7 @@ Inside the `package.json` file, add a script `start` with the value of `node tes
586595
"scripts": {
587596
    "start": "node test.js"
588597
    },
589-
...
598+
...
590599
}
591600
```
592601

@@ -611,7 +620,7 @@ If you do run `npm start`, it fails.
611620

612621
```bash
613622
### Outputs
614-
[Error: ENOENT: no such file or directory, open 'logs/LogTar_2023-08-18T19:14:46.log']
623+
[Error: ENOENT: no such file or directory, open 'logs/LogTar_2023-08-18T19:14:46.log']
615624
```
616625

617626
It's failing because it could not find the `log` directory. If we mention something like this in our library's readme:
@@ -632,15 +641,17 @@ But, require is not just a function!
632641

633642
For example:
634643

635-
- `require.resolve('module-name')`: Returns the path of the module without actually loading it.
636-
- `require.cache`: Provides access to the cache of loaded modules.
637-
- `require.main`: Provides access to the `Module` object representing the entry script loaded when the Node.js process launched. This is exactly what we need.
644+
- `require.resolve('module-name')`: Returns the path of the module without actually loading it.
645+
- `require.cache`: Provides access to the cache of loaded modules.
646+
- `require.main`: Provides access to the `Module` object representing the entry script loaded when the Node.js process launched. This is exactly what we need.
638647

639648
The reason `require` might feel like both an object and a function is because JavaScript allows functions to have properties. You can test this yourself
640649

641650
```js
642-
function my_fun() { console.log('hi'); }
643-
my_fun.hey = 'there';
651+
function my_fun() {
652+
console.log("hi");
653+
}
654+
my_fun.hey = "there";
644655

645656
console.log(my_fun.hey); // there
646657
my_fun(); // 'hi'
@@ -653,8 +664,8 @@ Create a new file called `helpers.js` inside the `lib/utils` folder.
653664
```js
654665
// file: lib/utils/helpers.js
655666

656-
const fs_sync = require('node:fs');
657-
const path = require('path')
667+
const fs_sync = require("node:fs");
668+
const path = require("path");
658669

659670
/**
660671
* @returns {fs_sync.PathLike} The path to the directory.
@@ -665,21 +676,21 @@ function check_and_create_dir(path_to_dir) {
665676
fs_sync.mkdirSync(log_dir, { recursive: true });
666677
}
667678

668-
return log_dir
679+
return log_dir;
669680
}
670681

671682
module.exports = {
672-
check_and_create_dir
673-
}
683+
check_and_create_dir,
684+
};
674685
```
675686

676687
Let's go through the `check_and_create_dir` function's code line by line.
677688

678689
1. The `path.resolve()` function creates an absolute path by combining a sequence of paths or path segments.
679-
680-
It processes the sequence from right to left, with each subsequent path added to the beginning until an absolute path is created. For example, if the path segments are `/foo`, `/bar`, and `baz`, calling `path.resolve('/foo', '/bar', 'baz')` would return `/bar/baz` because `'/bar' + '/' + 'baz'` creates an absolute path, but `'baz'` does not.
681-
682-
If, after processing all the segments in the given path, an absolute path hasn't been created yet, then the current working directory is used instead.
690+
691+
It processes the sequence from right to left, with each subsequent path added to the beginning until an absolute path is created. For example, if the path segments are `/foo`, `/bar`, and `baz`, calling `path.resolve('/foo', '/bar', 'baz')` would return `/bar/baz` because `'/bar' + '/' + 'baz'` creates an absolute path, but `'baz'` does not.
692+
693+
If, after processing all the segments in the given path, an absolute path hasn't been created yet, then the current working directory is used instead.
683694

684695
2. The `require.main.path` holds the absolute path to the directory of the entry point. The entry point is the `test.js` in our case. Or whatever file you specify while running `node file_name.js` command.
685696

@@ -732,7 +743,7 @@ Instead of logging to the standard output, let's write it to the log file that i
732743
class Logger {
733744
...
734745
async #log(message, log_level) {
735-
// dont' write to the file if
746+
// dont' write to the file if
736747
// 1. The `log_level` argument is less than the `#config.level` value
737748
// 2. If the `fd` (file descriptor) is either 0 or -1, which means the file
738749
// descriptor is closed or not opened yet.
@@ -756,14 +767,14 @@ const path = require("node:path");
756767
const { Logger, LogConfig } = require("./index");
757768

758769
async function initialize_logger() {
759-
let logger = Logger.with_config(LogConfig.from_file(path.join(__dirname, "config.json")))
770+
let logger = Logger.with_config(LogConfig.from_file(path.join(__dirname, "config.json")));
760771
await logger.init();
761772

762773
return logger;
763774
}
764775
async function main() {
765776
let logger = await initialize_logger();
766-
logger.error('This is an error message');
777+
logger.error("This is an error message");
767778
}
768779

769780
main();

0 commit comments

Comments
 (0)