You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: chapters/ch04.2-writing-logs.md
+71-60Lines changed: 71 additions & 60 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,23 +30,23 @@ When logging is performed synchronously, each log entry is written immediately t
30
30
31
31
By decoupling the logging process from the main application logic, we can achieve several advantages:
32
32
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.
34
34
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.
36
36
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.
38
38
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
40
40
41
41
### 4. Getting Caller Information (Module and Line Number)
42
42
43
43
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.
44
44
45
45
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:
46
46
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.
50
50
51
51
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:
52
52
@@ -67,9 +67,9 @@ To do the testing, we'll create a new file `test.js` and a `config.json`. `test.
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 {
117
117
getsize_threshold() {
118
118
returnthis.#config.rolling_config.size_threshold;
119
119
}
120
-
...
120
+
...
121
121
}
122
122
```
123
123
@@ -143,7 +143,7 @@ Error: time_option must be an instance of RollingConfig. Unsupported param 400
143
143
at Function.assert (/Users/ishtmeet/Code/logtard/lib/utils/rolling-options.js:36:19)
144
144
```
145
145
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
147
147
148
148
```js
149
149
// file: lib/util/rolling-options.js
@@ -229,9 +229,9 @@ The "Don't Repeat Yourself" (DRY) principle is a basic concept in software devel
229
229
230
230
DRY encourages developers to write clean, efficient, and modular code by:
231
231
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.
235
235
236
236
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.
@@ -372,17 +372,17 @@ We only write logs based off the current logger's `log_level`
372
372
373
373
### Writing to a file
374
374
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.
376
376
377
377
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.
378
378
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.
379
379
380
380
Create a private `log_file_handle` member
381
381
382
382
```js
383
-
// file: lib/logger.js
383
+
// file: lib/logger.js
384
384
385
-
constfs=require('node:fs/promises')
385
+
constfs=require('node:fs/promises')
386
386
387
387
classLogger {
388
388
...
@@ -402,7 +402,7 @@ We'll expose a public method on the `Logger` called `init` and make that `async`
402
402
403
403
```js
404
404
// file: lib/logger.js
405
-
constfs=require('node:fs/promises')
405
+
constfs=require('node:fs/promises')
406
406
407
407
classLogger {
408
408
...
@@ -426,29 +426,38 @@ There's a lot going on in this method. Let's break it down.
426
426
427
427
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".
428
428
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.
442
451
443
452
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.
444
453
445
454
6.`.log` is appended to the modified date and time string to form the final log file name.
446
455
447
456
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.
452
461
453
462
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.
454
463
@@ -480,11 +489,11 @@ And then call the `init` method from the `test.js`
@@ -586,7 +595,7 @@ Inside the `package.json` file, add a script `start` with the value of `node tes
586
595
"scripts": {
587
596
"start": "node test.js"
588
597
},
589
-
...
598
+
...
590
599
}
591
600
```
592
601
@@ -611,7 +620,7 @@ If you do run `npm start`, it fails.
611
620
612
621
```bash
613
622
### 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']
615
624
```
616
625
617
626
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!
632
641
633
642
For example:
634
643
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.
638
647
639
648
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
640
649
641
650
```js
642
-
functionmy_fun() { console.log('hi'); }
643
-
my_fun.hey='there';
651
+
functionmy_fun() {
652
+
console.log("hi");
653
+
}
654
+
my_fun.hey="there";
644
655
645
656
console.log(my_fun.hey); // there
646
657
my_fun(); // 'hi'
@@ -653,8 +664,8 @@ Create a new file called `helpers.js` inside the `lib/utils` folder.
653
664
```js
654
665
// file: lib/utils/helpers.js
655
666
656
-
constfs_sync=require('node:fs');
657
-
constpath=require('path')
667
+
constfs_sync=require("node:fs");
668
+
constpath=require("path");
658
669
659
670
/**
660
671
* @returns{fs_sync.PathLike} The path to the directory.
@@ -665,21 +676,21 @@ function check_and_create_dir(path_to_dir) {
665
676
fs_sync.mkdirSync(log_dir, { recursive:true });
666
677
}
667
678
668
-
return log_dir
679
+
return log_dir;
669
680
}
670
681
671
682
module.exports= {
672
-
check_and_create_dir
673
-
}
683
+
check_and_create_dir,
684
+
};
674
685
```
675
686
676
687
Let's go through the `check_and_create_dir` function's code line by line.
677
688
678
689
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.
683
694
684
695
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.
685
696
@@ -732,7 +743,7 @@ Instead of logging to the standard output, let's write it to the log file that i
732
743
classLogger {
733
744
...
734
745
async #log(message, log_level) {
735
-
// dont' write to the file if
746
+
// dont' write to the file if
736
747
// 1. The `log_level` argument is less than the `#config.level` value
737
748
// 2. If the `fd` (file descriptor) is either 0 or -1, which means the file
0 commit comments