├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.yml ├── .github └── FUNDING.yml ├── .gitignore ├── .ncurc.json ├── .npmignore ├── .nvmrc ├── .vscode ├── launch-logality.js ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── export-wrapper.js ├── functions.js ├── logality.js ├── pretty-print.js ├── serializers │ ├── custom.serializer.js │ ├── error.serializer.js │ ├── express-request.serializer.js │ └── user.serializer.js └── utils.js ├── assets ├── logality_flow_chart.png ├── logality_flow_chart_preview.png └── logality_preview.png ├── package-lock.json ├── package.json └── test ├── .eslintrc.yml ├── fixtures └── log-samples.fix.js ├── lib └── tester.lib.js ├── serializers ├── __snapshots__ │ ├── custom-serializers.test.js.snap │ ├── multi-key-serializers.test.js.snap │ ├── user-custom-serializer.test.js.snap │ └── user.test.js.snap ├── custom-serializers.test.js ├── error-serializer.test.js ├── multi-key-serializers.test.js ├── user-custom-serializer.test.js └── user.test.js └── spec ├── __snapshots__ ├── async-log.test.js.snap ├── logging.test.js.snap ├── pipe.test.js.snap ├── pretty.test.js.snap ├── surface.test.js.snap ├── use.test.js.snap └── writepretty.test.js.snap ├── async-log.test.js ├── logging.test.js ├── pipe.test.js ├── pretty.test.js ├── surface.test.js ├── use.test.js ├── util.test.js └── writepretty.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # JavaScript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:14 11 | 12 | working_directory: ~/repo 13 | 14 | steps: 15 | - checkout 16 | 17 | # Download and cache dependencies 18 | - restore_cache: 19 | keys: 20 | - v1-dependencies-{{ checksum "package.json" }} 21 | # fallback to using the latest cache if no exact match is found 22 | - v1-dependencies- 23 | 24 | - run: npm install 25 | 26 | - save_cache: 27 | paths: 28 | - node_modules 29 | key: v1-dependencies-{{ checksum "package.json" }} 30 | 31 | # run tests! 32 | - run: npm run test 33 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | parserOptions: 4 | ecmaVersion: 2020 5 | 6 | extends: 7 | - airbnb-base 8 | - plugin:jsdoc/recommended 9 | - plugin:security/recommended 10 | - plugin:prettier/recommended 11 | 12 | plugins: 13 | - jsdoc 14 | - security 15 | 16 | rules: 17 | arrow-body-style: 0 18 | camelcase: 0 19 | class-methods-use-this: 0 20 | consistent-return: 0 21 | curly: ["error", "all"] 22 | func-names: 0 23 | function-paren-newline: 0 24 | global-require: 0 25 | implicit-arrow-linebreak: "off" 26 | jsdoc/check-examples: "off" 27 | jsdoc/check-tag-names: "error" 28 | jsdoc/no-undefined-types: "off" 29 | jsdoc/require-returns-description: "off" 30 | no-multi-assign: 0 31 | no-param-reassign: 0 32 | no-restricted-syntax: ["error", "ForInStatement", "LabeledStatement", "WithStatement"] 33 | no-underscore-dangle: 0 34 | prefer-arrow-callback: 0 35 | security/detect-object-injection: 0 36 | 37 | settings: 38 | jsdoc: 39 | preferredTypes: 40 | object: Object 41 | express: Express 42 | Function: function 43 | knex: Knex 44 | tagNamePreference: 45 | constant: const 46 | file: fileoverview 47 | returns: return 48 | 49 | globals: 50 | # Testing 51 | suite: true 52 | test: true 53 | describe: true 54 | it: true 55 | setup: true 56 | before: true 57 | beforeEach: true 58 | after: true 59 | afterEach: true 60 | teardown: true 61 | assert: true 62 | fit: true 63 | fdescribe: true 64 | expect: true 65 | jest: true 66 | BigInt: true 67 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [thanpolas] 4 | custom: ["thanpolas.eth", "https://gitcoin.co/grants/3393/uniswap-and-daos-library-development"] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dump.rdb 4 | wiki 5 | temp 6 | coverage 7 | -------------------------------------------------------------------------------- /.ncurc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reject": [ 3 | "faker", 4 | "chalk", 5 | "release-it" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | assets 2 | test 3 | .circleci 4 | .vscode 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.vscode/launch-logality.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Launches logality manually for node debugger debugging. 3 | */ 4 | 5 | const logality = require('..'); 6 | 7 | function run() { 8 | const logInst = logality(); 9 | 10 | const log = logInst.get(); 11 | 12 | log.info('hello world'); 13 | } 14 | 15 | run(); 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/.vscode/launch-logality.js" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | Copyright 2020 Athanasios Polychronakis 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 13 | OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 14 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logality 2 | 3 | > Versatile JSON Logger. 4 | 5 | [![NPM Version][npm-image]][npm-url] 6 | [![CircleCI][circle-image]][circle-url] 7 | [![codecov](https://codecov.io/gh/thanpolas/logality/branch/master/graph/badge.svg?token=UvpkiCwJHj)](https://codecov.io/gh/thanpolas/logality) 8 | [![Discord](https://img.shields.io/discord/847075821276758096?label=discord&color=CBE9F0)](https://discord.gg/GkyEqzJWEY) 9 | [![Twitter Follow](https://img.shields.io/twitter/follow/thanpolas.svg?label=thanpolas&style=social)](https://twitter.com/thanpolas) 10 | 11 | ![Logality](/assets/logality_preview.png) 12 | 13 | ## Why Logality 14 | 15 | - JSON and Pretty Print log messages. 16 | - Extend or alter logging schema to fit your needs. 17 | - Customize built-in serializers by overwriting them to create your 18 | own logging schema. 19 | - Middleware support. 20 | - Allows full manipulation of output. 21 | - Use in libraries and compose multiple Logality instances on the root 22 | project. 23 | - Automatically detects the module filename and path and includes in the log. 24 | 25 | [👉 See how Logality compares to other popular loggers.][comparison]. 26 | 27 | # Install 28 | 29 | Install the module using NPM: 30 | 31 | ``` 32 | npm install logality --save 33 | ``` 34 | 35 | # Documentation 36 | 37 | ## Quick Start 38 | 39 | ```js 40 | const Logality = require('logality'); 41 | 42 | const logality = Logality(); 43 | 44 | const log = logality.get(); 45 | 46 | log.info('Hello World!'); 47 | ``` 48 | 49 | ## Initial Configuration 50 | 51 | Logality requires to be initialized and configured once, 52 | then use the instance throughout your application. 53 | 54 | You can configure Logality during instantiation, here are the available 55 | configuration options: 56 | 57 | - `appName` {string} An arbitrary string to uniquely identify 58 | the service (logger instance). 59 | - `prettyPrint` {boolean|Object} If true will format and prettify the event and 60 | context, default is `false`. You may define additional options to configure 61 | pretty printing, they can be combined: 62 | - `prettyPrint.noTimestamp` {boolean} Do not print timestamp. 63 | - `prettyPrint.noFilename` {boolean} Do not print Log filename source. 64 | - `prettyPrint.onlyMessage` {boolean} Only print the log message (no context). 65 | - `minLevel` {number|string} Define the minimum level to be logged and ignore lower log levels. [See log levels for input values][log-levels], accepts both the string or numeric representations of the levels. 66 | - `serializers` {Object} You can define custom serializers or overwrite 67 | logality's. Read more [about Serializers bellow][serializers]. 68 | - `async` {boolean} Set to true to enable the asynchronous API for logging, 69 | see more bellow. Read more [on the async option bellow][async]. 70 | - `output` {Function(logContext:Object, isPiped:boolean)} Replace the output 71 | process of logality with a custom one. Read more [on the custom output documentation bellow][output]. 72 | 73 | ```js 74 | const Logality = require('logality'); 75 | 76 | const logality = Logality({ 77 | appName: 'service-something', 78 | prettyPrint: false, 79 | serializers: [(logContext) => {}], 80 | async: false, 81 | output: (logMessage) => { 82 | process.stdout.write(logMessage); 83 | }, 84 | }); 85 | ``` 86 | 87 | ### Logality Terminology 88 | 89 | - **Message {string}** The text (string) Log message input from the user. 90 | - **Context {Object}** The Context (or bindings) input from the user. 91 | - **LogContext {Object}** Log Context (the schema) used internally by logality for 92 | processing and ultimately output. 93 | - **LogMessage {String}** The serialized `LogContext` into a string for output. 94 | 95 | ### Logality Execution Flow 96 | 97 | [👉 Click here or on the Flow Chart for Full Resolution](/assets/logality_flow_chart.png). 98 | 99 | [![Logality Flow Chart](/assets/logality_flow_chart_preview.png)](/assets/logality_flow_chart.png) 100 | 101 | ### Logality Can be Asynchronous 102 | 103 | When logging has a transactional requirement, such as storing logs to a database or sending through an API, you can 104 | enable asynchronous mode. 105 | 106 | When Async is enabled all logs should be prefixed with the `await` keyword. 107 | 108 | Both the [middleware defined through `use()`][middleware] 109 | and the [output function if defined][output] will be expected to execute 110 | asynchronously. 111 | 112 | To enable the async API all you have to do is set the option `async` to true. All logging methods will now return 113 | a promise for you to handle: 114 | 115 | ```js 116 | const Logality = require('logality'); 117 | 118 | const logality = Logality({ 119 | appName: 'service-audit', 120 | async: true, 121 | }); 122 | 123 | /** ... */ 124 | 125 | async function createUser (userData) => { 126 | await log.info('New user creation', { 127 | userData, 128 | }); 129 | } 130 | ``` 131 | 132 | ### The custom "output" Function 133 | 134 | The custom output function will receive two arguments and is the final operation in the [execution flow][logality-flow]. The input arguments are: 135 | 136 | - `logContext` **Object** logContext is a native 137 | JS Object representing the entire log message. 138 | - `isPiped` **boolean** This argument indicates if the inbound "logContext" is the output of a piped instance or not (comes from a library). 139 | 140 | #### Importance of Return Value for "output" 141 | 142 | Depending on what value is returned by your custom output function different actions are performed by Logality. 143 | 144 | #### Custom Output: Object Return 145 | 146 | This is what you would typically want to always return. When an object is 147 | returned from your custom output function you pass the responsibility of 148 | serializing the Log Context into a string to Logality. 149 | 150 | As per the [Logality Flow Diagram][logality-flow], there are a few more steps 151 | that are done after your custom output returns an Object value: 152 | 153 | 1. Logality checks your `prettyPrint` setting and: 154 | 1. If it's true will format your Log Context into a pretty formatted string 155 | message. 156 | 2. If it's false will serialize using `JSON.stringify`. 157 | 2. Logality will then output that serialized stream by writing to the `process.stdout` stream. 158 | 159 | #### Custom Output: String Return 160 | 161 | When you return a string, Logality will skip the serialization of your Log 162 | Message and will directly invoke the output by writing to the `process.stdout` 163 | stream. 164 | 165 | This technique gives you the freedom to implement your own output format and/or 166 | create your pretty output formats. 167 | 168 | #### Custom Output: No Return 169 | 170 | When your custom output does not return anything, Logality will assume that you 171 | have handled everything and will not perform any further actions. 172 | 173 | In those cases your custom output function is responsible for serializing 174 | the Log Context and outputting it to the medium you see fit (stdout or a 175 | database). 176 | 177 | > **ℹ️ Note**: This is the recommended way to apply filters on what messages you want to be logged. 178 | 179 | ## Logality Instance Methods 180 | 181 | ### get() :: Getting a Logger 182 | 183 | To get a logger you have to invoke the `get()` method. That method will detect and use the module filename that it was invoked from so it is advised that you use the `get()` method in each module to have proper log messages. 184 | 185 | ```js 186 | const log = logality.get(); 187 | 188 | log(level, message, context); 189 | ``` 190 | 191 | The `get()` method will return the `log()` method partialed with arguments. 192 | The full argument requirements of `log()`, are: 193 | 194 | ```js 195 | logality.log(filename, level, message, context);` 196 | ``` 197 | 198 | When using `get()` you will receive the logger function with the `filename` argument already filled out. That is why you don't need to input the `filename` argument when you are using `logality.get()`. 199 | 200 | The partialed and returned `log` function will also have level helpers as 201 | illustrated in ["Log Levels"](#log-levels) section. 202 | 203 | ### Logging Messages 204 | 205 | Using any log level function (e.g. `log.info()`), your first argument is the "message". This is any arbitrary string to describe what has happened. It is the second argument, "context" that you will need to put any and all data you also want to attach with the logging message. 206 | 207 | ```js 208 | log.info(message, context); 209 | ``` 210 | 211 | The `context` argument is an object literal, parsed by what are called "Serializers". Serializers 212 | will take your data as input and format them in an appropriate, logging schema 213 | compliant output. 214 | 215 | You may extend logality with new [serializers][serializers] or you may 216 | overwrite the existing ones. 217 | 218 | ### pipe() :: Compose Multiple Logality Instances 219 | 220 | Use `pipe()` to link multiple logality instances to the root instance: 221 | 222 | ```js 223 | const Logality = require('logality'); 224 | 225 | const parentLogality = Logality(); 226 | const childLogality = Logality(); 227 | 228 | parentLogality.pipe(childLogality); 229 | ``` 230 | 231 | What this does is pipe all the output of the piped (child) logality instances to the "parent" Logality. This is particularly useful if a library is using Logality and you want to pipe its output or you want to have multiple classes of log streams (i.e. for audit logging purposes). 232 | 233 | - `pipe()` Accepts a single Logality instance or an Array of Logality instances. 234 | 235 | > **ℹ️ Note**: The LogContext of the child instance, will go through all the middleware and custom output functions defined in the parent instance. 236 | 237 | > **ℹ️ Note**: This is the case when the second argument `isPiped` will have a `true` value. 238 | 239 | ### use() :: Add Middleware. 240 | 241 | You can add multiple Middleware that will be invoked after all the [serializers][serializers] are applied (built-in and custom defined) and before the "Write to output" method is called. 242 | 243 | The middleware will receive the "Log Message" as a native Javascript Object and you can mutate or process it. 244 | 245 | All middleware with `use()` are synchronous. To support async middleware you have to enable the [`async` mode][async] when instantiating. 246 | 247 | #### use() Synchronous Example 248 | 249 | ```js 250 | const Logality = require('logality'); 251 | 252 | const logality = Logality(); 253 | 254 | logality.use((context) => { 255 | delete context.user; 256 | }); 257 | ``` 258 | 259 | #### use() Asynchronous Example 260 | 261 | ```js 262 | const Logality = require('logality'); 263 | 264 | const logality = Logality({ 265 | async: true, 266 | }); 267 | 268 | logality.use(async (context) => { 269 | await db.write(context); 270 | }); 271 | ``` 272 | 273 | ## The Logging Schema 274 | 275 | Logality automatically calculates and formats a series of system information 276 | which is then included in the output. When you log using: 277 | 278 | ```js 279 | log.info('Hello World!'); 280 | ``` 281 | 282 | Logality, when on production, will output the following (expanded) JSON string: 283 | 284 | ```JSON 285 | { 286 | "severity": 6, 287 | "level": "info", 288 | "dt": "2018-05-18T16:25:57.815Z", 289 | "message": "hello world", 290 | "event": {}, 291 | "context": { 292 | "runtime": { 293 | "application": "testLogality" 294 | }, 295 | "source": { 296 | "file_name": "/test/spec/surface.test.js" 297 | }, 298 | "system": { 299 | "hostname": "localhost", 300 | "pid": 36255, 301 | "process_name": "node ." 302 | } 303 | } 304 | } 305 | ``` 306 | 307 | - `severity` **{number}** Message severity expressed in an integer (7 lowest, 308 | 0 higher), see bellow fow values. 309 | - `level` **{string}** Message severity expressed in a unique string, 310 | see bellow fow values. 311 | - `dt` **{string}** An [ISO8601][iso8601] date. 312 | - `message` **{string}** Any message provided to the logger. 313 | - `event` **{Object}** When the log was triggered by an event, the metadata 314 | of that event are stored here. Logality supports many kinds of events as 315 | explained in the Serializers section. 316 | - `context` **{Object}** Context related to the log message. 317 | - `context.runtime.application` **{string}** Name of the service, define this 318 | when first instantiating the locality service. 319 | - `context.source.file_name` **{string}** The module where the log originated. 320 | - `context.system.hostname` **{string}** The local system's hostname. 321 | - `context.system.pid` **{string}** The local process id. 322 | - `context.system.process_name` **{string}** The local process name. 323 | 324 | ## Log Levels 325 | 326 | As per the [Log Schema](log-schema), the logging levels map to those of Syslog 327 | RFC 5424: 328 | 329 | | Syslog Level | Level Enum | Description | 330 | | ------------ | ----------- | --------------------------------- | 331 | | 0 | `emergency` | System is unusable | 332 | | 1 | `alert` | Action must be taken immediately | 333 | | 2 | `critical` | Critical conditions | 334 | | 3 | `error` | Error Conditions | 335 | | 4 | `warn` | Warning Conditions | 336 | | 5 | `notice` | Normal, but significant condition | 337 | | 6 | `info` | Informational messages | 338 | | 7 | `debug` | Debug-level messages | 339 | 340 | Each one of the "Level Enum" values is an available function at the logger that is returned using the `get()` method: 341 | 342 | ```js 343 | const Logality = require('logality'); 344 | const logality = new Logality(); 345 | const log = logality.get(); 346 | 347 | log.debug('This is message of level: Debug'); 348 | log.info('This is message of level: Info'); 349 | log.notice('This is message of level: Notice'); 350 | log.warn('This is message of level: warning'); 351 | log.error('This is message of level: Error'); 352 | log.critical('This is message of level: Critical'); 353 | log.alert('This is message of level: Alert'); 354 | log.emergency('This is message of level: Emergency'); 355 | ``` 356 | 357 | ## Logality Serializers 358 | 359 | Serializers are triggered by defined keys in the `context` object. 360 | Every serializer is configured to listen to a specific context key, for example 361 | the user serializer expects the `user` key in the context: 362 | 363 | ```js 364 | log.info('User Logged in', { 365 | user: udo, 366 | }); 367 | ``` 368 | 369 | If no serializer is configured for the `user` property, the data will be 370 | ignored. Logality has implemented the following serializers out of the box: 371 | 372 | ### The User Serializer 373 | 374 | > Serializes a User Data Object. 375 | 376 | ```js 377 | // a user logged in 378 | const user = login(username, password); 379 | 380 | // Let log the event 381 | log.info('User Logged in', { user: user }); 382 | ``` 383 | 384 | #### Expects 385 | 386 | - `id` The user's id. 387 | - `email` The user's email. 388 | 389 | #### Outputs 390 | 391 | ```JSON 392 | "context": { 393 | "user": { 394 | "id": 10, 395 | "email": "one@go.com", 396 | } 397 | } 398 | ``` 399 | 400 | ### The Error Serializer 401 | 402 | > Serializes a Javascript Error Object or an Exception. 403 | 404 | ```js 405 | const err = new Error('Broke'); 406 | 407 | log.error('Something broke', { error: err }); 408 | ``` 409 | 410 | #### Expects 411 | 412 | A native JS Error Object, or similar: 413 | 414 | - `name` **{string}** Name of the error. 415 | - `message` **{string}** The error's message. 416 | - `stack` **{string}** The stack trace. Logality will automatically parse the stack trace to a JSON object. 417 | 418 | #### Outputs 419 | 420 | ```JSON 421 | "event":{ 422 | "error":{ 423 | "name":"Error", 424 | "message":"Broke", 425 | "backtrace": "Stack Trace...", 426 | } 427 | } 428 | ``` 429 | 430 | ### The Request Serializer 431 | 432 | > Serializes an Express.JS Request Object. 433 | 434 | ```js 435 | function index(req, res) { 436 | log.info('Index Request', { req: req }); 437 | } 438 | ``` 439 | 440 | #### Expects 441 | 442 | Express JS Request Object. 443 | 444 | #### Outputs 445 | 446 | ```JSON 447 | "event":{ 448 | "http_request": { 449 | "headers": {}, 450 | "host": "localhost", 451 | "method": "GET", 452 | "path": "/", 453 | "query_string": "", 454 | "scheme": "http" 455 | } 456 | } 457 | ``` 458 | 459 | - `event.http_request` **{Object}** When the request object is passed the following additional data are stored: 460 | - `event.http_request.headers` **{Object}** Key-value pairs of all the HTTP headers, excluding sensitive headers. 461 | - `event.http_request.host` **{string}** The hostname. 462 | - `event.http_request.method` **{string}** HTTP method used. 463 | - `event.http_request.path` **{string}** The request path. 464 | - `event.http_request.query_string` **{string}** Quer string used. 465 | - `event.http_request.scheme` **{string}** One of "http" or "https". 466 | 467 | ### The Custom Serializer 468 | 469 | > Serializes any data that is passed as JSON. 470 | 471 | ```js 472 | // Custom log 473 | log.info('Something happened', { 474 | custom: { 475 | any: 'value', 476 | }, 477 | }); 478 | ``` 479 | 480 | #### Expects 481 | 482 | Anything 483 | 484 | #### Outputs 485 | 486 | ```JSON 487 | "context": { 488 | "custom": { 489 | "any": "value" 490 | } 491 | } 492 | ``` 493 | 494 | ## Custom Serializers 495 | 496 | You can define your own serializers or overwrite the existing ones when you first instantiate Logality. There are three parameters when creating a serializer: 497 | 498 | - **Context Name** The name on your `context` object that will trigger the serializer. 499 | - **Output Path** The path in the JSON output where you want the serializer's value to be stored. Use dot notation to signify the exact path. 500 | - **Value** The serialized value to output on the log message. 501 | 502 | The _Context Name_ is the key on which you define your serializer. So for instance when you set a serializer on the user key like so `mySerializers.user = userSerializer` the keyword `user` will be used. 503 | 504 | Output Path and Value are the output of your serializer function and are 505 | expected as separate keys in the object you must return: 506 | 507 | - `path` **{string}** Path to save the value, use dot notation. 508 | - `value` **{\*}** Any value to store on that path. 509 | 510 | An Example: 511 | 512 | ```js 513 | const Logality = require('logality'); 514 | 515 | mySerializers = { 516 | user: function (user) { 517 | return { 518 | path: 'context.user', 519 | value: { 520 | id: user.id, 521 | email: email.id, 522 | type: user.type, 523 | }, 524 | }; 525 | }, 526 | order: function (order) { 527 | return { 528 | path: 'context.order', 529 | value: { 530 | order_id: order.id, 531 | sku_id: order.sku, 532 | total_price: order.item_price * order.quantity, 533 | quantity: order.quantity, 534 | }, 535 | }; 536 | }, 537 | }; 538 | 539 | const logality = new Logality({ 540 | appName: 'service-something', 541 | serializers: mySerializers, 542 | }); 543 | ``` 544 | 545 | ### Multi Key Custom Serializers 546 | 547 | In some cases you may need to write to more than one keys in the log context. 548 | To be able to do that, simply return an Array instead of an Object like so: 549 | 550 | ```js 551 | const Logality = require('logality'); 552 | 553 | mySerializers = { 554 | user: function (user) { 555 | return [ 556 | { 557 | path: 'context.user', 558 | value: { 559 | id: user.id, 560 | email: email.id, 561 | type: user.type, 562 | }, 563 | }, 564 | { 565 | path: 'context.request', 566 | value: { 567 | user_id: user.id, 568 | }, 569 | }, 570 | ]; 571 | }, 572 | }; 573 | 574 | const logality = new Logality({ 575 | appName: 'service-something', 576 | serializers: mySerializers, 577 | }); 578 | ``` 579 | 580 | ## Example of How To Initialize Logality on Your Project 581 | 582 | ### /app/services/logger.service.js 583 | 584 | This is the initializing module. During your application bootstrap and before 585 | you anything else, you need to require this module and invoke the `init()` 586 | function to initialize logality. 587 | 588 | ```js 589 | const Logality = require('logality'); 590 | 591 | const logger = (module.exports = {}); 592 | 593 | // Will store the logality reference. 594 | logger.logality = null; 595 | 596 | /** 597 | * Initialize the logging service. 598 | * 599 | * @param {Object} bootOpts boot options. This module will check for: 600 | * @param {string=} bootOpts.appName Set a custom appname for the logger. 601 | * @param {WriteStream|null} bootOpts.wstream Optionally define a custom 602 | * writable stream. 603 | */ 604 | logger.init = function (bootOpts = {}) { 605 | // check if already initialized. 606 | if (logger.logality) { 607 | return; 608 | } 609 | 610 | const appName = bootOpts.appName || 'app-name'; 611 | 612 | logger.logality = new Logality({ 613 | prettyPrint: process.env.ENV !== 'production', 614 | appName, 615 | wstream: bootOpts.wstream, 616 | }); 617 | 618 | // Create the get method 619 | logger.get = logger.logality.get.bind(logger.logality); 620 | }; 621 | ``` 622 | 623 | ### /app/model/user.model.js 624 | 625 | Then, in any module you want to log something you fetch the logality instance 626 | from your logger service. 627 | 628 | ```js 629 | const log = require('../services/log.service').get(); 630 | 631 | /* ... */ 632 | 633 | function register (userData) => { 634 | log.info('New user registration', { 635 | userData 636 | }); 637 | } 638 | ``` 639 | 640 | > **ℹ️ Note**: You can view a real-world example of Logality being used in production [in this Discord Bot Project](https://github1s.com/skgtech/skgbot/blob/HEAD/app/services/log.service.js). 641 | 642 | ## How Logality Compares to Other Loggers 643 | 644 | Comparison table as of 16th of April 2021. 645 | 646 | | | Logality | [Winston][winston] | [Bunyan][bunyan] | [Pino][pino] | 647 | | ------------------ | -------- | ------------------ | ---------------- | ------------ | 648 | | JSON Output | ✅ | ✅ | ✅ | ✅ | 649 | | Pretty Print | ✅ | ✅ | ❌ | ✅ | 650 | | Custom Log Levels | ❌ | ✅ | ✅ | ✅ | 651 | | Serializers | ✅ | ❌ | ✅ | ✅ | 652 | | Middleware | ✅ | ✅ | ❌ | ✅ | 653 | | Mutate JSON Schema | ✅ | ✅ | ❌ | ❌ | 654 | | Output Destination | ✅ | ✅ | ✅ | ✅ | 655 | | Mutate Output | ✅ | ✅ | ❌ | ❌ | 656 | | Async Operation | ✅ | ❌ | ❌ | ❌ | 657 | | Filename Detection | ✅ | ❌ | ❌ | ❌ | 658 | | Speed Optimised | ❌ | ❌ | ❌ | ✅ | 659 | | Used in Libraries | ✅ | ❌ | ❌ | ❌ | 660 | 661 | # Project Meta 662 | 663 | ## Releasing 664 | 665 | 1. Update the changelog bellow ("Release History"). 666 | 1. Ensure you are on master and your repository is clean. 667 | 1. Type: `npm run release` for patch version jump. 668 | - `npm run release:minor` for minor version jump. 669 | - `npm run release:major` for major major jump. 670 | 671 | ## Release History 672 | 673 | - **v3.1.3**, _19 Nov 2021_ 674 | - Will now safely JSON serialize BitInt values. Handles also edge case on pretty print. 675 | - Updated all dependencies to latest. 676 | - **v3.1.1**, _26 Sep 2021_ 677 | - Removed emojis for UTF-8 chars and corrected formating of pretty print. 678 | - **v3.1.0**, _26 Sep 2021_ 679 | - Added new options for pretty print (noTimestamp, noFilename, onlyMessage). 680 | - Added log level filtering. 681 | - Added codecoverage report. 682 | - Replaced figures package with emojis. 683 | - Updated all dependencies to latest. 684 | - **v3.0.4**, _31 May 2021_ 685 | - Updated all dependencies to latest. 686 | - Tweaked eslint and prettier configurations. 687 | - **v3.0.3**, _16 Apr 2021_ 688 | - Updated all dependencies to latest. 689 | - Tweaked, fixed and updated README, added comparison chart with popular loggers. 690 | - **v3.0.2**, _30 Oct 2020_ 691 | - Updated all dependencies to latest. 692 | - **v3.0.1**, _03 Jul 2020_ 693 | - Updated all dependencies to latest. 694 | - **v3.0.0**, _04 Apr 2020_ 695 | - Introduced [middleware][middleware] for pre-processing log messages. 696 | - Introduced the [pipe()][pipe] method to link multiple Logality 697 | instances together, enables using logality in dependencies and libraries. 698 | - **Breaking Change** Replaced "wstream" with ["output"][output] to 699 | customize logality's output. 700 | - **v2.1.2**, _24 Feb 2020_ 701 | - Removed http serializer when pretty print is enabled. 702 | - Replaced aged grunt with "release-it" for automated releasing. 703 | - **v2.1.1**, _19 Feb 2020_ 704 | - Added the "objectMode" configuration. 705 | - Implemented multi-key serializers feature. 706 | - Fixed async logging issues and tests. 707 | - **v2.1.0**, _18 Feb 2020_ 708 | - Added Async feature. 709 | - **v2.0.1**, _18 Feb 2020_ 710 | - Fixed issue with null http headers on sanitizer helper. 711 | - **v2.0.0**, _29 Jan 2020_ :: Extensible Serializers 712 | - Enables new serializers and allows over-writing the built-in ones. 713 | - Backwards compatible. 714 | - **v1.1.0**, _05 Jun 2018_ :: JSON Log Schema Version: 4.1.0 715 | - Added `prettyPrint` option, thank you [Marius](https://github.com/balajmarius). 716 | - **v1.0.0**, _21 May 2018_ :: JSON Log Schema Version: 4.1.0 717 | - Big Bang 718 | 719 | ## License 720 | 721 | Copyright Thanasis Polychronakis [Licensed under the ISC license](/LICENSE) 722 | 723 | [log-schema]: https://github.com/timberio/log-event-json-schema 724 | [iso8601]: https://en.wikipedia.org/wiki/ISO_8601 725 | [npm-image]: https://img.shields.io/npm/v/logality.svg 726 | [npm-url]: https://npmjs.org/package/logality 727 | [circle-image]: https://img.shields.io/circleci/build/gh/thanpolas/logality/master?label=Tests 728 | [circle-url]: https://circleci.com/gh/thanpolas/logality 729 | [stream-docs]: https://nodejs.org/api/stream.html#stream_object_mode 730 | [serializers]: #logality-serializers 731 | [async]: #about-asynchronous-logging 732 | [logality-flow]: #logality-execution-flow 733 | [middleware]: #use--add-middleware 734 | [output]: #the-custom-output-function 735 | [pipe]: #pipe--compose-multiple-logality-instances 736 | [comparison]: #how-logality-compares-to-other-loggers 737 | [winston]: https://github.com/winstonjs/winston 738 | [bunyan]: https://github.com/trentm/node-bunyan 739 | [pino]: https://github.com/pinojs/pino 740 | [log-levels]: #log-levels 741 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 3.0.0 | :white_check_mark: | 11 | | < 2.0 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Please report any severe vulnerabilities directly to the author, Thanasis Polychronakis at thanpolas@gmail.com 16 | -------------------------------------------------------------------------------- /app/export-wrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Wraps and exports the Logality class. 3 | */ 4 | 5 | const Logality = require('./logality'); 6 | const { writePretty } = require('./pretty-print'); 7 | 8 | /** 9 | * Wraps and returns a new instance of Logality. 10 | * 11 | * @param {...any} args Any argument[s]. 12 | * @return {Logality} Logality instance. 13 | */ 14 | function LogalityWrapper(...args) { 15 | const logality = new Logality(...args); 16 | 17 | logality.writePretty = writePretty; 18 | 19 | return logality; 20 | } 21 | 22 | module.exports = LogalityWrapper; 23 | -------------------------------------------------------------------------------- /app/functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Logality functional functions. 3 | */ 4 | 5 | const utils = require('./utils'); 6 | 7 | const fn = (module.exports = {}); 8 | 9 | /** 10 | * Simple function that returns the first argument it receives. 11 | * 12 | * @param {*} arg Any argument. 13 | * @return {*} The same argument. 14 | */ 15 | fn.returnArg = (arg) => arg; 16 | 17 | /** 18 | * This is where Log Contexts are born, isn't that cute? 19 | * 20 | * @param {string} level The level of the log. 21 | * @param {number} levelSeverity The level expressed in an index. 22 | * @param {string} message Human readable log message. 23 | * @param {string} filePath Path of module that the log originated from. 24 | * @param {string} appName The application name to use. 25 | * @return {Object} The log Context. 26 | */ 27 | fn.getContext = (level, levelSeverity, message, filePath, appName) => { 28 | return { 29 | level, 30 | severity: levelSeverity, 31 | dt: utils.getDt(), 32 | message, 33 | context: { 34 | runtime: { 35 | application: appName, 36 | }, 37 | source: { 38 | file_name: filePath, 39 | }, 40 | }, 41 | event: {}, 42 | }; 43 | }; 44 | 45 | /** 46 | * Write log to process standard out. 47 | * 48 | * @param {string} logMessage The log context to write. 49 | */ 50 | fn.output = function (logMessage) { 51 | process.stdout.write(logMessage); 52 | }; 53 | 54 | /** 55 | * Assign system-wide details. 56 | * 57 | * @param {Object} logContext The log record context. 58 | * @param {string} hostname The hostname of the machine. 59 | */ 60 | fn.assignSystem = (logContext, hostname) => { 61 | logContext.context.system = { 62 | hostname, 63 | pid: utils.getProcessId(), 64 | process_name: utils.getProcessName(), 65 | }; 66 | }; 67 | 68 | /** 69 | * Master serializer of object to be written to the output stream, basically 70 | * stringifies to JSON and adds a newline at the end. 71 | * 72 | * @param {Object} logContext The log context to write. 73 | * @return {string} Serialized message to output. 74 | */ 75 | fn.masterSerialize = (logContext) => { 76 | let strLogContext = utils.safeStringify(logContext); 77 | strLogContext += '\n'; 78 | 79 | return strLogContext; 80 | }; 81 | -------------------------------------------------------------------------------- /app/logality.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable security/detect-object-injection */ 2 | /** 3 | * Logality 4 | * Extensible JSON Logger. 5 | * https://github.com/thanpolas/logality 6 | * 7 | * Copyright © Thanos Polychronakis 8 | * Licensed under the ISC license. 9 | */ 10 | 11 | /** 12 | * @fileoverview bootstrap and master exporting module. 13 | */ 14 | 15 | /** 16 | * Custom JSDoc Type definitions 17 | */ 18 | 19 | /** 20 | * The logality instance. 21 | * 22 | * @typedef {(Object)} Logality 23 | */ 24 | 25 | /** 26 | * A writable stream. 27 | * 28 | * @typedef {(Object)} WriteStream 29 | */ 30 | 31 | const os = require('os'); 32 | 33 | const assign = require('lodash.assign'); 34 | const middlewarify = require('middlewarify'); 35 | 36 | const fn = require('./functions'); 37 | const prettyPrint = require('./pretty-print'); 38 | const utils = require('./utils'); 39 | const { version } = require('../package.json'); 40 | 41 | const userSerializer = require('./serializers/user.serializer'); 42 | const errorSerializer = require('./serializers/error.serializer'); 43 | const reqSerializer = require('./serializers/express-request.serializer'); 44 | const customSerializer = require('./serializers/custom.serializer'); 45 | 46 | /** @const {Array.} ALLOWED_LEVELS All levels, sequence MATTERS */ 47 | const ALLOWED_LEVELS = [ 48 | 'emergency', // Syslog level 0 49 | 'alert', // Syslog level 1 50 | 'critical', // Syslog level 2 51 | 'error', // Syslog level 3 52 | 'warn', // Syslog level 4 53 | 'notice', // Syslog level 5 54 | 'info', // Syslog level 6 55 | 'debug', // Syslog level 7 56 | ]; 57 | 58 | /** @type {string} Get the Current Working Directory of the app */ 59 | const CWD = process.cwd(); 60 | 61 | /** 62 | * The Logality Class 63 | * 64 | */ 65 | class Logality { 66 | /** 67 | * Initialize the logging service 68 | * 69 | * @param {Object} opts Set of options to configure Logality: 70 | * @param {string=} opts.appName The application name to log. 71 | * @param {function=} opts.output Overwrite the final output operation. 72 | * @param {boolean=} opts.async Use Asynchronous API returning a promise 73 | * on writes. 74 | * @param {boolean|Object=} opts.prettyPrint Enable and configure pretty print 75 | * to stdout, default false. 76 | * @param {number|string=} opts.minLevel Define the minimum log level. 77 | * @return {Logality} Logality instance. 78 | */ 79 | constructor(opts = {}) { 80 | // Force instantiation 81 | if (!(this instanceof Logality)) { 82 | // eslint-disable-next-line no-constructor-return 83 | return new Logality(opts); 84 | } 85 | 86 | /** @type {string} Store the current logality version */ 87 | this.version = version; 88 | 89 | /** @type {boolean} indicates if the current instance is piped to a parent */ 90 | this._isPiped = false; 91 | 92 | /** @type {?Logality} stores the parent logality instance when piped */ 93 | this._parentLogality = null; 94 | 95 | const outputHandler = opts.output || fn.returnArg; 96 | 97 | let minLevel = 7; 98 | if (typeof opts.minLevel === 'number') { 99 | minLevel = opts.minLevel; 100 | } else if (typeof opts.minLevel === 'string') { 101 | const level = ALLOWED_LEVELS.indexOf(opts.minLevel.toLowerCase()); 102 | if (level !== -1) { 103 | minLevel = level; 104 | } 105 | } 106 | 107 | /** @type {Object} Logality configuration */ 108 | this._opts = { 109 | appName: opts.appName || 'Logality', 110 | prettyPrint: opts.prettyPrint || false, 111 | async: opts.async || false, 112 | minLevel, 113 | }; 114 | 115 | // Create Middleware functionality 116 | middlewarify.make(this, '_middleware', outputHandler, { 117 | async: this._opts.async, 118 | }); 119 | this.use = this._middleware.use; 120 | 121 | /** @type {Object} Logality serializers */ 122 | this._serializers = { 123 | user: userSerializer, 124 | error: errorSerializer, 125 | req: reqSerializer, 126 | custom: customSerializer, 127 | }; 128 | 129 | if (opts.serializers) { 130 | this._serializers = assign(this._serializers, opts.serializers); 131 | } 132 | 133 | /** @type {string} Cache the hostname */ 134 | this._hostname = os.hostname(); 135 | } 136 | 137 | /** 138 | * Get a logger and hard-assign the filepath location of the invoking 139 | * module to populate with the rest of the log data. 140 | * 141 | * Do not reuse loggers returned from this function in multiple modules. 142 | * 143 | * @return {Logality.log} The log method partialed with the filePath of the 144 | * invoking module. 145 | */ 146 | get() { 147 | const filePath = this._getFilePath(); 148 | 149 | // Do a partial application on log and return it. 150 | const partialedLog = this.log.bind(this, filePath); 151 | 152 | // Attach log levels as methods 153 | ALLOWED_LEVELS.forEach((level) => { 154 | partialedLog[level] = this.log.bind(this, filePath, level); 155 | }); 156 | 157 | return partialedLog; 158 | } 159 | 160 | /** 161 | * The main logging method. 162 | * 163 | * @param {string} filePath Path of module that the log originated from. 164 | * @param {string} level The level of the log. 165 | * @param {string} message Human readable log message. 166 | * @param {Object|null} context Extra data to log. 167 | * @return {Promise|void} Returns promise when async opt is enabled. 168 | */ 169 | log(filePath, level, message, context) { 170 | const levelSeverity = ALLOWED_LEVELS.indexOf(level); 171 | if (levelSeverity === -1) { 172 | throw new Error('Invalid log level'); 173 | } 174 | 175 | // Check for level filtering 176 | if (levelSeverity > this._opts.minLevel) { 177 | return; 178 | } 179 | 180 | const logContext = fn.getContext( 181 | level, 182 | levelSeverity, 183 | message, 184 | filePath, 185 | this._opts.appName, 186 | ); 187 | 188 | fn.assignSystem(logContext, this._hostname); 189 | 190 | this._applySerializers(logContext, context); 191 | return this.invokeOutput(logContext); 192 | } 193 | 194 | /** 195 | * Invokes any defined middleware and the output methods, custom or built-in 196 | * depending on configuration. 197 | * 198 | * @param {Object|*} logContext The log context or any value a piped child 199 | * might output, usually either string or undefined. 200 | * @param {Object=} pipedCall Set to true if this method is invoked from 201 | * piped child. 202 | * @return {Promise|void} Returns promise when async opt is enabled. 203 | */ 204 | invokeOutput(logContext, pipedCall = false) { 205 | // run Middleware, they can mutate the logContext. 206 | const result = this._middleware(logContext, pipedCall); 207 | 208 | if (this._opts.async) { 209 | return this._handleAsync(result); 210 | } 211 | 212 | this._handleOutput(result); 213 | } 214 | 215 | /** 216 | * Pipes output of other logality instances to this one. 217 | * 218 | * @param {Logality|Array} logality Single or multiple logality 219 | * instances. 220 | */ 221 | pipe(logality) { 222 | let logalities = []; 223 | 224 | if (Array.isArray(logality)) { 225 | logalities = logality; 226 | } else { 227 | logalities.push(logality); 228 | } 229 | 230 | logalities.forEach((logalityInstance) => { 231 | if (!logalityInstance.version) { 232 | throw new Error('Argument passed not a Logality instance'); 233 | } 234 | 235 | logalityInstance.youArePiped(this); 236 | }); 237 | } 238 | 239 | /** 240 | * Internal method that's invoked by the parent logality instance when 241 | * pipe() method is used, instructs the current logality instance to pipe 242 | * the LogContext to the parent. 243 | * 244 | * @param {Logality} parentLogality The parent logality. 245 | */ 246 | youArePiped(parentLogality) { 247 | if (!parentLogality.version) { 248 | throw new Error( 249 | 'Argument passed for youArePiped() not a Logality instance', 250 | ); 251 | } 252 | if (this._isPiped) { 253 | throw new Error('This instance is already piped to another parent'); 254 | } 255 | this._isPiped = true; 256 | this._parentLogality = parentLogality; 257 | } 258 | 259 | /** 260 | * Handles asynchronous output of the log context. 261 | * 262 | * @param {Promise} prom Promise outcome of custom output 263 | * or logContext. 264 | * @return {Promise} A Promise. 265 | * @private 266 | */ 267 | async _handleAsync(prom) { 268 | this._handleOutput(await prom); 269 | } 270 | 271 | /** 272 | * Final output handler method, determines if the log message needs 273 | * further processing or output to standard out. 274 | * 275 | * @param {Object|string|void} result Outcome of custom output or logContext. 276 | */ 277 | _handleOutput(result) { 278 | if (this._isPiped && result) { 279 | this._parentLogality.invokeOutput(result, true); 280 | return; 281 | } 282 | 283 | let logMessage; 284 | switch (typeof result) { 285 | case 'string': 286 | fn.output(result); 287 | break; 288 | case 'object': 289 | logMessage = this._getLogMessage(result); 290 | fn.output(logMessage); 291 | break; 292 | default: 293 | // no action 294 | break; 295 | } 296 | } 297 | 298 | /** 299 | * Apply serializers on context contents. 300 | * 301 | * @param {Object} logContext The log context to write. 302 | * @param {Object|null} context Extra data to log. 303 | */ 304 | _applySerializers(logContext, context) { 305 | if (!context) { 306 | return; 307 | } 308 | 309 | const contextKeys = Object.keys(context); 310 | 311 | contextKeys.forEach(function (key) { 312 | if (this._serializers[key]) { 313 | const res = this._serializers[key](context[key]); 314 | 315 | if (Array.isArray(res)) { 316 | res.forEach(function (serial) { 317 | utils.assignPath(serial.path, logContext, serial.value); 318 | }); 319 | } else { 320 | utils.assignPath(res.path, logContext, res.value); 321 | } 322 | } 323 | }, this); 324 | } 325 | 326 | /** 327 | * Determines the kind of output to be send downstream to the writable stream. 328 | * 329 | * 330 | * @param {Object} logContext The log context to serialize to string. 331 | * @return {string} Log Message to be output. 332 | * @private 333 | */ 334 | _getLogMessage(logContext) { 335 | let stringOutput = ''; 336 | if (this._opts.prettyPrint) { 337 | stringOutput = prettyPrint.writePretty( 338 | logContext, 339 | this._opts.prettyPrint, 340 | ); 341 | } else { 342 | stringOutput = fn.masterSerialize(logContext); 343 | } 344 | 345 | return stringOutput; 346 | } 347 | 348 | /** 349 | * Get the relative filepath to the invoking module. 350 | * 351 | * @return {string} Relative filepath of callee. 352 | * @private 353 | */ 354 | _getFilePath() { 355 | try { 356 | throw new Error(); 357 | } catch (ex) { 358 | const stackLines = ex.stack.split('\n'); 359 | 360 | // Select the invoking module from the stack trace lines. 361 | // This is highly arbitrary based on how / when this function 362 | // got invoked, however once set for a codebase it will remain constant. 363 | const invokerRaw = stackLines[3]; 364 | 365 | const startSplit = invokerRaw.split('('); 366 | const finalSplit = startSplit[1].split(':'); 367 | const invokerPath = finalSplit[0]; 368 | 369 | // invokerPath now stores the full path, we need to extract the 370 | // relative path of the app which is the "root folder" of the app. 371 | const filePath = invokerPath.substr(CWD.length); 372 | 373 | return filePath; 374 | } 375 | } 376 | } 377 | 378 | module.exports = Logality; 379 | -------------------------------------------------------------------------------- /app/pretty-print.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable security/detect-object-injection */ 2 | /** 3 | * @fileoverview Handles pretty printing for logality, used in 4 | * local development. 5 | */ 6 | const chalk = require('chalk'); 7 | const format = require('json-format'); 8 | 9 | const { isObjectEmpty, safeStringify } = require('./utils'); 10 | 11 | /** @const {Object} LEVELS_CONFIG Levels colors and icons */ 12 | const LEVELS_CONFIG = { 13 | emergency: { 14 | color: chalk.red.underline, 15 | icon: '●', 16 | }, 17 | alert: { 18 | color: chalk.red.underline, 19 | icon: '◆', 20 | }, 21 | critical: { 22 | color: chalk.red, 23 | icon: '✖', 24 | }, 25 | error: { 26 | color: chalk.red, 27 | icon: '■', 28 | }, 29 | warn: { 30 | color: chalk.yellow, 31 | icon: '⚠', 32 | }, 33 | notice: { 34 | color: chalk.cyan, 35 | icon: '▶', 36 | }, 37 | info: { 38 | color: chalk.blue, 39 | icon: 'ℹ', 40 | }, 41 | debug: { 42 | color: chalk.green, 43 | icon: '★', 44 | }, 45 | }; 46 | 47 | /** 48 | * Keys to ignore on context object when pretty printing 49 | * 50 | * @const {Array} CONTEXT_IGNORE_KEYS 51 | */ 52 | const CONTEXT_IGNORE_KEYS = ['runtime', 'source', 'system']; 53 | 54 | /** 55 | * Keys to ignore on context object when pretty printing 56 | * 57 | * @const {Array} EVENT_IGNORE_KEYS 58 | */ 59 | const EVENT_IGNORE_KEYS = ['http_request']; 60 | 61 | /** 62 | * Write prettified log to selected output. 63 | * 64 | * @param {Object} logContext The log context to write. 65 | * @param {Object=} prettyOpts Rendering options. 66 | * @param {boolean=} prettyOpts.noTimestamp Set to true to not log timestamps. 67 | * @param {boolean=} prettyOpts.noFilename Set to true to not log filename. 68 | * @param {boolean=} prettyOpts.onlyMessage Set to true to only log the message 69 | * and not the context. 70 | * @param {boolean=} prettyOpts.noColor Do not color the logs. 71 | * @return {string} Formatted output. 72 | * @private 73 | */ 74 | exports.writePretty = function (logContext, prettyOpts = {}) { 75 | const { noTimestamp, noFilename, onlyMessage, noColor } = prettyOpts; 76 | 77 | // current level icon and color 78 | const levelsConfig = LEVELS_CONFIG[logContext.level]; 79 | 80 | const file = noFilename 81 | ? '' 82 | : ` ${exports._applyColor( 83 | chalk.underline.green, 84 | logContext.context.source.file_name, 85 | noColor, 86 | )}`; 87 | 88 | const date = noTimestamp 89 | ? '' 90 | : exports._applyColor(chalk.white, `[${logContext.dt}] `, noColor); 91 | 92 | const level = exports._applyColor( 93 | levelsConfig.color, 94 | `${levelsConfig.icon} ${logContext.level}`, 95 | noColor, 96 | ); 97 | const message = exports._applyColor( 98 | levelsConfig.color, 99 | logContext.message, 100 | noColor, 101 | ); 102 | 103 | const logs = onlyMessage ? '' : exports._getLogs(logContext); 104 | 105 | const output = `${date}${level}${file} - ${message}\n${logs}`; 106 | 107 | return output; 108 | }; 109 | 110 | /** 111 | * Will apply the color conditionally upon the provided noColor argument/ 112 | * 113 | * @param {function} colorFn The color function. 114 | * @param {string} string The string to color or not. 115 | * @param {boolean} noColor Set to true to not apply color. 116 | * @return {string} formatted string. 117 | * @private 118 | */ 119 | exports._applyColor = (colorFn, string, noColor) => { 120 | if (noColor) { 121 | return string; 122 | } 123 | 124 | return colorFn(string); 125 | }; 126 | 127 | /** 128 | * Returns formatted logs for pretty print. 129 | * 130 | * @param {Object} logContext The log context to format. 131 | * @return {string} Log output. 132 | * @private 133 | */ 134 | exports._getLogs = function (logContext) { 135 | const logs = {}; 136 | 137 | const { event, context } = logContext; 138 | 139 | const eventKeys = Object.keys(event); 140 | const contextKeys = Object.keys(context); 141 | 142 | contextKeys.forEach((key) => { 143 | if (CONTEXT_IGNORE_KEYS.includes(key)) { 144 | return; 145 | } 146 | 147 | if (logs.context) { 148 | logs.context[key] = context[key]; 149 | } else { 150 | logs.context = { 151 | [key]: context[key], 152 | }; 153 | } 154 | }); 155 | 156 | eventKeys.forEach((eventKey) => { 157 | if (EVENT_IGNORE_KEYS.includes(eventKey)) { 158 | return; 159 | } 160 | 161 | if (logs.event) { 162 | logs.event[eventKey] = event[eventKey]; 163 | } else { 164 | logs.event = { 165 | [eventKey]: event[eventKey], 166 | }; 167 | } 168 | }); 169 | 170 | // empty string if the logs are emtpy 171 | if (isObjectEmpty(logs)) { 172 | return ''; 173 | } 174 | 175 | // Perform a safe serialization so that any BigInt values are safely serialized 176 | // into strings, and then deserialize back to object. 177 | // 178 | // The performance hit can be afforded when pretty printing as it is only 179 | // used on development. 180 | const logsSerialized = safeStringify(logs); 181 | const logsSafe = JSON.parse(logsSerialized); 182 | 183 | const prettyLogs = format(logsSafe, { type: 'space', size: 2 }); 184 | 185 | return `${prettyLogs}\n`; 186 | }; 187 | -------------------------------------------------------------------------------- /app/serializers/custom.serializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom Object Serializer. 3 | * 4 | * @param {*} custom Any value. 5 | * @return {Object} Properly serialized for Logging Schema. 6 | */ 7 | module.exports = function (custom) { 8 | const result = { 9 | path: 'context.custom', 10 | value: custom, 11 | }; 12 | 13 | return result; 14 | }; 15 | -------------------------------------------------------------------------------- /app/serializers/error.serializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error Object Serializer. 3 | * 4 | * @param {Error} error Javascript Error or Exception. 5 | * @return {Object} Properly serialized for Logging Schema. 6 | */ 7 | module.exports = function (error) { 8 | const result = { 9 | path: 'event.error', 10 | value: { 11 | name: error.name, 12 | message: error.message, 13 | backtrace: null, 14 | }, 15 | }; 16 | 17 | if (error.stack) { 18 | result.value.backtrace = error.stack; 19 | } 20 | 21 | return result; 22 | }; 23 | -------------------------------------------------------------------------------- /app/serializers/express-request.serializer.js: -------------------------------------------------------------------------------- 1 | const { sanitizeHttpHeaders, safeStringify } = require('../utils'); 2 | 3 | /** 4 | * Express Request Object Serializer. 5 | * 6 | * @param {Express.Request} req Express request object. 7 | * @return {Object} Properly serialized for Logging Schema. 8 | */ 9 | module.exports = function (req) { 10 | const result = { 11 | path: 'event.http_request', 12 | value: { 13 | headers: sanitizeHttpHeaders(req.headers), 14 | host: req.hostname, 15 | method: req.method, 16 | path: req.path, 17 | query_string: safeStringify(req.query), 18 | scheme: req.secure ? 'https' : 'http', 19 | }, 20 | }; 21 | 22 | return result; 23 | }; 24 | -------------------------------------------------------------------------------- /app/serializers/user.serializer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Standard User serializer. 3 | * 4 | * @param {Object} udo User Data Object. 5 | * @return {Object} Properly serialized for Logging Schema. 6 | */ 7 | module.exports = function (udo) { 8 | return { 9 | path: 'context.user', 10 | value: { 11 | id: udo.id, 12 | email: udo.email, 13 | }, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /app/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable security/detect-object-injection */ 2 | const utils = (module.exports = {}); 3 | 4 | /** 5 | * Check if object is empty 6 | * 7 | * @param {Object} obj The object to examine. 8 | * @return {boolean} True if it is empty. 9 | */ 10 | utils.isObjectEmpty = function (obj) { 11 | return Object.keys(obj).length === 0 && obj.constructor === Object; 12 | }; 13 | 14 | /** 15 | * Assigns the value on the target using the path. 16 | * 17 | * @param {string} path Dot notation path to save the value. 18 | * @param {Object} target The target object to save the value on. 19 | * @param {*} value The value to save. 20 | * @return {Object} The target object. 21 | */ 22 | utils.assignPath = function (path, target, value) { 23 | const parts = path.split('.'); 24 | 25 | if (parts.length === 1) { 26 | // Save on root 27 | target[path] = value; 28 | return target; 29 | } 30 | 31 | let ref = target; 32 | parts.forEach(function (part, index) { 33 | if (index === parts.length - 1) { 34 | ref[part] = value; 35 | } else { 36 | ref[part] = ref[part] || {}; 37 | ref = ref[part]; 38 | } 39 | }); 40 | 41 | return target; 42 | }; 43 | 44 | /** 45 | * Returns the process name, required for testing. 46 | * 47 | * @return {string} The process name. 48 | */ 49 | utils.getProcessName = function () { 50 | return process.argv[0]; 51 | }; 52 | 53 | /** 54 | * Returns the process id, required for testing. 55 | * 56 | * @return {string} The process id. 57 | */ 58 | utils.getProcessId = function () { 59 | return process.pid; 60 | }; 61 | 62 | /** 63 | * Clean HTTP Headers from sensitive data. 64 | * 65 | * @param {Object} headers The headers. 66 | * @return {Object} Sanitized headers. 67 | */ 68 | utils.sanitizeHttpHeaders = function (headers) { 69 | if (typeof headers !== 'object') { 70 | return headers; 71 | } 72 | const REMOVE = ['cookie', 'authorization']; 73 | 74 | REMOVE.forEach(function (header) { 75 | if (headers[header]) { 76 | headers[header] = '-- REMOVED FOR SAFETY --'; 77 | } 78 | }); 79 | 80 | return headers; 81 | }; 82 | 83 | /** 84 | * Return an ISO8601 formatted date. 85 | * 86 | * @return {string} The formatted date. 87 | */ 88 | utils.getDt = function () { 89 | const dt = new Date(); 90 | return dt.toISOString(); 91 | }; 92 | 93 | /** 94 | * Will safely JSON serialize any value to JSON, accounting for BigInt. 95 | * 96 | * @param {*} obj Any value to serialize. 97 | * @return {string} 98 | */ 99 | utils.safeStringify = (obj) => { 100 | return JSON.stringify(obj, (key, value) => 101 | typeof value === 'bigint' ? value.toString() : value, 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /assets/logality_flow_chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanpolas/logality/e90f3d9ae52ab723293f7c34026990622be9c74c/assets/logality_flow_chart.png -------------------------------------------------------------------------------- /assets/logality_flow_chart_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanpolas/logality/e90f3d9ae52ab723293f7c34026990622be9c74c/assets/logality_flow_chart_preview.png -------------------------------------------------------------------------------- /assets/logality_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thanpolas/logality/e90f3d9ae52ab723293f7c34026990622be9c74c/assets/logality_preview.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logality", 3 | "version": "3.1.3", 4 | "main": "./app/export-wrapper", 5 | "description": "Extensible JSON Logger", 6 | "homepage": "https://github.com/thanpolas/logality", 7 | "bugs": "https://github.com/thanpolas/logality/issues", 8 | "author": { 9 | "name": "Thanasis Polychronakis", 10 | "email": "thanpolas@gmail.com" 11 | }, 12 | "contributors": [ 13 | "Marius Balaj" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/thanpolas/logality" 18 | }, 19 | "license": "ISC", 20 | "engines": { 21 | "node": ">=10.1.x" 22 | }, 23 | "scripts": { 24 | "test": "jest && eslint app test && codecov", 25 | "release": "release-it --ci", 26 | "release:minor": "release-it minor --ci", 27 | "release:major": "release-it major --ci", 28 | "eslint": "eslint app test" 29 | }, 30 | "release-it": { 31 | "github": { 32 | "release": false 33 | } 34 | }, 35 | "jest": { 36 | "coverageDirectory": "./coverage/", 37 | "collectCoverage": true, 38 | "collectCoverageFrom": [ 39 | "./app/*.js", 40 | "./app/**/*.js" 41 | ], 42 | "coverageReporters": [ 43 | "html", 44 | "json" 45 | ], 46 | "roots": [ 47 | "./test/spec", 48 | "./test/serializers" 49 | ], 50 | "testEnvironment": "node", 51 | "setupFilesAfterEnv": [ 52 | "jest-extended/all" 53 | ] 54 | }, 55 | "dependencies": { 56 | "chalk": "^4.1.2", 57 | "codecov": "^3.8.3", 58 | "json-format": "^1.0.1", 59 | "lodash.assign": "^4.2.0", 60 | "middlewarify": "^2.2.0" 61 | }, 62 | "prettier": { 63 | "singleQuote": true, 64 | "trailingComma": "all", 65 | "printWidth": 80 66 | }, 67 | "devDependencies": { 68 | "@types/jest": "28.1.1", 69 | "eslint": "8.17.0", 70 | "eslint-config-airbnb-base": "15.0.0", 71 | "eslint-config-prettier": "8.5.0", 72 | "eslint-plugin-import": "2.26.0", 73 | "eslint-plugin-jest": "26.5.3", 74 | "eslint-plugin-jsdoc": "39.3.2", 75 | "eslint-plugin-prettier": "4.0.0", 76 | "eslint-plugin-security": "1.5.0", 77 | "jest": "28.1.1", 78 | "jest-extended": "2.0.0", 79 | "prettier": "2.6.2", 80 | "release-it": "14.11.7", 81 | "sinon": "14.0.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | jsdoc/require-jsdoc: 0 3 | -------------------------------------------------------------------------------- /test/fixtures/log-samples.fix.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Samples of JSON logs. 3 | */ 4 | 5 | exports.infoLogJsonFix = 6 | '{"level":"info","severity":6,"dt":"2018-05-18T16:25:57.815Z","message":"hello pretty world","context":{"runtime":{"application":"testLogality"},"source":{"file_name":"/test/spec/writepretty.test.js"},"system":{"hostname":"localhost","pid":36255,"process_name":"node ."}},"event":{}}'; 7 | exports.infoLogFix = () => JSON.parse(exports.infoLogJsonFix); 8 | 9 | exports.errorLogJsonFix = 10 | '{"level":"error","severity":3,"dt":"2018-05-18T16:25:57.815Z","message":"Error log","context":{"runtime":{"application":"testLogality"},"source":{"file_name":"/test/spec/writepretty.test.js"},"system":{"hostname":"localhost","pid":36255,"process_name":"node ."}},"event":{"error":{"name":"Error","message":"an error occured","backtrace":"Error: an error occured\\n at Object. (/Users/thanpolas/Projects/libs/logality/test/spec/writepretty.test.js:23:17)\\n at Promise.then.completed (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/utils.js:333:28)\\n at new Promise ()\\n at callAsyncCircusFn (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/utils.js:259:10)\\n at _callCircusTest (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:276:40)\\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\\n at _runTest (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:208:3)\\n at _runTestsForDescribeBlock (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:96:9)\\n at _runTestsForDescribeBlock (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:90:9)\\n at run (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:31:3)"}}}'; 11 | exports.errorLogFix = () => JSON.parse(exports.errorLogJsonFix); 12 | -------------------------------------------------------------------------------- /test/lib/tester.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Main testing helper lib. 3 | */ 4 | const os = require('os'); 5 | 6 | const sinon = require('sinon'); 7 | 8 | const utils = require('../../app/utils'); 9 | 10 | const tester = (module.exports = {}); 11 | 12 | tester.DT_VAL = '2018-05-18T16:25:57.815Z'; 13 | tester.PID_VAL = 36255; 14 | 15 | /** 16 | * Have a Cooldown period between tests. 17 | * 18 | * @param {number} seconds cooldown in seconds. 19 | * @return {function} use is beforeEach(). 20 | */ 21 | tester.cooldown = function (seconds = 0) { 22 | return function (done) { 23 | setTimeout(done, seconds); 24 | }; 25 | }; 26 | 27 | /** 28 | * Stub Logality so it can be properly tested with snapshots. 29 | * 30 | */ 31 | tester.stubLogality = function () { 32 | let dateStub; 33 | let processStub; 34 | let hostnameStub; 35 | let processNameStub; 36 | beforeEach(() => { 37 | dateStub = sinon.stub(Date.prototype, 'toISOString'); 38 | dateStub.returns(tester.DT_VAL); 39 | hostnameStub = sinon.stub(os, 'hostname'); 40 | hostnameStub.returns('localhost'); 41 | processStub = sinon.stub(utils, 'getProcessId'); 42 | processStub.returns(tester.PID_VAL); 43 | processNameStub = sinon.stub(utils, 'getProcessName'); 44 | processNameStub.returns('node .'); 45 | }); 46 | 47 | afterEach(() => { 48 | hostnameStub.restore(); 49 | dateStub.restore(); 50 | processNameStub.restore(); 51 | processStub.restore(); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /test/serializers/__snapshots__/custom-serializers.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Custom Serializers The build-in custom serializer 1`] = ` 4 | Object { 5 | "context": Object { 6 | "custom": Object { 7 | "a": 1, 8 | "b": 2, 9 | }, 10 | "runtime": Object { 11 | "application": "testLogality", 12 | }, 13 | "source": Object { 14 | "file_name": "/test/serializers/custom-serializers.test.js", 15 | }, 16 | "system": Object { 17 | "hostname": "localhost", 18 | "pid": 36255, 19 | "process_name": "node .", 20 | }, 21 | }, 22 | "dt": "2018-05-18T16:25:57.815Z", 23 | "event": Object {}, 24 | "level": "info", 25 | "message": "hello world", 26 | "severity": 6, 27 | } 28 | `; 29 | 30 | exports[`Custom Serializers Will log custom serializer on context 1`] = ` 31 | Object { 32 | "context": Object { 33 | "runtime": Object { 34 | "application": "testLogality", 35 | }, 36 | "source": Object { 37 | "file_name": "/test/serializers/custom-serializers.test.js", 38 | }, 39 | "system": Object { 40 | "hostname": "localhost", 41 | "pid": 36255, 42 | "process_name": "node .", 43 | }, 44 | "zit": Object { 45 | "id": 12, 46 | "property": "zero", 47 | "value": 0.23, 48 | }, 49 | }, 50 | "dt": "2018-05-18T16:25:57.815Z", 51 | "event": Object {}, 52 | "level": "info", 53 | "message": "hello world", 54 | "severity": 6, 55 | } 56 | `; 57 | 58 | exports[`Custom Serializers Will log custom serializer on root 1`] = ` 59 | Object { 60 | "context": Object { 61 | "runtime": Object { 62 | "application": "testLogality", 63 | }, 64 | "source": Object { 65 | "file_name": "/test/serializers/custom-serializers.test.js", 66 | }, 67 | "system": Object { 68 | "hostname": "localhost", 69 | "pid": 36255, 70 | "process_name": "node .", 71 | }, 72 | }, 73 | "dt": "2018-05-18T16:25:57.815Z", 74 | "event": Object {}, 75 | "level": "info", 76 | "message": "hello world", 77 | "severity": 6, 78 | "zit": Object { 79 | "id": 12, 80 | "property": "zero", 81 | "value": 0.23, 82 | }, 83 | } 84 | `; 85 | -------------------------------------------------------------------------------- /test/serializers/__snapshots__/multi-key-serializers.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Multi-Key Serializers Will log multi-key serializer on context 1`] = ` 4 | Object { 5 | "context": Object { 6 | "nit": Object { 7 | "id": 12, 8 | "property": "zero", 9 | "value": 0.23, 10 | }, 11 | "runtime": Object { 12 | "application": "testLogality", 13 | }, 14 | "source": Object { 15 | "file_name": "/test/serializers/multi-key-serializers.test.js", 16 | }, 17 | "system": Object { 18 | "hostname": "localhost", 19 | "pid": 36255, 20 | "process_name": "node .", 21 | }, 22 | "zit": Object { 23 | "id": 12, 24 | "property": "zero", 25 | "value": 0.23, 26 | }, 27 | }, 28 | "dt": "2018-05-18T16:25:57.815Z", 29 | "event": Object {}, 30 | "level": "info", 31 | "message": "hello world", 32 | "rootpath": Object { 33 | "id": 12, 34 | "property": "zero", 35 | "value": 0.23, 36 | }, 37 | "severity": 6, 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /test/serializers/__snapshots__/user-custom-serializer.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Custom User Serializer Will log custom UDO 1`] = ` 4 | Object { 5 | "context": Object { 6 | "runtime": Object { 7 | "application": "testLogality", 8 | }, 9 | "source": Object { 10 | "file_name": "/test/serializers/user-custom-serializer.test.js", 11 | }, 12 | "system": Object { 13 | "hostname": "localhost", 14 | "pid": 36255, 15 | "process_name": "node .", 16 | }, 17 | "user": Object { 18 | "email": "one@go.com", 19 | "id": 10, 20 | }, 21 | }, 22 | "dt": "2018-05-18T16:25:57.815Z", 23 | "event": Object {}, 24 | "level": "info", 25 | "message": "hello world", 26 | "severity": 6, 27 | } 28 | `; 29 | 30 | exports[`Custom User Serializer Will log custom UDO on custom path 1`] = ` 31 | Object { 32 | "context": Object { 33 | "runtime": Object { 34 | "application": "testLogality", 35 | }, 36 | "source": Object { 37 | "file_name": "/test/serializers/user-custom-serializer.test.js", 38 | }, 39 | "system": Object { 40 | "hostname": "localhost", 41 | "pid": 36255, 42 | "process_name": "node .", 43 | }, 44 | }, 45 | "dt": "2018-05-18T16:25:57.815Z", 46 | "event": Object {}, 47 | "level": "info", 48 | "message": "hello world", 49 | "severity": 6, 50 | "user": Object { 51 | "email": "one@go.com", 52 | "id": 10, 53 | }, 54 | } 55 | `; 56 | -------------------------------------------------------------------------------- /test/serializers/__snapshots__/user.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`User Data Object Logging Will log UDO Properly by default 1`] = ` 4 | Object { 5 | "context": Object { 6 | "runtime": Object { 7 | "application": "testLogality", 8 | }, 9 | "source": Object { 10 | "file_name": "/test/serializers/user.test.js", 11 | }, 12 | "system": Object { 13 | "hostname": "localhost", 14 | "pid": 36255, 15 | "process_name": "node .", 16 | }, 17 | "user": Object { 18 | "email": "one@go.com", 19 | "id": 10, 20 | }, 21 | }, 22 | "dt": "2018-05-18T16:25:57.815Z", 23 | "event": Object {}, 24 | "level": "info", 25 | "message": "hello world", 26 | "severity": 6, 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /test/serializers/custom-serializers.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test custom serializers. 3 | */ 4 | const Logality = require('../../app/logality'); 5 | const { stubLogality } = require('../lib/tester.lib'); 6 | 7 | const DATA = { 8 | id: 12, 9 | property: 'zero', 10 | value: 0.23, 11 | }; 12 | 13 | describe('Custom Serializers', () => { 14 | stubLogality(); 15 | 16 | test('The build-in custom serializer', () => { 17 | let outputDone = false; 18 | const logality = new Logality({ 19 | appName: 'testLogality', 20 | output: (logMessage) => { 21 | expect(logMessage).toBeObject(); 22 | expect(logMessage).toMatchSnapshot(); 23 | outputDone = true; 24 | }, 25 | }); 26 | 27 | const log = logality.get(); 28 | 29 | log('info', 'hello world', { custom: { a: 1, b: 2 } }); 30 | expect(outputDone).toBeTrue(); 31 | }); 32 | 33 | test('Will log custom serializer on context', () => { 34 | const serializers = { 35 | zit: (data) => { 36 | return { 37 | path: 'context.zit', 38 | value: data, 39 | }; 40 | }, 41 | }; 42 | 43 | let outputDone = false; 44 | const logality = new Logality({ 45 | appName: 'testLogality', 46 | serializers, 47 | output: (logMessage) => { 48 | expect(logMessage).toMatchSnapshot(); 49 | outputDone = true; 50 | }, 51 | }); 52 | 53 | const log = logality.get(); 54 | 55 | log('info', 'hello world', { zit: DATA }); 56 | expect(outputDone).toBeTrue(); 57 | }); 58 | 59 | test('Will log custom serializer on root', () => { 60 | const serializers = { 61 | zit: (data) => { 62 | return { 63 | path: 'zit', 64 | value: data, 65 | }; 66 | }, 67 | }; 68 | 69 | let outputDone = false; 70 | const logality = new Logality({ 71 | appName: 'testLogality', 72 | serializers, 73 | output: (logMessage) => { 74 | expect(logMessage).toMatchSnapshot(); 75 | outputDone = true; 76 | }, 77 | }); 78 | 79 | const log = logality.get(); 80 | 81 | log('info', 'hello world', { zit: DATA }); 82 | expect(outputDone).toBeTrue(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/serializers/error-serializer.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test logging a JS error with error stack. 3 | */ 4 | const Logality = require('../../app/logality'); 5 | const { stubLogality } = require('../lib/tester.lib'); 6 | 7 | describe('Error Stack Testing', () => { 8 | stubLogality(); 9 | 10 | test('Will properly figure out invoking function module on error', () => { 11 | let outputDone = false; 12 | const logality = new Logality({ 13 | appName: 'testLogality', 14 | objectMode: true, 15 | output: (logContext) => { 16 | expect(logContext).toHaveProperty('severity', 3); 17 | expect(logContext).toHaveProperty( 18 | 'message', 19 | 'An error happened, maybe?', 20 | ); 21 | expect(logContext).toHaveProperty('level', 'error'); 22 | expect(logContext).toHaveProperty('event'); 23 | expect(logContext).toHaveProperty('dt', '2018-05-18T16:25:57.815Z'); 24 | expect(logContext).toHaveProperty('context'); 25 | 26 | expect(logContext.event).toHaveProperty('error'); 27 | expect(logContext.event.error).toHaveProperty('backtrace'); 28 | expect(logContext.event.error).toHaveProperty( 29 | 'message', 30 | 'An error did not occur', 31 | ); 32 | expect(logContext.event.error).toHaveProperty('name', 'Error'); 33 | outputDone = true; 34 | }, 35 | }); 36 | 37 | const log = logality.get(); 38 | 39 | const error = new Error('An error did not occur'); 40 | log('error', 'An error happened, maybe?', { 41 | error, 42 | }); 43 | 44 | expect(outputDone).toBeTrue(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/serializers/multi-key-serializers.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test multi-key serializers. 3 | */ 4 | const Logality = require('../../app/logality'); 5 | const { stubLogality } = require('../lib/tester.lib'); 6 | 7 | const DATA = { 8 | id: 12, 9 | property: 'zero', 10 | value: 0.23, 11 | }; 12 | 13 | describe('Multi-Key Serializers', () => { 14 | stubLogality(); 15 | 16 | test('Will log multi-key serializer on context', () => { 17 | const serializers = { 18 | zit: (data) => { 19 | return [ 20 | { 21 | path: 'context.zit', 22 | value: data, 23 | }, 24 | { 25 | path: 'context.nit', 26 | value: data, 27 | }, 28 | { 29 | path: 'rootpath', 30 | value: data, 31 | }, 32 | ]; 33 | }, 34 | }; 35 | 36 | let outputDone = false; 37 | const logality = new Logality({ 38 | appName: 'testLogality', 39 | serializers, 40 | output: (logMessage) => { 41 | expect(logMessage).toMatchSnapshot(); 42 | outputDone = true; 43 | }, 44 | }); 45 | 46 | const log = logality.get(); 47 | 48 | log('info', 'hello world', { zit: DATA }); 49 | expect(outputDone).toBeTrue(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/serializers/user-custom-serializer.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test custom user serializer 3 | */ 4 | const Logality = require('../../app/logality'); 5 | const { stubLogality } = require('../lib/tester.lib'); 6 | 7 | const UDO_MOCK = { 8 | userId: 10, 9 | userEmail: 'one@go.com', 10 | }; 11 | 12 | describe('Custom User Serializer', () => { 13 | stubLogality(); 14 | 15 | test('Will log custom UDO', () => { 16 | const serializers = { 17 | user: (udo) => { 18 | return { 19 | path: 'context.user', 20 | value: { 21 | id: udo.userId, 22 | email: udo.userEmail, 23 | }, 24 | }; 25 | }, 26 | }; 27 | 28 | let outputDone = false; 29 | const logality = new Logality({ 30 | appName: 'testLogality', 31 | serializers, 32 | output: (chunk) => { 33 | expect(chunk).toMatchSnapshot(); 34 | outputDone = true; 35 | }, 36 | }); 37 | 38 | const log = logality.get(); 39 | 40 | log('info', 'hello world', { user: UDO_MOCK }); 41 | expect(outputDone).toBeTrue(); 42 | }); 43 | 44 | test('Will log custom UDO on custom path', () => { 45 | const serializers = { 46 | user: (udo) => { 47 | return { 48 | path: 'user', 49 | value: { 50 | id: udo.userId, 51 | email: udo.userEmail, 52 | }, 53 | }; 54 | }, 55 | }; 56 | 57 | let outputDone = false; 58 | const logality = new Logality({ 59 | appName: 'testLogality', 60 | serializers, 61 | output: (chunk) => { 62 | expect(chunk).toMatchSnapshot(); 63 | outputDone = true; 64 | }, 65 | }); 66 | 67 | const log = logality.get(); 68 | 69 | log('info', 'hello world', { user: UDO_MOCK }); 70 | expect(outputDone).toBeTrue(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/serializers/user.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test logging of user data objects. 3 | */ 4 | const Logality = require('../..'); 5 | const { stubLogality } = require('../lib/tester.lib'); 6 | 7 | const UDO_MOCK = { 8 | id: 10, 9 | email: 'one@go.com', 10 | }; 11 | 12 | describe('User Data Object Logging', () => { 13 | stubLogality(); 14 | 15 | test('Will log UDO Properly by default', (done) => { 16 | const logality = new Logality({ 17 | appName: 'testLogality', 18 | output: (logMessage) => { 19 | expect(logMessage).toMatchSnapshot(); 20 | done(); 21 | }, 22 | }); 23 | 24 | const log = logality.get(); 25 | 26 | log('info', 'hello world', { user: UDO_MOCK }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/spec/__snapshots__/async-log.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Asynchronous Logging Async Logging 1`] = ` 4 | Object { 5 | "context": Object { 6 | "custom": Object { 7 | "a": 1, 8 | "b": 2, 9 | }, 10 | "runtime": Object { 11 | "application": "testLogality", 12 | }, 13 | "source": Object { 14 | "file_name": "/test/spec/async-log.test.js", 15 | }, 16 | "system": Object { 17 | "hostname": "localhost", 18 | "pid": 36255, 19 | "process_name": "node .", 20 | }, 21 | }, 22 | "dt": "2018-05-18T16:25:57.815Z", 23 | "event": Object {}, 24 | "level": "info", 25 | "message": "hello world", 26 | "severity": 6, 27 | } 28 | `; 29 | 30 | exports[`Asynchronous Logging Async Logging output Error Propagates 1`] = ` 31 | Object { 32 | "context": Object { 33 | "custom": Object { 34 | "a": 1, 35 | "b": 2, 36 | }, 37 | "runtime": Object { 38 | "application": "testLogality", 39 | }, 40 | "source": Object { 41 | "file_name": "/test/spec/async-log.test.js", 42 | }, 43 | "system": Object { 44 | "hostname": "localhost", 45 | "pid": 36255, 46 | "process_name": "node .", 47 | }, 48 | }, 49 | "dt": "2018-05-18T16:25:57.815Z", 50 | "event": Object {}, 51 | "level": "info", 52 | "message": "hello world", 53 | "severity": 6, 54 | } 55 | `; 56 | 57 | exports[`Asynchronous Logging Test API helpers with Async Logging 1`] = ` 58 | "[2018-05-18T16:25:57.815Z] ★ debug /test/spec/async-log.test.js - This is message of level: Debug 59 | " 60 | `; 61 | 62 | exports[`Asynchronous Logging Test prettyPrint all with Async Logging 1`] = ` 63 | "★ debug - This is message of level: Debug 64 | " 65 | `; 66 | 67 | exports[`Asynchronous Logging Test prettyPrint noFilename with Async Logging 1`] = ` 68 | "[2018-05-18T16:25:57.815Z] ★ debug - This is message of level: Debug 69 | " 70 | `; 71 | 72 | exports[`Asynchronous Logging Test prettyPrint onlyMessage with Async Logging 1`] = ` 73 | "[2018-05-18T16:25:57.815Z] ★ debug /test/spec/async-log.test.js - This is message of level: Debug 74 | " 75 | `; 76 | 77 | exports[`Asynchronous Logging Test prettyprint noTimestamp with Async Logging 1`] = ` 78 | "★ debug /test/spec/async-log.test.js - This is message of level: Debug 79 | " 80 | `; 81 | -------------------------------------------------------------------------------- /test/spec/__snapshots__/logging.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Normal Logging Will output to standard out 1`] = ` 4 | "{\\"level\\":\\"info\\",\\"severity\\":6,\\"dt\\":\\"2018-05-18T16:25:57.815Z\\",\\"message\\":\\"Good sleep now...\\",\\"context\\":{\\"runtime\\":{\\"application\\":\\"testLogality\\"},\\"source\\":{\\"file_name\\":\\"/test/spec/logging.test.js\\"},\\"system\\":{\\"hostname\\":\\"localhost\\",\\"pid\\":36255,\\"process_name\\":\\"node .\\"}},\\"event\\":{}} 5 | " 6 | `; 7 | -------------------------------------------------------------------------------- /test/spec/__snapshots__/pipe.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`pipe interface Parent's middleware will receive the piped child messages 1`] = ` 4 | Object { 5 | "context": Object { 6 | "runtime": Object { 7 | "application": "child", 8 | }, 9 | "source": Object { 10 | "file_name": "/test/spec/pipe.test.js", 11 | }, 12 | "system": Object { 13 | "hostname": "localhost", 14 | "pid": 36255, 15 | "process_name": "node .", 16 | }, 17 | }, 18 | "dt": "2018-05-18T16:25:57.815Z", 19 | "event": Object {}, 20 | "level": "info", 21 | "message": "hello world", 22 | "severity": 6, 23 | } 24 | `; 25 | 26 | exports[`pipe interface Piped async child with object custom output will go through synch parent custom output 1`] = ` 27 | Object { 28 | "context": Object { 29 | "runtime": Object { 30 | "application": "child", 31 | }, 32 | "source": Object { 33 | "file_name": "/test/spec/pipe.test.js", 34 | }, 35 | "system": Object { 36 | "hostname": "localhost", 37 | "pid": 36255, 38 | "process_name": "node .", 39 | }, 40 | }, 41 | "dt": "2018-05-18T16:25:57.815Z", 42 | "event": Object {}, 43 | "level": "info", 44 | "message": "hello world", 45 | "severity": 6, 46 | } 47 | `; 48 | 49 | exports[`pipe interface Piped async child with object custom output will go through synch parent custom output 2`] = ` 50 | "{\\"level\\":\\"info\\",\\"severity\\":6,\\"dt\\":\\"2018-05-18T16:25:57.815Z\\",\\"message\\":\\"hello world\\",\\"context\\":{\\"runtime\\":{\\"application\\":\\"child\\"},\\"source\\":{\\"file_name\\":\\"/test/spec/pipe.test.js\\"},\\"system\\":{\\"hostname\\":\\"localhost\\",\\"pid\\":36255,\\"process_name\\":\\"node .\\"}},\\"event\\":{}} 51 | " 52 | `; 53 | 54 | exports[`pipe interface Piped async child with string custom output will go through parent custom output 1`] = `"{\\"level\\":\\"info\\",\\"severity\\":6,\\"dt\\":\\"2018-05-18T16:25:57.815Z\\",\\"message\\":\\"hello loki world\\",\\"context\\":{\\"runtime\\":{\\"application\\":\\"child\\"},\\"source\\":{\\"file_name\\":\\"/test/spec/pipe.test.js\\"},\\"system\\":{\\"hostname\\":\\"localhost\\",\\"pid\\":36255,\\"process_name\\":\\"node .\\"}},\\"event\\":{}}"`; 55 | 56 | exports[`pipe interface Will pipe one logality instance to another 1`] = ` 57 | Object { 58 | "context": Object { 59 | "runtime": Object { 60 | "application": "child", 61 | }, 62 | "source": Object { 63 | "file_name": "/test/spec/pipe.test.js", 64 | }, 65 | "system": Object { 66 | "hostname": "localhost", 67 | "pid": 36255, 68 | "process_name": "node .", 69 | }, 70 | }, 71 | "dt": "2018-05-18T16:25:57.815Z", 72 | "event": Object {}, 73 | "level": "info", 74 | "message": "hello world", 75 | "severity": 6, 76 | } 77 | `; 78 | 79 | exports[`pipe interface Will pipe three logality instances to another in chain 1`] = ` 80 | Object { 81 | "context": Object { 82 | "runtime": Object { 83 | "application": "child", 84 | }, 85 | "source": Object { 86 | "file_name": "/test/spec/pipe.test.js", 87 | }, 88 | "system": Object { 89 | "hostname": "localhost", 90 | "pid": 36255, 91 | "process_name": "node .", 92 | }, 93 | }, 94 | "dt": "2018-05-18T16:25:57.815Z", 95 | "event": Object {}, 96 | "level": "info", 97 | "message": "hello world", 98 | "severity": 6, 99 | } 100 | `; 101 | 102 | exports[`pipe interface Will pipe three logality instances to another in chain 2`] = ` 103 | Object { 104 | "context": Object { 105 | "runtime": Object { 106 | "application": "child", 107 | }, 108 | "source": Object { 109 | "file_name": "/test/spec/pipe.test.js", 110 | }, 111 | "system": Object { 112 | "hostname": "localhost", 113 | "pid": 36255, 114 | "process_name": "node .", 115 | }, 116 | }, 117 | "dt": "2018-05-18T16:25:57.815Z", 118 | "event": Object {}, 119 | "level": "info", 120 | "message": "hello world", 121 | "severity": 6, 122 | } 123 | `; 124 | -------------------------------------------------------------------------------- /test/spec/__snapshots__/pretty.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Pretty Logging Will pretty log an object in context 1`] = ` 4 | "[2018-05-18T16:25:57.815Z] ℹ info /test/spec/pretty.test.js - hello prettier world 5 | { 6 | \\"context\\": { 7 | \\"custom\\": { 8 | \\"a\\": 1, 9 | \\"b\\": 2 10 | } 11 | } 12 | } 13 | " 14 | `; 15 | 16 | exports[`Pretty Logging Will pretty log an object with noFilename 1`] = ` 17 | "[2018-05-18T16:25:57.815Z] ℹ info - hello prettier world 18 | { 19 | \\"context\\": { 20 | \\"custom\\": { 21 | \\"a\\": 1, 22 | \\"b\\": 2 23 | } 24 | } 25 | } 26 | " 27 | `; 28 | 29 | exports[`Pretty Logging Will pretty log an object with noTimestamp 1`] = ` 30 | "ℹ info /test/spec/pretty.test.js - hello prettier world 31 | { 32 | \\"context\\": { 33 | \\"custom\\": { 34 | \\"a\\": 1, 35 | \\"b\\": 2 36 | } 37 | } 38 | } 39 | " 40 | `; 41 | 42 | exports[`Pretty Logging Will pretty log an object with noTimestamp and noFilename 1`] = ` 43 | "ℹ info - hello prettier world 44 | { 45 | \\"context\\": { 46 | \\"custom\\": { 47 | \\"a\\": 1, 48 | \\"b\\": 2 49 | } 50 | } 51 | } 52 | " 53 | `; 54 | 55 | exports[`Pretty Logging Will pretty log an object with noTimestamp, noFilename and onlyMessage 1`] = ` 56 | "ℹ info - hello prettier world 57 | " 58 | `; 59 | 60 | exports[`Pretty Logging Will pretty log expected JSON properties 1`] = ` 61 | "[2018-05-18T16:25:57.815Z] ℹ info /test/spec/pretty.test.js - hello pretty world 62 | " 63 | `; 64 | -------------------------------------------------------------------------------- /test/spec/__snapshots__/surface.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`API Surface Logging Function Levels Testing log level: alert 1`] = ` 4 | Object { 5 | "context": Object { 6 | "runtime": Object { 7 | "application": "testLogality", 8 | }, 9 | "source": Object { 10 | "file_name": "/test/spec/surface.test.js", 11 | }, 12 | "system": Object { 13 | "hostname": "localhost", 14 | "pid": 36255, 15 | "process_name": "node .", 16 | }, 17 | }, 18 | "dt": "2018-05-18T16:25:57.815Z", 19 | "event": Object {}, 20 | "level": "alert", 21 | "message": "hello world", 22 | "severity": 1, 23 | } 24 | `; 25 | 26 | exports[`API Surface Logging Function Levels Testing log level: critical 1`] = ` 27 | Object { 28 | "context": Object { 29 | "runtime": Object { 30 | "application": "testLogality", 31 | }, 32 | "source": Object { 33 | "file_name": "/test/spec/surface.test.js", 34 | }, 35 | "system": Object { 36 | "hostname": "localhost", 37 | "pid": 36255, 38 | "process_name": "node .", 39 | }, 40 | }, 41 | "dt": "2018-05-18T16:25:57.815Z", 42 | "event": Object {}, 43 | "level": "critical", 44 | "message": "hello world", 45 | "severity": 2, 46 | } 47 | `; 48 | 49 | exports[`API Surface Logging Function Levels Testing log level: debug 1`] = ` 50 | Object { 51 | "context": Object { 52 | "runtime": Object { 53 | "application": "testLogality", 54 | }, 55 | "source": Object { 56 | "file_name": "/test/spec/surface.test.js", 57 | }, 58 | "system": Object { 59 | "hostname": "localhost", 60 | "pid": 36255, 61 | "process_name": "node .", 62 | }, 63 | }, 64 | "dt": "2018-05-18T16:25:57.815Z", 65 | "event": Object {}, 66 | "level": "debug", 67 | "message": "hello world", 68 | "severity": 7, 69 | } 70 | `; 71 | 72 | exports[`API Surface Logging Function Levels Testing log level: emergency 1`] = ` 73 | Object { 74 | "context": Object { 75 | "runtime": Object { 76 | "application": "testLogality", 77 | }, 78 | "source": Object { 79 | "file_name": "/test/spec/surface.test.js", 80 | }, 81 | "system": Object { 82 | "hostname": "localhost", 83 | "pid": 36255, 84 | "process_name": "node .", 85 | }, 86 | }, 87 | "dt": "2018-05-18T16:25:57.815Z", 88 | "event": Object {}, 89 | "level": "emergency", 90 | "message": "hello world", 91 | "severity": 0, 92 | } 93 | `; 94 | 95 | exports[`API Surface Logging Function Levels Testing log level: error 1`] = ` 96 | Object { 97 | "context": Object { 98 | "runtime": Object { 99 | "application": "testLogality", 100 | }, 101 | "source": Object { 102 | "file_name": "/test/spec/surface.test.js", 103 | }, 104 | "system": Object { 105 | "hostname": "localhost", 106 | "pid": 36255, 107 | "process_name": "node .", 108 | }, 109 | }, 110 | "dt": "2018-05-18T16:25:57.815Z", 111 | "event": Object {}, 112 | "level": "error", 113 | "message": "hello world", 114 | "severity": 3, 115 | } 116 | `; 117 | 118 | exports[`API Surface Logging Function Levels Testing log level: info 1`] = ` 119 | Object { 120 | "context": Object { 121 | "runtime": Object { 122 | "application": "testLogality", 123 | }, 124 | "source": Object { 125 | "file_name": "/test/spec/surface.test.js", 126 | }, 127 | "system": Object { 128 | "hostname": "localhost", 129 | "pid": 36255, 130 | "process_name": "node .", 131 | }, 132 | }, 133 | "dt": "2018-05-18T16:25:57.815Z", 134 | "event": Object {}, 135 | "level": "info", 136 | "message": "hello world", 137 | "severity": 6, 138 | } 139 | `; 140 | 141 | exports[`API Surface Logging Function Levels Testing log level: notice 1`] = ` 142 | Object { 143 | "context": Object { 144 | "runtime": Object { 145 | "application": "testLogality", 146 | }, 147 | "source": Object { 148 | "file_name": "/test/spec/surface.test.js", 149 | }, 150 | "system": Object { 151 | "hostname": "localhost", 152 | "pid": 36255, 153 | "process_name": "node .", 154 | }, 155 | }, 156 | "dt": "2018-05-18T16:25:57.815Z", 157 | "event": Object {}, 158 | "level": "notice", 159 | "message": "hello world", 160 | "severity": 5, 161 | } 162 | `; 163 | 164 | exports[`API Surface Logging Function Levels Testing log level: warn 1`] = ` 165 | Object { 166 | "context": Object { 167 | "runtime": Object { 168 | "application": "testLogality", 169 | }, 170 | "source": Object { 171 | "file_name": "/test/spec/surface.test.js", 172 | }, 173 | "system": Object { 174 | "hostname": "localhost", 175 | "pid": 36255, 176 | "process_name": "node .", 177 | }, 178 | }, 179 | "dt": "2018-05-18T16:25:57.815Z", 180 | "event": Object {}, 181 | "level": "warn", 182 | "message": "hello world", 183 | "severity": 4, 184 | } 185 | `; 186 | -------------------------------------------------------------------------------- /test/spec/__snapshots__/use.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Middleware interface Async Middleware will receive log context 1`] = ` 4 | Object { 5 | "context": Object { 6 | "runtime": Object { 7 | "application": "Logality", 8 | }, 9 | "source": Object { 10 | "file_name": "/test/spec/use.test.js", 11 | }, 12 | "system": Object { 13 | "hostname": "localhost", 14 | "pid": 36255, 15 | "process_name": "node .", 16 | }, 17 | }, 18 | "dt": "2018-05-18T16:25:57.815Z", 19 | "event": Object {}, 20 | "level": "info", 21 | "message": "hello async world", 22 | "severity": 6, 23 | } 24 | `; 25 | 26 | exports[`Middleware interface Middleware can mutate the Log Context 1`] = ` 27 | Object { 28 | "context": Object { 29 | "runtime": Object { 30 | "application": "Logality", 31 | }, 32 | "source": Object { 33 | "file_name": "/test/spec/use.test.js", 34 | }, 35 | "system": Object { 36 | "hostname": "localhost", 37 | "pid": 36255, 38 | "process_name": "node .", 39 | }, 40 | }, 41 | "dt": "2018-05-18T16:25:57.815Z", 42 | "event": Object {}, 43 | "level": "info", 44 | "message": "hello bogus world", 45 | "middleware": true, 46 | "middleware2": true, 47 | "severity": 6, 48 | } 49 | `; 50 | 51 | exports[`Middleware interface Middleware return value will be ignored 1`] = ` 52 | Object { 53 | "context": Object { 54 | "runtime": Object { 55 | "application": "Logality", 56 | }, 57 | "source": Object { 58 | "file_name": "/test/spec/use.test.js", 59 | }, 60 | "system": Object { 61 | "hostname": "localhost", 62 | "pid": 36255, 63 | "process_name": "node .", 64 | }, 65 | }, 66 | "dt": "2018-05-18T16:25:57.815Z", 67 | "event": Object {}, 68 | "level": "info", 69 | "message": "hello bogus world", 70 | "severity": 6, 71 | } 72 | `; 73 | 74 | exports[`Middleware interface Middleware will receive log context 1`] = ` 75 | Object { 76 | "context": Object { 77 | "runtime": Object { 78 | "application": "Logality", 79 | }, 80 | "source": Object { 81 | "file_name": "/test/spec/use.test.js", 82 | }, 83 | "system": Object { 84 | "hostname": "localhost", 85 | "pid": 36255, 86 | "process_name": "node .", 87 | }, 88 | }, 89 | "dt": "2018-05-18T16:25:57.815Z", 90 | "event": Object {}, 91 | "level": "info", 92 | "message": "hello world", 93 | "severity": 6, 94 | } 95 | `; 96 | -------------------------------------------------------------------------------- /test/spec/__snapshots__/writepretty.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`writePretty function Will convert log to pretty for error type 1`] = ` 4 | "[2018-05-18T16:25:57.815Z] ■ error /test/spec/writepretty.test.js - Error log 5 | { 6 | \\"event\\": { 7 | \\"error\\": { 8 | \\"name\\": \\"Error\\", 9 | \\"message\\": \\"an error occured\\", 10 | \\"backtrace\\": \\"Error: an error occured\\\\n at Object. (/Users/thanpolas/Projects/libs/logality/test/spec/writepretty.test.js:23:17)\\\\n at Promise.then.completed (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/utils.js:333:28)\\\\n at new Promise ()\\\\n at callAsyncCircusFn (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/utils.js:259:10)\\\\n at _callCircusTest (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:276:40)\\\\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\\\\n at _runTest (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:208:3)\\\\n at _runTestsForDescribeBlock (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:96:9)\\\\n at _runTestsForDescribeBlock (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:90:9)\\\\n at run (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:31:3)\\" 11 | } 12 | } 13 | } 14 | " 15 | `; 16 | 17 | exports[`writePretty function Will convert log to pretty for error type without colors 1`] = ` 18 | "[2018-05-18T16:25:57.815Z] ■ error /test/spec/writepretty.test.js - Error log 19 | { 20 | \\"event\\": { 21 | \\"error\\": { 22 | \\"name\\": \\"Error\\", 23 | \\"message\\": \\"an error occured\\", 24 | \\"backtrace\\": \\"Error: an error occured\\\\n at Object. (/Users/thanpolas/Projects/libs/logality/test/spec/writepretty.test.js:23:17)\\\\n at Promise.then.completed (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/utils.js:333:28)\\\\n at new Promise ()\\\\n at callAsyncCircusFn (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/utils.js:259:10)\\\\n at _callCircusTest (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:276:40)\\\\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\\\\n at _runTest (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:208:3)\\\\n at _runTestsForDescribeBlock (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:96:9)\\\\n at _runTestsForDescribeBlock (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:90:9)\\\\n at run (/Users/thanpolas/.nvm/versions/node/v16.15.0/lib/node_modules/jest/node_modules/jest-circus/build/run.js:31:3)\\" 25 | } 26 | } 27 | } 28 | " 29 | `; 30 | 31 | exports[`writePretty function Will convert log to pretty for into type 1`] = ` 32 | "[2018-05-18T16:25:57.815Z] ℹ info /test/spec/writepretty.test.js - hello pretty world 33 | " 34 | `; 35 | 36 | exports[`writePretty function Will convert log to pretty for into type without colors 1`] = ` 37 | "[2018-05-18T16:25:57.815Z] ℹ info /test/spec/writepretty.test.js - hello pretty world 38 | " 39 | `; 40 | -------------------------------------------------------------------------------- /test/spec/async-log.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test asynchronous logging. 3 | */ 4 | const Logality = require('../..'); 5 | const { stubLogality, cooldown } = require('../lib/tester.lib'); 6 | 7 | describe('Asynchronous Logging', () => { 8 | stubLogality(); 9 | 10 | test('Async Logging', async () => { 11 | const logality = new Logality({ 12 | appName: 'testLogality', 13 | async: true, 14 | output: async (logMessage) => { 15 | expect(logMessage).toBeObject(); 16 | expect(logMessage).toMatchSnapshot(); 17 | await cooldown(); 18 | }, 19 | }); 20 | 21 | const log = logality.get(); 22 | 23 | await log('info', 'hello world', { custom: { a: 1, b: 2 } }); 24 | }); 25 | 26 | test('Async Logging will return a promise.', async () => { 27 | const logality = new Logality({ 28 | appName: 'testLogality', 29 | async: true, 30 | }); 31 | 32 | const log = logality.get(); 33 | 34 | const promise = log('info', 'hello world', { custom: { a: 1, b: 2 } }); 35 | 36 | expect(typeof promise).toEqual('object'); 37 | expect(typeof promise.then).toEqual('function'); 38 | expect(typeof promise.catch).toEqual('function'); 39 | await promise; 40 | }); 41 | 42 | test('Test API helpers with Async Logging', async () => { 43 | const logality = new Logality({ 44 | appName: 'testLogality', 45 | async: true, 46 | prettyPrint: true, 47 | }); 48 | 49 | const log = logality.get(); 50 | const spy = jest.spyOn(process.stdout, 'write'); 51 | 52 | await log.debug('This is message of level: Debug'); 53 | await log.info('This is message of level: Info'); 54 | await log.notice('This is message of level: Notice'); 55 | await log.warn('This is message of level: warning'); 56 | await log.error('This is message of level: Error'); 57 | await log.critical('This is message of level: Critical'); 58 | await log.alert('This is message of level: Alert'); 59 | await log.emergency('This is message of level: Emergency\n\n'); 60 | 61 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 62 | 63 | spy.mockRestore(); 64 | }); 65 | 66 | test('Test prettyprint noTimestamp with Async Logging', async () => { 67 | const logality = new Logality({ 68 | appName: 'testLogality', 69 | async: true, 70 | prettyPrint: { noTimestamp: true }, 71 | }); 72 | 73 | const log = logality.get(); 74 | const spy = jest.spyOn(process.stdout, 'write'); 75 | 76 | await log.debug('This is message of level: Debug'); 77 | await log.info('This is message of level: Info'); 78 | await log.notice('This is message of level: Notice'); 79 | await log.warn('This is message of level: warning'); 80 | await log.error('This is message of level: Error'); 81 | await log.critical('This is message of level: Critical'); 82 | await log.alert('This is message of level: Alert'); 83 | await log.emergency('This is message of level: Emergency\n\n'); 84 | 85 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 86 | 87 | spy.mockRestore(); 88 | }); 89 | 90 | test('Test prettyPrint noFilename with Async Logging', async () => { 91 | const logality = new Logality({ 92 | appName: 'testLogality', 93 | async: true, 94 | prettyPrint: { noFilename: true }, 95 | }); 96 | 97 | const log = logality.get(); 98 | const spy = jest.spyOn(process.stdout, 'write'); 99 | 100 | await log.debug('This is message of level: Debug'); 101 | await log.info('This is message of level: Info'); 102 | await log.notice('This is message of level: Notice'); 103 | await log.warn('This is message of level: warning'); 104 | await log.error('This is message of level: Error'); 105 | await log.critical('This is message of level: Critical'); 106 | await log.alert('This is message of level: Alert'); 107 | await log.emergency('This is message of level: Emergency\n\n'); 108 | 109 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 110 | 111 | spy.mockRestore(); 112 | }); 113 | 114 | test('Test prettyPrint onlyMessage with Async Logging', async () => { 115 | const logality = new Logality({ 116 | appName: 'testLogality', 117 | async: true, 118 | prettyPrint: { onlyMessage: true }, 119 | }); 120 | 121 | const log = logality.get(); 122 | const spy = jest.spyOn(process.stdout, 'write'); 123 | 124 | await log.debug('This is message of level: Debug'); 125 | await log.info('This is message of level: Info'); 126 | await log.notice('This is message of level: Notice'); 127 | await log.warn('This is message of level: warning'); 128 | await log.error('This is message of level: Error'); 129 | await log.critical('This is message of level: Critical'); 130 | await log.alert('This is message of level: Alert'); 131 | await log.emergency('This is message of level: Emergency\n\n'); 132 | 133 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 134 | 135 | spy.mockRestore(); 136 | }); 137 | 138 | test('Test prettyPrint all with Async Logging', async () => { 139 | const logality = new Logality({ 140 | appName: 'testLogality', 141 | async: true, 142 | prettyPrint: { 143 | noTimestamp: true, 144 | noFilename: true, 145 | onlyMessage: true, 146 | }, 147 | }); 148 | 149 | const log = logality.get(); 150 | const spy = jest.spyOn(process.stdout, 'write'); 151 | 152 | await log.debug('This is message of level: Debug'); 153 | await log.info('This is message of level: Info'); 154 | await log.notice('This is message of level: Notice'); 155 | await log.warn('This is message of level: warning'); 156 | await log.error('This is message of level: Error'); 157 | await log.critical('This is message of level: Critical'); 158 | await log.alert('This is message of level: Alert'); 159 | await log.emergency('This is message of level: Emergency\n\n'); 160 | 161 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 162 | 163 | spy.mockRestore(); 164 | }); 165 | 166 | test('Async Logging output Error Propagates', async () => { 167 | const logality = new Logality({ 168 | appName: 'testLogality', 169 | async: true, 170 | output: (logMessage) => { 171 | expect(logMessage).toMatchSnapshot(); 172 | throw new Error('420'); 173 | }, 174 | }); 175 | 176 | const log = logality.get(); 177 | 178 | let errorThrown = false; 179 | 180 | try { 181 | await log('info', 'hello world', { custom: { a: 1, b: 2 } }); 182 | } catch (ex) { 183 | expect(ex.message).toEqual('420'); 184 | errorThrown = true; 185 | } 186 | 187 | expect(errorThrown).toEqual(true); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /test/spec/logging.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test normal logging. 3 | */ 4 | const Logality = require('../..'); 5 | const { stubLogality, DT_VAL, PID_VAL } = require('../lib/tester.lib'); 6 | 7 | function assertContext(logContext) { 8 | expect(logContext).toBeObject(); 9 | expect(logContext).toContainAllKeys([ 10 | 'level', 11 | 'severity', 12 | 'dt', 13 | 'message', 14 | 'context', 15 | 'event', 16 | ]); 17 | expect(logContext.context).toContainAllKeys(['runtime', 'source', 'system']); 18 | expect(logContext.context.runtime).toContainAllKeys(['application']); 19 | expect(logContext.context.source).toContainAllKeys(['file_name']); 20 | expect(logContext.context.system).toContainAllKeys([ 21 | 'hostname', 22 | 'pid', 23 | 'process_name', 24 | ]); 25 | expect(logContext.event).toContainAllKeys([]); 26 | 27 | expect(logContext.level).toEqual('info'); 28 | expect(logContext.severity).toEqual(6); 29 | expect(logContext.dt).toEqual(DT_VAL); 30 | expect(logContext.message).toEqual('hello world'); 31 | expect(logContext.context.runtime.application).toEqual('testLogality'); 32 | expect(logContext.context.source.file_name).toEqual( 33 | '/test/spec/logging.test.js', 34 | ); 35 | expect(logContext.context.system.hostname).toEqual('localhost'); 36 | expect(logContext.context.system.pid).toEqual(PID_VAL); 37 | expect(logContext.context.system.process_name).toEqual('node .'); 38 | } 39 | 40 | describe('Normal Logging', () => { 41 | stubLogality(); 42 | 43 | test('Custom output will receive expected logContext object', () => { 44 | const output = jest.fn(); 45 | 46 | const logality = new Logality({ 47 | appName: 'testLogality', 48 | output, 49 | }); 50 | 51 | const log = logality.get(); 52 | 53 | log('info', 'hello world'); 54 | expect(output).toHaveBeenCalledTimes(1); 55 | assertContext(output.mock.calls[0][0]); 56 | }); 57 | test('Will output to standard out', () => { 58 | const spy = jest.spyOn(process.stdout, 'write'); 59 | 60 | const logality = new Logality({ 61 | appName: 'testLogality', 62 | }); 63 | const log = logality.get(); 64 | 65 | log('info', 'Good sleep now...'); 66 | 67 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 68 | 69 | spy.mockRestore(); 70 | }); 71 | describe('Filter Logging', () => { 72 | test('Will filter out debug level with string key', () => { 73 | const spy = jest.spyOn(process.stdout, 'write'); 74 | 75 | const logality = new Logality({ 76 | appName: 'testLogality', 77 | minLevel: 'info', 78 | }); 79 | const log = logality.get(); 80 | 81 | log('debug', 'Good sleep now...'); 82 | 83 | expect(spy).toHaveBeenCalledTimes(0); 84 | 85 | spy.mockRestore(); 86 | }); 87 | test('Will filter out debug level with nunber', () => { 88 | const spy = jest.spyOn(process.stdout, 'write'); 89 | 90 | const logality = new Logality({ 91 | appName: 'testLogality', 92 | minLevel: 6, 93 | }); 94 | const log = logality.get(); 95 | 96 | log('debug', 'Good sleep now...'); 97 | 98 | expect(spy).toHaveBeenCalledTimes(0); 99 | 100 | spy.mockRestore(); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/spec/pipe.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test the pipe interface. 3 | */ 4 | 5 | const Logality = require('../..'); 6 | const { stubLogality, cooldown } = require('../lib/tester.lib'); 7 | 8 | describe('pipe interface', () => { 9 | stubLogality(); 10 | test('Will pipe one logality instance to another', () => { 11 | const output = jest.fn(); 12 | const logalityChild = new Logality({ 13 | appName: 'child', 14 | }); 15 | const logalityParent = new Logality({ 16 | appName: 'parent', 17 | output, 18 | }); 19 | 20 | logalityParent.pipe(logalityChild); 21 | logalityChild.get()('info', 'hello world'); 22 | expect(output).toHaveBeenCalledTimes(1); 23 | expect(output.mock.calls[0][0]).toMatchSnapshot(); 24 | // Second argument "isPiped" should be true 25 | expect(output.mock.calls[0][1]).toBeTrue(); 26 | }); 27 | 28 | test('Will pipe multiple logality instances to a single one', () => { 29 | const logalityChild1 = new Logality({ appName: 'child1' }); 30 | const logalityChild2 = new Logality({ appName: 'child2' }); 31 | const logalityChild3 = new Logality({ appName: 'child3' }); 32 | 33 | const output = jest.fn(); 34 | 35 | const logalityParent = new Logality({ 36 | appName: 'parent', 37 | output, 38 | }); 39 | 40 | logalityParent.pipe([logalityChild1, logalityChild2, logalityChild3]); 41 | logalityChild1.get()('info', 'hello world 1'); 42 | logalityChild2.get()('info', 'hello world 2'); 43 | logalityChild3.get()('info', 'hello world 3'); 44 | 45 | expect(output).toHaveBeenCalledTimes(3); 46 | }); 47 | test('Piped async child with string custom output will go through parent custom output', async () => { 48 | const logalityChild = new Logality({ 49 | appName: 'child', 50 | async: true, 51 | output: async (logContext) => { 52 | await cooldown(); 53 | return JSON.stringify(logContext); 54 | }, 55 | }); 56 | 57 | const output = jest.fn((logContext) => logContext); 58 | const logalityParent = new Logality({ 59 | appName: 'parent', 60 | output, 61 | }); 62 | 63 | logalityParent.pipe(logalityChild); 64 | 65 | const spy = jest.spyOn(process.stdout, 'write'); 66 | 67 | await logalityChild.get()('info', 'hello loki world'); 68 | 69 | expect(output).toHaveBeenCalledTimes(1); 70 | expect(output.mock.calls[0][0]).toBeString(); 71 | expect(output.mock.calls[0][1]).toBeTrue(); 72 | expect(spy).toHaveBeenCalledTimes(1); 73 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 74 | spy.mockRestore(); 75 | }); 76 | test('Piped async child with object custom output will go through synch parent custom output', async () => { 77 | const logalityChild = new Logality({ 78 | appName: 'child', 79 | async: true, 80 | output: async (logContext) => { 81 | await cooldown(); 82 | return logContext; 83 | }, 84 | }); 85 | 86 | const output = jest.fn((logContext) => logContext); 87 | const logalityParent = new Logality({ 88 | appName: 'parent', 89 | output, 90 | }); 91 | 92 | logalityParent.pipe(logalityChild); 93 | 94 | const spy = jest.spyOn(process.stdout, 'write'); 95 | 96 | await logalityChild.get()('info', 'hello world'); 97 | 98 | expect(output).toHaveBeenCalledTimes(1); 99 | expect(spy).toHaveBeenCalledTimes(1); 100 | expect(output.mock.calls[0][0]).toMatchSnapshot(); 101 | // Second argument "isPiped" should be true 102 | expect(output.mock.calls[0][1]).toBeTrue(); 103 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 104 | spy.mockRestore(); 105 | }); 106 | test('Piped async child with no return custom output will not go through synch parent custom output', async () => { 107 | const logalityChild = new Logality({ 108 | appName: 'child', 109 | async: true, 110 | output: async () => { 111 | await cooldown(); 112 | }, 113 | }); 114 | 115 | const output = jest.fn((logContext) => logContext); 116 | const logalityParent = new Logality({ 117 | appName: 'parent', 118 | output, 119 | }); 120 | 121 | logalityParent.pipe(logalityChild); 122 | 123 | const spy = jest.spyOn(process.stdout, 'write'); 124 | 125 | await logalityChild.get()('info', 'hello piko world'); 126 | 127 | expect(output).toHaveBeenCalledTimes(0); 128 | expect(spy).not.toHaveBeenCalled(); 129 | spy.mockRestore(); 130 | }); 131 | 132 | test("Parent's middleware will receive the piped child messages", () => { 133 | const output = jest.fn((logContext) => logContext); 134 | const middleware = jest.fn(); 135 | const logalityChild = new Logality({ appName: 'child' }); 136 | const logalityParent = new Logality({ 137 | appName: 'parent', 138 | output, 139 | }); 140 | 141 | logalityParent.pipe(logalityChild); 142 | logalityParent.use(middleware); 143 | 144 | logalityChild.get()('info', 'hello world'); 145 | 146 | expect(output).toHaveBeenCalledTimes(1); 147 | expect(middleware).toHaveBeenCalledTimes(1); 148 | const logContext = middleware.mock.calls[0][0]; 149 | expect(logContext.context.runtime.application).toEqual('child'); 150 | expect(logContext).toMatchSnapshot(); 151 | // Second argument "isPiped" should be true 152 | expect(output.mock.calls[0][1]).toBeTrue(); 153 | }); 154 | 155 | test('Will pipe three logality instances to another in chain', () => { 156 | const output = jest.fn((logContext) => logContext); 157 | const outputGrand = jest.fn((logContext) => logContext); 158 | const logalityChild = new Logality({ 159 | appName: 'child', 160 | }); 161 | const logalityParent = new Logality({ 162 | appName: 'parent', 163 | output, 164 | }); 165 | const logalityGrandParent = new Logality({ 166 | appName: 'grandParent', 167 | output: outputGrand, 168 | }); 169 | 170 | logalityParent.pipe(logalityChild); 171 | logalityGrandParent.pipe(logalityParent); 172 | 173 | logalityChild.get()('info', 'hello world'); 174 | 175 | expect(output).toHaveBeenCalledTimes(1); 176 | expect(outputGrand).toHaveBeenCalledTimes(1); 177 | expect(output.mock.calls[0][0]).toMatchSnapshot(); 178 | expect(outputGrand.mock.calls[0][0]).toMatchSnapshot(); 179 | // Second argument "isPiped" should be true 180 | expect(output.mock.calls[0][1]).toBeTrue(); 181 | // Second argument "isPiped" should be true 182 | expect(outputGrand.mock.calls[0][1]).toBeTrue(); 183 | }); 184 | }); 185 | -------------------------------------------------------------------------------- /test/spec/pretty.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test normal logging. 3 | */ 4 | const Logality = require('../..'); 5 | const { stubLogality } = require('../lib/tester.lib'); 6 | 7 | describe('Pretty Logging', () => { 8 | stubLogality(); 9 | 10 | test('Will pretty log expected JSON properties', () => { 11 | const spy = jest.spyOn(process.stdout, 'write'); 12 | const logality = new Logality({ 13 | prettyPrint: true, 14 | appName: 'testLogality', 15 | }); 16 | 17 | const log = logality.get(); 18 | 19 | log('info', 'hello pretty world'); 20 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 21 | spy.mockRestore(); 22 | }); 23 | 24 | test('Will pretty log an object in context', () => { 25 | const spy = jest.spyOn(process.stdout, 'write'); 26 | const logality = new Logality({ 27 | prettyPrint: true, 28 | appName: 'testLogality', 29 | }); 30 | 31 | const log = logality.get(); 32 | 33 | log('info', 'hello prettier world', { custom: { a: 1, b: 2 } }); 34 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 35 | spy.mockRestore(); 36 | }); 37 | 38 | test('Will pretty log an object with noTimestamp', () => { 39 | const spy = jest.spyOn(process.stdout, 'write'); 40 | const logality = new Logality({ 41 | prettyPrint: { 42 | noTimestamp: true, 43 | }, 44 | appName: 'testLogality', 45 | }); 46 | 47 | const log = logality.get(); 48 | 49 | log('info', 'hello prettier world', { custom: { a: 1, b: 2 } }); 50 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 51 | spy.mockRestore(); 52 | }); 53 | test('Will pretty log an object with noFilename', () => { 54 | const spy = jest.spyOn(process.stdout, 'write'); 55 | const logality = new Logality({ 56 | prettyPrint: { 57 | noFilename: true, 58 | }, 59 | appName: 'testLogality', 60 | }); 61 | 62 | const log = logality.get(); 63 | 64 | log('info', 'hello prettier world', { custom: { a: 1, b: 2 } }); 65 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 66 | spy.mockRestore(); 67 | }); 68 | 69 | test('Will pretty log an object with noTimestamp and noFilename', () => { 70 | const spy = jest.spyOn(process.stdout, 'write'); 71 | const logality = new Logality({ 72 | prettyPrint: { 73 | noTimestamp: true, 74 | noFilename: true, 75 | }, 76 | appName: 'testLogality', 77 | }); 78 | 79 | const log = logality.get(); 80 | 81 | log('info', 'hello prettier world', { custom: { a: 1, b: 2 } }); 82 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 83 | spy.mockRestore(); 84 | }); 85 | 86 | test('Will pretty log an object with noTimestamp, noFilename and onlyMessage', () => { 87 | const spy = jest.spyOn(process.stdout, 'write'); 88 | const logality = new Logality({ 89 | prettyPrint: { 90 | noTimestamp: true, 91 | noFilename: true, 92 | onlyMessage: true, 93 | }, 94 | appName: 'testLogality', 95 | }); 96 | 97 | const log = logality.get(); 98 | 99 | log('info', 'hello prettier world', { custom: { a: 1, b: 2 } }); 100 | expect(spy.mock.calls[0][0]).toMatchSnapshot(); 101 | spy.mockRestore(); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /test/spec/surface.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test the API surface, library properly exporting methods. 3 | */ 4 | const Logality = require('../..'); 5 | const { stubLogality } = require('../lib/tester.lib'); 6 | 7 | describe('API Surface', () => { 8 | test('Logality is a function', () => { 9 | expect(Logality).toBeFunction(); 10 | }); 11 | test('Logality instantiates properly', () => { 12 | const logality = new Logality(); 13 | expect(logality.writePretty).toBeFunction(); 14 | expect(logality.get).toBeFunction(); 15 | }); 16 | test('Logality instance has expected methods exported', () => { 17 | const logality = Logality(); 18 | expect(logality.version).toBeString(); 19 | expect(logality.get).toBeFunction(); 20 | expect(logality.log).toBeFunction(); 21 | expect(logality.pipe).toBeFunction(); 22 | expect(logality.use).toBeFunction(); 23 | }); 24 | 25 | describe('Logging Function Levels', () => { 26 | stubLogality(); 27 | 28 | test('Testing log level: debug', (done) => { 29 | const logality = new Logality({ 30 | appName: 'testLogality', 31 | output: (logMessage) => { 32 | expect(logMessage).toMatchSnapshot(); 33 | done(); 34 | }, 35 | }); 36 | 37 | const log = logality.get(); 38 | 39 | log.debug('hello world'); 40 | }); 41 | test('Testing log level: info', (done) => { 42 | const logality = new Logality({ 43 | appName: 'testLogality', 44 | output: (logMessage) => { 45 | expect(logMessage).toMatchSnapshot(); 46 | done(); 47 | }, 48 | }); 49 | 50 | const log = logality.get(); 51 | 52 | log.info('hello world'); 53 | }); 54 | test('Testing log level: notice', (done) => { 55 | const logality = new Logality({ 56 | appName: 'testLogality', 57 | output: (logMessage) => { 58 | expect(logMessage).toMatchSnapshot(); 59 | done(); 60 | }, 61 | }); 62 | 63 | const log = logality.get(); 64 | 65 | log.notice('hello world'); 66 | }); 67 | test('Testing log level: warn', (done) => { 68 | const logality = new Logality({ 69 | appName: 'testLogality', 70 | output: (logMessage) => { 71 | expect(logMessage).toMatchSnapshot(); 72 | done(); 73 | }, 74 | }); 75 | 76 | const log = logality.get(); 77 | 78 | log.warn('hello world'); 79 | }); 80 | test('Testing log level: error', (done) => { 81 | const logality = new Logality({ 82 | appName: 'testLogality', 83 | output: (logMessage) => { 84 | expect(logMessage).toMatchSnapshot(); 85 | done(); 86 | }, 87 | }); 88 | 89 | const log = logality.get(); 90 | 91 | log.error('hello world'); 92 | }); 93 | test('Testing log level: critical', (done) => { 94 | const logality = new Logality({ 95 | appName: 'testLogality', 96 | output: (logMessage) => { 97 | expect(logMessage).toMatchSnapshot(); 98 | done(); 99 | }, 100 | }); 101 | 102 | const log = logality.get(); 103 | 104 | log.critical('hello world'); 105 | }); 106 | test('Testing log level: alert', (done) => { 107 | const logality = new Logality({ 108 | appName: 'testLogality', 109 | output: (logMessage) => { 110 | expect(logMessage).toMatchSnapshot(); 111 | done(); 112 | }, 113 | }); 114 | 115 | const log = logality.get(); 116 | 117 | log.alert('hello world'); 118 | }); 119 | test('Testing log level: emergency', (done) => { 120 | const logality = new Logality({ 121 | appName: 'testLogality', 122 | output: (logMessage) => { 123 | expect(logMessage).toMatchSnapshot(); 124 | done(); 125 | }, 126 | }); 127 | 128 | const log = logality.get(); 129 | 130 | log.emergency('hello world'); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/spec/use.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test the middleware interface. 3 | */ 4 | 5 | const Logality = require('../..'); 6 | const { stubLogality, cooldown } = require('../lib/tester.lib'); 7 | 8 | describe('Middleware interface', () => { 9 | stubLogality(); 10 | test('Middleware will receive log context', () => { 11 | const logality = new Logality(); 12 | 13 | const middStub = jest.fn(); 14 | logality.use(middStub); 15 | 16 | logality.get()('info', 'hello world'); 17 | 18 | expect(middStub).toHaveBeenCalledTimes(1); 19 | const callArgs = middStub.mock.calls[0]; 20 | 21 | expect(callArgs[0]).toBeObject(); 22 | expect(callArgs[0]).toMatchSnapshot(); 23 | expect(callArgs[1]).toBeFalse(); 24 | }); 25 | 26 | test('Async Middleware will receive log context', async () => { 27 | const logality = new Logality({ async: true }); 28 | 29 | const middStub = jest.fn(async () => { 30 | await cooldown(); 31 | }); 32 | logality.use(middStub); 33 | 34 | logality.get()('info', 'hello async world'); 35 | 36 | expect(middStub).toHaveBeenCalledTimes(1); 37 | const callArgs = middStub.mock.calls[0]; 38 | 39 | expect(callArgs[0]).toBeObject(); 40 | expect(callArgs[0]).toMatchSnapshot(); 41 | expect(callArgs[1]).toBeFalse(); 42 | }); 43 | 44 | test('Middleware return value will be ignored', () => { 45 | const logality = new Logality(); 46 | 47 | const middStub = jest.fn(() => 'bogus'); 48 | logality.use(middStub); 49 | 50 | logality.get()('info', 'hello bogus world'); 51 | 52 | expect(middStub).toHaveBeenCalledTimes(1); 53 | const callArgs = middStub.mock.calls[0]; 54 | 55 | expect(callArgs[0]).toBeObject(); 56 | expect(callArgs[0]).toMatchSnapshot(); 57 | expect(callArgs[1]).toBeFalse(); 58 | }); 59 | 60 | test('Middleware can mutate the Log Context', () => { 61 | const output = jest.fn(); 62 | const logality = new Logality({ output }); 63 | 64 | const middStub = jest.fn((logContext) => { 65 | logContext.middleware = true; 66 | logContext.middleware2 = true; 67 | }); 68 | logality.use(middStub); 69 | 70 | logality.get()('info', 'hello bogus world'); 71 | 72 | expect(middStub).toHaveBeenCalledTimes(1); 73 | const callArgs = middStub.mock.calls[0]; 74 | 75 | expect(callArgs[0]).toBeObject(); 76 | expect(callArgs[0]).toMatchSnapshot(); 77 | expect(callArgs[1]).toBeFalse(); 78 | 79 | expect(output).toHaveBeenCalledTimes(1); 80 | const logContext = output.mock.calls[0][0]; 81 | expect(logContext.middleware).toBeTrue(); 82 | expect(logContext.middleware2).toBeTrue(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/spec/util.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test utility functions. 3 | */ 4 | const util = require('../../app/utils'); 5 | 6 | describe('Utility Functions', () => { 7 | describe('assignPath() Helper', () => { 8 | test('Will assign at root as expected', () => { 9 | const target = {}; 10 | 11 | util.assignPath('one', target, 1); 12 | 13 | expect(target).toHaveProperty('one'); 14 | expect(target.one).toEqual(1); 15 | }); 16 | test('Will assign at level 1', () => { 17 | const target = {}; 18 | 19 | util.assignPath('one.two', target, 'two'); 20 | 21 | expect(target).toHaveProperty('one'); 22 | expect(target.one).toHaveProperty('two'); 23 | expect(target.one.two).toEqual('two'); 24 | }); 25 | test('Will assign at level 2', () => { 26 | const target = {}; 27 | 28 | util.assignPath('one.two.three', target, 'two'); 29 | 30 | expect(target).toHaveProperty('one'); 31 | expect(target.one).toHaveProperty('two'); 32 | expect(target.one.two).toHaveProperty('three'); 33 | expect(target.one.two.three).toEqual('two'); 34 | }); 35 | test('Will assign at level 3', () => { 36 | const target = {}; 37 | 38 | util.assignPath('one.two.three.four', target, 'two'); 39 | 40 | expect(target).toHaveProperty('one'); 41 | expect(target.one).toHaveProperty('two'); 42 | expect(target.one.two).toHaveProperty('three'); 43 | expect(target.one.two.three).toHaveProperty('four'); 44 | expect(target.one.two.three.four).toEqual('two'); 45 | }); 46 | 47 | test('Will overwrite', () => { 48 | const target = { 49 | one: 1, 50 | }; 51 | 52 | util.assignPath('one', target, 'two'); 53 | 54 | expect(target).toHaveProperty('one'); 55 | expect(target.one).toEqual('two'); 56 | }); 57 | 58 | test('Will not have sideeffects on siblings', () => { 59 | const target = { 60 | one: { 61 | first: 1, 62 | }, 63 | }; 64 | 65 | util.assignPath('one.second', target, 'two'); 66 | 67 | expect(target).toHaveProperty('one'); 68 | expect(target.one).toHaveProperty('first'); 69 | expect(target.one).toHaveProperty('second'); 70 | expect(target.one.first).toEqual(1); 71 | expect(target.one.second).toEqual('two'); 72 | }); 73 | }); 74 | describe('safeStringify()', () => { 75 | test('Should safely JSON serialize an object containing BigInt', () => { 76 | const obj = { 77 | a: 1, 78 | b: BigInt(1), 79 | c: 2, 80 | }; 81 | 82 | util.safeStringify(obj); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/spec/writepretty.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Test writePretty function. 3 | */ 4 | const Logality = require('../..'); 5 | 6 | const { infoLogFix, errorLogFix } = require('../fixtures/log-samples.fix'); 7 | const { stubLogality } = require('../lib/tester.lib'); 8 | 9 | describe('writePretty function', () => { 10 | stubLogality(); 11 | 12 | test('Will convert log to pretty for into type', () => { 13 | const logality = new Logality({ 14 | appName: 'testLogality', 15 | }); 16 | 17 | const prettyLogInfo = logality.writePretty(infoLogFix()); 18 | expect(prettyLogInfo).toMatchSnapshot(); 19 | }); 20 | 21 | test('Will convert log to pretty for into type without colors', () => { 22 | const logality = new Logality({ 23 | appName: 'testLogality', 24 | }); 25 | 26 | const prettyLogInfo = logality.writePretty(infoLogFix(), { noColor: true }); 27 | expect(prettyLogInfo).toMatchSnapshot(); 28 | }); 29 | 30 | test('Will convert log to pretty for error type', () => { 31 | const logality = new Logality({ 32 | appName: 'testLogality', 33 | }); 34 | 35 | const prettyLogInfo = logality.writePretty(errorLogFix()); 36 | expect(prettyLogInfo).toMatchSnapshot(); 37 | }); 38 | 39 | test('writePretty() should not mutate passed log context', () => { 40 | const logality = new Logality({ 41 | appName: 'testLogality', 42 | }); 43 | 44 | const errorLogContext = errorLogFix(); 45 | logality.writePretty(errorLogContext); 46 | 47 | expect(errorLogContext).toEqual(errorLogFix()); 48 | }); 49 | 50 | test('Will convert log to pretty for error type without colors', () => { 51 | const logality = new Logality({ 52 | appName: 'testLogality', 53 | }); 54 | 55 | const prettyLogInfo = logality.writePretty(errorLogFix(), { 56 | noColor: true, 57 | }); 58 | expect(prettyLogInfo).toMatchSnapshot(); 59 | }); 60 | }); 61 | --------------------------------------------------------------------------------