Compare commits

..

1 Commits

Author SHA1 Message Date
Giorgio Regni 5d0f03204f change bunyan dep to point to alternative version with a different text output filter 2016-01-26 15:03:06 -08:00
35 changed files with 617 additions and 2807 deletions

7
.babelrc Normal file
View File

@ -0,0 +1,7 @@
{
"plugins": [
"transform-es2015-destructuring",
"transform-es2015-modules-commonjs",
"transform-es2015-parameters"
]
}

View File

@ -1 +1,13 @@
{ "extends": "scality" } {
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"node": true,
"mocha": true
},
"rules": {
"indent": [2,4],
"no-multi-spaces": [2, { exceptions: { "SwitchCase": true, "CallExpression": true } } ],
"strict": 0
}
}

View File

@ -1,31 +0,0 @@
name: Tests
on:
push:
branches-ignore:
- development/**
- q/*/**
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkokut
uses: actions/checkout@v4
- name: Install deps
run: sudo apt-get update -q
- uses: actions/setup-node@v4
with:
node-version: '16'
- name: Install Yarn
run: npm install -g yarn
- name: install dependencies
run: yarn install --frozen-lockfile
- name: run lint
run: echo "linter is disabled temporarily ()" || yarn run --silent lint -- --max-warnings 0
- name: run lint_md
run: yarn --silent lint_md
- name: run test
run: yarn test
- name: run coverage
run: yarn coverage

View File

@ -1,5 +1,53 @@
# Contributing rules # Contributing to WereLogs
Please follow the Contributing to WereLogs can take multiple shapes:
[Contributing Guidelines]( * Reporting a bug
https://github.com/scality/Guidelines/blob/master/CONTRIBUTING.md). * Requesting/Suggesting/Discussing a feature
* Providing Code:
- a bug fix
- a new feature
- improving the documentation
Each of these shapes of contribution must comply to a given set of rules, to
facilitate cooperation.
## Discussing the project
This project is an opensource project, provided to you by other developers, and
they too, are human. Thus, please try to explain your point of view when
discussing a feature or bug, with examples, ascii art drawings, in an organized
discourse. This should help everyone understand your point better, as well as
it creates a context and cordial exchange between people. At any moment, try to
keep your calm, and avoid flaming, as it never helps any discussion, and surely
repulses some of the other contributors.
Summarizing:
* Keep calm and stay cordial
* Explain your point, using the help of samples of code, ascii art diagrams
etc.
## Reporting an Issue
In order to report a bug, here are a few rules that can help both the
reporter as well as the developer of the project:
* A bug report's name must shortly describe the user-side effect of the bug
* A bug report's body must provide the following informations:
- Describe in detail the effect of the bug in an expected usage situation
- Provide information about the setup in which werelogs is used, if you feel
it is relevant.
Some additional useful information for a bug report can be:
- Running environment
- A sample of code that triggers the bug (REALLY helpful and appreciated)
- Describe any potential work-around found for the bug
## Requesting or Suggesting a feature
In order for any feature suggestion to be clear, please comply to the following
basic rules:
* Issue title must be clear about the feature (a clear and semantic name might
help) and what value it adds to the project
* The description of the suggestion must provide insight into:
- What the feature provides
- At least one use-case of the feature (to show the value of it for the
project)
- If thought of, one suggestion as to how technically implement the feature

View File

@ -41,10 +41,9 @@ as it means that whenever the errors happen, depending on your log level, you
might have already lost quite a bit of priceless information about the error might have already lost quite a bit of priceless information about the error
encountered, and the code path the request went through. To address this, we encountered, and the code path the request went through. To address this, we
offer multiple features: offer multiple features:
* [Request ID namespacing](###request-id-namespacing)
* [Request ID namespacing](###request-id-namespacing) * [Request unit Logs](###request-unit-logs)
* [Request unit Logs](###request-unit-logs)
### Request ID namespacing ### Request ID namespacing
Usually, in networked systems, it is considered good practice to generate an Usually, in networked systems, it is considered good practice to generate an
@ -88,3 +87,5 @@ error (or higher level logging operation) is logged before the log context is
freed, then the full set of buffered logging messages is freed, not taking freed, then the full set of buffered logging messages is freed, not taking
any logging resources for the log entries not considered 'useless' by a given any logging resources for the log entries not considered 'useless' by a given
log level configuration. log level configuration.

191
LICENSE
View File

@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2016 Scality
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

121
README.md
View File

@ -1,81 +1,51 @@
# WereLogs # WereLogs
[![Scality CI][badgepriv]](http://ci.ironmann.io/gh/scality/werelogs)
This repository provides a NodeJS Library that aims to be an efficient logging This repository provides a NodeJS Library that aims to be an efficient logging
library, reducing as much as possible the need to compute anything in NodeJS, library, reducing as much as possible the need to compute anything in NodeJS,
and focusing on a simple I/O scheme. The goal here is to make the most of and focusing on a simple I/O scheme. The goal here is to make the most of
NodeJS's strengths, but relying on its I/O capacities, and avoiding any form of NodeJS's strengths, but relying on its I/O capacities, and avoiding any form of
computation that is known to not be advantageous in Node. computation that is known to not be advantageous in Node.
## Contributing
In order to contribute, please follow the
[Contributing Guidelines](
https://github.com/scality/Guidelines/blob/master/CONTRIBUTING.md).
## Installing the Library ## Installing the Library
In order to install WereLogs, you can use NPM with github's HTTP url, and save In order to install WereLogs, you can use NPM with github's HTTP url, and save
it in your own package.json: it in your own package.json:
```
```sh
$> npm i --save scality/werelogs $> npm i --save scality/werelogs
``` ```
As the repository is currently private, you will need to provide your username As the repository is currently private, you will need to provide your username
and your password, or use the git+ssh protocol with a properly configured and your password, or use the git+ssh protocol with a properly configured
environment, or use the git+https protocol with your username and cleartext environment, or use the git+https protocol with your username and cleartext
password in the URL (which I absolutely don't recommend for security reasons). password in the URL (which I absolutely don't recomment for security reasons).
## Using the Library ## Using the Library
Werelogs is a logging library that provides both per-request and per-module As WereLogs is a per-request logging library and not a per-module one,
logging facilities, through the intermediary of the per-module Logger that importing the library is not enough by itself (the module itself does not
is the default export. provide logging methods). The module provides a method to instanciate a
request-specific logger object. That object is the one you want to use for any
logging operation related to your request, so you will have to pass it to
any function that requires it.
Werelogs may be configured only once throughout your application's lifetime, ```es6
through the configuration options available in the per-module logger
constructor.
The per-module Logger object is used to log relevant events for a given module.
The RequestLogger object is the one you want to use for any logging operation
related to an ongoing request, so you will have to pass it to any function that
requires it.
All logging methods (trace, debug, info, warn, error and fatal) follow the same
prototype and usage pattern. They can take up to two parameters, the first one,
mandatory, being a string message, and the second one, optional, being an
object used to provide additional information to be included in the log entry.
The RequestLogger also provides a way to include some attributes in the JSON by
default for all subsequent logging calls, by explicitly inputting them only
once for the whole request's lifetime through the method
`addDefaultFields`.
As the RequestLogger is a logger strongly associated to a request's processing
operations, it provides a builtin facility to log the elapsed time in ms of the
said processing of the request. This is done through a specific logging method,
`end` that returns a prepared logging object. Using this returned object with
the usual logging methods will automatically compute the elapsed time from the
instantiation of the RequestLogger to the moment it is called, by using an
internal hi-res time generated at the instantiation of the logger.
```javascript
import Logger from 'werelogs'; import Logger from 'werelogs';
/* /*
* Here, configure your WereLogs Logger at a global level * Here, configure your WereLogs Logger at a global level
* It can be instantiated with a Name (for the module), and a config options * It can be instanciated with a Name (for the module), and a config options
* Object. * Object.
* *
* This config options object contains a log level called 'level', a log * This config options object contains a log level called 'level', a log
* dumping threshold called 'dump'. The only unnecessary * dumping threshold called 'dump', and an array of stream called 'streams'.
* field is the 'level' of each individual stream, as werelogs is managing * The 'streams' option shall follow bunyan's configuration needs, as werelogs
* that on its own. * acts almost as a passthrough for this specific option. The only unnecessary
* field is the 'level' of each individual stream, as werelogs is managning
* that on its own. For the reference about how to configure bunyan's streams,
* please refer to its repository's readme:
* https://github.com/trentm/node-bunyan
* *
* All request loggers instantiated through this Logger will inherit its * All request loggers instanciated through this Logger will inherit its
* configuration. * configuration.
*/ */
const log = new Logger( const log = new Logger(
@ -83,6 +53,19 @@ const log = new Logger(
{ {
level: 'debug', level: 'debug',
dump: 'error', dump: 'error',
streams: [
// A simple, usual logging stream
{ stream: process.stdout},
// A more complex logging stream provided by a bunyan plugin
// that will upload the logs directly to an ELK's logstash service
{
type: 'raw',
stream: require('bunyan-logstash').createStream({
host: '127.0.0.1',
port: 5505,
}),
}
],
} }
); );
@ -94,28 +77,18 @@ const log = new Logger(
* Logger. * Logger.
*/ */
log.info('Application started.'); log.info('Application started.');
log.warn('Starting RequestLogging...', {'metadata': new Date()}); log.warn({'metadata': new Date()}, 'Starting RequestLogging...');
doSomething(reqLogger) { doSomething(reqLogger) {
/*
* Let's add some kind of client-related data as default attributes first
*/
reqLogger.addDefaultFields({ clientIP: '127.0.0.1',
clientPort: '65535',
clientName: 'Todd'});
/* /*
* Then, you can log some data, either a string or an object, using one of * Then, you can log some data, either a string or an object, using one of
* the logging methods: 'trace', 'debug', 'info', 'warn', 'error', or * the logging methods: 'trace', 'debug', 'info', 'warn', 'error', or
* 'fatal'. * 'fatal'.
*/ */
reqLogger.info('This is a string log entry'); reqLogger.info('This is a string log entry');
// This example provides additional information to include into the JSON reqLogger.info('This is a template string log entry with an included date:'
reqLogger.info('Placing bet...', + ` ${new Date().toISOString()}`);
{ date: new Date().toISOString(), reqLogger.info({ status: 200, hasStuff: false });
odds: [1, 1000],
amount: 20000,
});
} }
function processRequest() { function processRequest() {
@ -128,26 +101,11 @@ function processRequest() {
*/ */
const reqLogger = log.newRequestLogger(); const reqLogger = log.newRequestLogger();
/* you need to provide your logger instance to the code that requires it, /* you need to provide your logger instance to the code that requires it, as
* as it is not a module-wide object instance */ * it is not a module-wide object instance */
doSomething(reqLogger, ...); doSomething(reqLogger, ...);
... ...
/*
* Planning for some specific data to be included in the last logging
* request, you could use the addDefaultFields of the end()'s object:
*/
reqLogger.end().addDefaultFields({method: 'GET', client: client.getIP()})
/*
* This call will generate a log entry with an added elapsed_ms
* field. This object can only be used once, as it should only be used for
* the last log entry associated to this specific RequestLogger.
* This call will be reusing potential data fields previously added through
* end().addDefaultFields().
*/
reqLogger.end().info('End of request.', { status: 200 });
} }
``` ```
@ -157,3 +115,8 @@ In order to find out the known issues, it is advised to take a look at the
[project's github page](http://github.com/scality/werelogs). There, you should [project's github page](http://github.com/scality/werelogs). There, you should
be able to find the issues, tagged with the releases they are impacting, be able to find the issues, tagged with the releases they are impacting,
whether they're open or closed. whether they're open or closed.
## Contributing
The contributing rules for this project are defined in the associated
CONTRIBUTING.md file.

11
circle.yml Normal file
View File

@ -0,0 +1,11 @@
general:
artifacts:
- coverage/
machine:
node:
version: 4.1.0
test:
override:
- npm run lint_md
- npm run lint
- npm run coverage

65
index.d.ts vendored
View File

@ -1,65 +0,0 @@
interface WerelogsConfigOptions {
level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
dump?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
streams?: object[];
}
declare class WerelogsConfig {
constructor(config?: WerelogsConfigOptions);
reset(): WerelogsConfig;
update(config: WerelogsConfig): WerelogsConfig;
logger: any;
level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
dump: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
end: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
}
interface LogDictionary {
httpMethod?: string;
httpURL?: string;
[field: string]: any;
}
declare module 'werelogs' {
export class RequestLogger {
constructor(
logger: any,
logLevel: string,
dumpThreshold: string,
endLevel: string,
uids?: string|Array<string>
);
getUids(): Array<string>;
getSerializedUids(): string;
addDefaultFields(fields: LogDictionary): LogDictionary;
trace(msg: string, data?: LogDictionary): void;
debug(msg: string, data?: LogDictionary): void;
info(msg: string, data?: LogDictionary): void;
warn(msg: string, data?: LogDictionary): void;
error(msg: string, data?: LogDictionary): void;
fatal(msg: string, data?: LogDictionary): void;
end(msg: string, data?: LogDictionary): void;
errorEnd(msg: string, data?:LogDictionary): void;
}
export class Logger {
name: string;
constructor(name: string);
newRequestLogger(uids?: string|Array<string>): RequestLogger;
newRequestLoggerFromSerializedUids(uids: string): RequestLogger;
trace(msg: string, data?: LogDictionary): void;
debug(msg: string, data?: LogDictionary): void;
info(msg: string, data?: LogDictionary): void;
warn(msg: string, data?: LogDictionary): void;
error(msg: string, data?: LogDictionary): void;
fatal(msg: string, data?: LogDictionary): void;
}
export function configure(config: WerelogsConfigOptions): void;
export class API {
constructor(config: WerelogsConfigOptions);
reconfigure(config: WerelogsConfigOptions): void;
Logger: Logger;
}
}

View File

@ -1,51 +1 @@
const API = require('./lib/api.js'); module.exports = require('./target/lib/Logger.js').default;
const stderrUtils = require('./lib/stderrUtils');
/*
* For convenience purposes, we provide an already instanciated API; so that
* old uses of the imported Logger class can be kept as-is. For quick logging,
* this also provides a hassle-free way to log using werelogs.
*/
const werelogs = new API();
module.exports = {
Logger: werelogs.Logger,
configure: werelogs.reconfigure.bind(werelogs),
Werelogs: API,
/**
* Timestamp logs going to stderr
*
* @example <caption>Simplest usage</caption>
* ```
* const { stderrUtils } = require('werelogs');
* stderrUtils.catchAndTimestampStderr();
* ```
*
* @example <caption>Manage process exit</caption>
* ```
* const { stderrUtils } = require('werelogs');
* // set exitCode to null to keep process running on uncaughtException
* stderrUtils.catchAndTimestampStderr(undefined, null);
* // application init
* process.on('uncaughtException', (err) => {
* // custom handling, close connections, files
* this.worker.kill(); // or process.exit(1);
* });
* // Note you could use prependListener to execute your callback first
* // and then let stderrUtils exit the process.
* ```
*
* @example <caption>Custom listener</caption>
* ```
* const { stderrUtils } = require('werelogs');
* stderrUtils.catchAndTimestampWarning();
* // application init
* process.on('uncaughtException', (err, origin) => {
* stderrUtils.printErrorWithTimestamp(err, origin);
* // close and stop everything
* process.exit(1);
* });
* ```
*/
stderrUtils,
};

View File

@ -1,191 +0,0 @@
// eslint-disable-line strict
const LogLevel = require('./LogLevel.js');
const SimpleLogger = require('./SimpleLogger.js');
/**
* This class is the central point of the once-only configuration system for
* werelogs. It is instanciated once to be exported, and all configuration
* operations go through this object. This is the way we chose for the
* configuration to be shared at a library-level, in order to set the
* configuration only once as a user of werelogs.
*/
class Config {
/**
* This is the default constructor of the Config Object, and the only way
* to instanciate it (with default parameters).
*
* @param {object} conf - A configuration object for werelogs.
* @param {string} conf.level - The string name of the logging level
* ('trace', 'debug', 'info', 'warn',
* 'error' and 'fatal' in order of
* importance.)
* @param {string} conf.dump - The string name of the log dumping
* level ('trace', 'debug', 'info',
* 'warn', 'error' and 'fatal' in order
* of importance.)
* @param {object[]} conf.streams - The array of streams into which to
* log. This is an Array of objects
* which have a field named 'stream',
* which is writeable.
*
* @returns {undefined}
*/
constructor(conf) {
this.logLevel = 'info';
this.dumpThreshold = 'error';
this.endLevel = 'info';
this.streams = [{ level: 'trace', stream: process.stdout }];
this.simpleLogger = new SimpleLogger('werelogs', this.streams);
if (conf) {
this.update(conf);
}
}
/**
* This method resets the state of the Config Object, by setting only
* default values inside.
*
* @returns {Config} - this
*/
reset() {
return this.update({
level: 'info',
dump: 'error',
end: 'info',
streams: [{ level: 'trace', stream: process.stdout }],
});
}
/**
* This is the central nervous system of this Configuration object.
* It supports updating the current configuration, and will re-generate
* a bunyan logger if the streams array changed.
*
* @param {object} config - A configuration object for werelogs.
* @param {string} config.level - The string name of the logging level
* ('trace', 'debug', 'info', 'warn',
* 'error' and 'fatal' in order of
* importance.)
* @param {string} config.dump - The string name of the log dumping level
* ('trace', 'debug', 'info', 'warn',
* 'error' and 'fatal' in order of
* importance.)
* @param {object[]} config.streams - The array of streams into which to
* log. This is an Array of objects
* which have a field named 'stream',
* which is writeable.
*
* @see [Bunyan's documentation]{@link
* https://github.com/trentm/node-bunyan/blob/master/README.md#streams} for
* a more detailed description of the streams array configuration.
*
* @returns {Config} - this
*/
update(config) {
if (!config) {
return this;
}
const checkedConfig = config || {};
if (Object.prototype.hasOwnProperty.call(checkedConfig, 'level')) {
LogLevel.throwIfInvalid(checkedConfig.level);
}
if (Object.prototype.hasOwnProperty.call(checkedConfig, 'dump')) {
LogLevel.throwIfInvalid(checkedConfig.dump);
}
// for check log level vs. log dump level
const newLogLevel = checkedConfig.level || this.logLevel;
const newLogDumpLevel = checkedConfig.dump || this.dumpThreshold;
if (newLogDumpLevel
&& !LogLevel.shouldLog(newLogDumpLevel, newLogLevel)) {
throw new Error(
'Logging level should be at most logging dump level',
);
}
if (Object.prototype.hasOwnProperty.call(checkedConfig, 'level')) {
this.logLevel = checkedConfig.level;
}
if (Object.prototype.hasOwnProperty.call(checkedConfig, 'dump')) {
this.dumpThreshold = checkedConfig.dump;
}
if (Object.prototype.hasOwnProperty.call(checkedConfig, 'end')) {
LogLevel.throwIfInvalid(checkedConfig.end);
this.endLevel = checkedConfig.end;
}
if (Object.prototype.hasOwnProperty.call(checkedConfig, 'streams')) {
if (!Array.isArray(checkedConfig.streams)) {
throw new TypeError('WereLogs config.streams must be an Array '
+ 'of Writeable Streams.');
}
if (!checkedConfig.streams.length) {
throw new Error('Werelogs config.streams must contain at '
+ 'least one stream.');
}
this.streams = checkedConfig.streams.map(stream => {
stream.level = 'trace'; // eslint-disable-line no-param-reassign
return stream;
});
this.simpleLogger = new SimpleLogger('werelogs', this.streams);
}
return this;
}
/**
* This function is a simple getter to get access to the config's internal
* bunyan Logger. By using this function instead of keeping a reference to
* a bunyan Logger instance, we make it so that we can switch loggers
* on-the-fly for the loggers that rely on this.
*
* @returns {Bunyan.Logger} - A Bunyan logger to be used for logging
* operations by the user code.
*/
get logger() {
return this.simpleLogger;
}
/**
* This function is a simple getter to get access to the config's internal
* logging level. By using this function instead of keeping a reference to
* it, we make it so that we can switch configuration on-the-fly for the
* loggers that rely on this.
*
* @returns {string} - The configured logging level
*/
get level() {
return this.logLevel;
}
/**
* This function is a simple getter to get access to the config's internal
* dump Threshold. By using this function instead of keeping a reference to
* it, we make it so that we can switch configuration on-the-fly for the
* loggers that rely on this.
*
* @returns {string} - The configured dump threshold
*/
get dump() {
return this.dumpThreshold;
}
/**
* This function is a simple getter to get access to the config's internal
* end logging level. By using this function instead of keeping a reference
* to it, we make it so that we can switch configuration on-the-fly for the
* loggers that rely on this.
*
* @returns {string} - The configured 'end' logging level
*/
get end() {
return this.endLevel;
}
}
module.exports = Config;

View File

@ -1,6 +1,3 @@
// eslint-disable-line strict
const logLevels = [ const logLevels = [
'trace', 'trace',
'debug', 'debug',
@ -15,19 +12,12 @@ const logLevels = [
* *
* @function throwIfInvalid * @function throwIfInvalid
* *
* @param {string} level The logging level's name to be checked * @param level {string} The logging level's name to be checked
* *
* @throw {Error} A human-readable message that tells which log * @throw {Error} A human-readable message that tells which log levels are
* levels are supported. * supported.
* @throw {TypeError} A human-readable message indicating that the
* provided logging level was not a string
*
* @returns {undefined}
*/ */
function throwIfInvalid(level) { export function throwIfInvalid(level) {
if (typeof level !== 'string') {
throw new TypeError('Logging level should be a string');
}
if (logLevels.indexOf(level) === -1) { if (logLevels.indexOf(level) === -1) {
throw new RangeError(`Invalid logging level: ${level} is neither` throw new RangeError(`Invalid logging level: ${level} is neither`
+ ` ${logLevels.join(', ')}.`); + ` ${logLevels.join(', ')}.`);
@ -42,20 +32,15 @@ function throwIfInvalid(level) {
* *
* @function shouldLog * @function shouldLog
* *
* @param {string} level The level of the log entry for which to check * @param level {string} The level of the log entry for which to check
* if it should log * if it should log
* @param {string} floorLevel The configured logging level, acting as a floor * @param floorLevel {string} The configured logging level, acting as a floor
* under which entries are not logged. * under which entries are not logged.
* *
* @return {boolean} true if the entry of log level 'level' should be output * @return {boolean} true if the entry of log level 'level' should be output
* false if the entry of log level 'level' should not be * false if the entry of log level 'level' should not be
* output * output
*/ */
function shouldLog(level, floorLevel) { export function shouldLog(level, floorLevel) {
return logLevels.indexOf(level) >= logLevels.indexOf(floorLevel); return logLevels.indexOf(level) >= logLevels.indexOf(floorLevel);
} }
module.exports = {
throwIfInvalid,
shouldLog,
};

View File

@ -1,175 +1,143 @@
import bunyan from 'bunyan';
// eslint-disable-line strict import * as LogLevel from './LogLevel.js';
import RequestLogger from './RequestLogger.js';
import { unserializeUids } from './Utils.js';
const LogLevel = require('./LogLevel.js'); export default class Logger {
const RequestLogger = require('./RequestLogger.js');
const { unserializeUids } = require('./Utils.js');
const Config = require('./Config.js');
class Logger {
/** /**
* This is the constructor of the Logger class. It takes optional * This is the constructor of the Logger class. It takes optional
* configuration parameters, that allow to modify its behavior. * configuration parameters, that allow to modify its behavior.
* *
* @param {Werelogs.Config} config - An instanciated Werelogs Config object * @param name {string} The name of the Logger. It can be found later on in
* the log entries.
* *
* @param {string} name - The name of the Logger. It can be found later on * @param config {Object} A configuration object for werelogs. It may
* in the log entries. * contain the following fields:
* * - 'level', being the string name of the logging
* @returns {undefined} * level
* - 'dump', being the string name of the logging
* dump Threshold
* - 'streams', being an array of Objects to be
* used by the underlying library bunyan as the
* 'streams' configuration field, for compatibility
* (and simplicity of werelog's code) purposes. As
* such, the streams are left almost untouched,
* except the level that is forced to 'trace', as
* werelogs is internally managning the logging
* level (due to the dump threshold mecanisms)
*/ */
constructor(config, name) { constructor(name, config = {}) {
if (!(config instanceof Config)) {
throw new TypeError('Invalid parameter Type for "config".');
}
if (!(typeof name === 'string' || name instanceof String)) {
throw new TypeError('Invalid parameter Type for "name".');
}
this.config = config;
this.name = name; this.name = name;
if (config.hasOwnProperty('level')) {
LogLevel.throwIfInvalid(config.level);
this.logLevel = config.level;
} else {
this.logLevel = 'info';
}
if (config.hasOwnProperty('dump')) {
LogLevel.throwIfInvalid(config.dump);
this.dumpThreshold = config.dump;
} else {
this.dumpThreshold = 'error';
}
if (config.hasOwnProperty('streams')) {
if (!Array.isArray(config.streams)) {
throw new Error('WereLogs config.streams must be an Array of Writeable Streams.');
}
if (!config.streams.length) {
throw new Error('Werelogs config.streams must contain at least one stream.');
}
this.streams = config.streams.map((stream) => {
stream.level = 'trace';
return stream;
});
} else {
this.streams = [{ level: 'trace', stream: process.stdout }];
}
this.bLogger = bunyan.createLogger({
name: this.name,
streams: this.streams,
});
}
setLevel(levelName) {
LogLevel.throwIfInvalid(levelName);
this.logLevel = levelName;
}
setDumpThreshold(levelName) {
LogLevel.throwIfInvalid(levelName);
this.dumpThreshold = levelName;
} }
/** /**
* This method creates a Request Logger using an array of UIDs or an * This method creates a Request Logger using an array of UIDs or an
* explicit UID to use as the origin request ID. * explicit UID to use as the origin request ID.
* *
* @param {(string|string[]|undefined)} uids - The uid List to set * @param {string|array|undefined} the uid List to set
* *
* @returns {RequestLogger} A Valid Request Logger * @returns {RequestLogger} a Valid Request Logger
*/ */
newRequestLogger(uids) { newRequestLogger(uids) {
const rLog = new RequestLogger(this.config.logger, return new RequestLogger(this.bLogger,
this.config.level, this.config.dump, this.logLevel, this.dumpThreshold,
this.config.end, uids); uids);
rLog.addDefaultFields({ name: this.name });
return rLog;
} }
/** /**
* This method creates a Request Logger using a serialized list of UIDs to * This method creates a Request Logger using a serialized list of UIDs to
* set the UID list into the newly created Request Logger.. * set the UID list into the newly created Request Logger..
* *
* @param {string} serializedUids - The Serialized UID list * @param {string} the Serialized UID list
* *
* @returns {RequestLogger} A Valid Request Logger * @returns {RequestLogger} a Valid Request Logger
*/ */
newRequestLoggerFromSerializedUids(serializedUids) { newRequestLoggerFromSerializedUids(serializedUids) {
const rLog = new RequestLogger(this.config.logger, return new RequestLogger(this.bLogger,
this.config.level, this.config.dump, this.logLevel, this.dumpThreshold,
this.config.end, unserializeUids(serializedUids));
unserializeUids(serializedUids));
rLog.addDefaultFields({ name: this.name });
return rLog;
} }
_doLog(levelName, msg, data) { /**
const sLogger = this.config.logger; * Logging functions
const finalData = { name: this.name, time: Date.now() }; *
if (!LogLevel.shouldLog(levelName, this.config.level)) { * For the module-level logging,
*/
_doLog(levelName, args) {
if (!LogLevel.shouldLog(levelName, this.logLevel)) {
return; return;
} }
if (data !== undefined && typeof data !== 'object') { const logArgs = Array.prototype.splice.apply(args);
sLogger.fatal( this.bLogger[levelName].apply(this.bLogger, logArgs);
{
callparams: [msg, data],
},
'Werelogs API was mis-used.'
+ ' This development error should be fixed ASAP.',
);
return;
}
if (data) {
Object.keys(data).forEach(k => {
finalData[k] = data[k];
});
}
const args = [finalData, msg];
sLogger[levelName].apply(sLogger, args);
} }
/** trace() {
* Logging function to write a trace-level log entry. this._doLog('trace', arguments);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
trace(msg, data) {
this._doLog('trace', msg, data);
} }
/** debug() {
* Logging function to write a debug-level log entry. this._doLog('debug', arguments);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
debug(msg, data) {
this._doLog('debug', msg, data);
} }
/** info() {
* Logging function to write a info-level log entry. this._doLog('info', arguments);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
info(msg, data) {
this._doLog('info', msg, data);
} }
/** warn() {
* Logging function to write a warn-level log entry. this._doLog('warn', arguments);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
warn(msg, data) {
this._doLog('warn', msg, data);
} }
/** error() {
* Logging function to write a error-level log entry. this._doLog('error', arguments);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
error(msg, data) {
this._doLog('error', msg, data);
} }
/** fatal() {
* Logging function to write a fatal-level log entry. this._doLog('fatal', arguments);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
fatal(msg, data) {
this._doLog('fatal', msg, data);
} }
} }
module.exports = Logger;

View File

@ -1,203 +1,70 @@
import * as LogLevel from './LogLevel.js';
// eslint-disable-line strict import { generateUid, serializeUids } from './Utils.js';
const LogLevel = require('./LogLevel.js');
const Utils = require('./Utils.js');
const { serializeUids, generateUid, objectCopy } = Utils;
function ensureUidValidity(uid) { function ensureUidValidity(uid) {
if (uid.indexOf(':') !== -1) { if (uid.indexOf(':') !== -1) {
throw new Error(`RequestLogger UID "${uid}" contains an illegal ` throw new Error(`RequestLogger UID "${uid}" contains an illegal character: ':'.`);
+ 'character: \':\'.');
} }
return uid; return uid;
} }
class EndLogger {
constructor(reqLogger) {
this.logger = reqLogger;
this.fields = {};
}
augmentedLog(level, msg, data) {
// We can alter current instance, as it won't be usable after this
// call.
this.fields = objectCopy(this.fields, data || {});
return this.logger.log(level, msg, this.fields, true);
}
/**
* This function allows the user to add default fields to include into all
* JSON log entries generated through this request logger. As this function
* attempt not to modify the provided fields object, it copies the field
* into a new object for safe keeping.
*
* @param {object} fields The dictionnary of additional fields to include
* by default for this instance of the
* RequestLogger.
*
* @returns {object} The previous set of default fields (can be
* safely ignored).
*/
addDefaultFields(fields) {
const oldFields = this.fields;
this.fields = objectCopy({}, this.fields, fields);
return oldFields;
}
/**
* Logging function to write a trace-level log entry as the last log entry.
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
trace(msg, data) {
this.augmentedLog('trace', msg, data);
}
/**
* Logging function to write a debug-level log entry as the last log entry.
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
debug(msg, data) {
this.augmentedLog('debug', msg, data);
}
/**
* Logging function to write a info-level log entry as the last log entry.
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
info(msg, data) {
this.augmentedLog('info', msg, data);
}
/**
* Logging function to write a warn-level log entry as the last log entry.
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
warn(msg, data) {
this.augmentedLog('warn', msg, data);
}
/**
* Logging function to write a error-level log entry as the last log entry.
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
error(msg, data) {
this.augmentedLog('error', msg, data);
}
/**
* Logging function to write a fatal-level log entry as the last log entry.
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
fatal(msg, data) {
this.augmentedLog('fatal', msg, data);
}
}
/** /**
* WereLogs Request Logger. This class provides all information required * WereLogs Request Logger. This class provides all information required
* to bufferise and dump log entries whenever it is relevant depending on * to bufferise and dump log entries whenever it is relevant depending on
* the global log level; and is used to track the log events for one given * the global log level; and is used to track the log events for one given
* request. * request.
*/ */
class RequestLogger { export default class RequestLogger {
/** /**
* Constructor of the WereLogs Request Logger. * Constructor of the WereLogs Request Logger.
* This function takes a logger instance, a logging level, and a last * This function takes a logger instance, a logging level, and a last
* parameter corresponding to the request UID. * parameter corresponding to the request UID.
* *
* @param { bunyan.Logger } logger - A bunyan logger instance by which all * @param logger { bunyan.Logger } A bunyan logger instance by which all
* logging output will go through, * logging output will go through,
* provided by the Werelogs main Logger. * provided by the Werelogs main Logger.
* @param { string } logLevel - The floor logging level. All logging * @param loglevel { string } The floor logging level. All logging
* requests equal or above this level * requests equal or above this level will
* will be immediately output. Others * be immediately output. Others will get
* will get bufferised. * bufferised.
* @param { string } dumpThreshold - The floor dumping level. The full * @param dumpThreshold { string } The floor dumping level. The full
* logging history (conditionned by the * logging history (conditionned by the
* logging level) is dumped whenever a * logging level) is dumped whenever a log
* log entry reaches this level. This * entry reaches this level. This helps
* helps understanding any kind of error * understanding any kind of error
* happenning in the system, and have a * happenning in the system, and have a
* track of a whole request, at the point * track of a whole request, at the point
* where it fails. dumpThreshold should * where it fails. dumpThreshold should be
* be equal or greater than logLevel * equal or greater than logLevel (geater
* (geater meaning on the more critical * meaning on the more critical side of)
* side of) * @param uids { string[] | string | undefined } An Unique Id in a String
* @param { string } endLevel - The logging level to use for the * format or Unique ID List of the parent
* special finish logging call 'end()' * request IDs. Any String of the UID shall
* @param {(string[] | string | undefined)} [uids] - An Unique ID in a * not contain any colon, due to the
* String format or Unique ID List of the * serialization format chosen for the UID
* parent request IDs. Any String of the * List.
* UID shall not contain any colon, due
* to the serialization format chosen for
* the UID List.
*
* @returns {undefined}
*/ */
constructor(logger, logLevel, dumpThreshold, endLevel, uids) { constructor(logger, logLevel, dumpThreshold, uids = undefined) {
let uidList; let uidList = undefined;
if (!LogLevel.shouldLog(dumpThreshold, logLevel)) { if (!LogLevel.shouldLog(dumpThreshold, logLevel)) {
throw new Error('Logging Dump level should be equal or' throw new Error('Logging Dump level should be equal or'
+ ' higher than logging filter level.'); + ' higher than logging filter level.');
} }
if (uids !== undefined && Array.isArray(uids)) { if (Array.isArray(uids)) {
uidList = uids.map(uid => ensureUidValidity(uid)); uidList = uids.map(uid => ensureUidValidity(uid));
uidList.push(generateUid()); uidList.push(generateUid());
} else if (uids !== undefined && typeof uids === 'string') { } else if (typeof(uids) === 'string') {
uidList = [ensureUidValidity(uids)]; uidList = [ ensureUidValidity(uids) ];
} }
this.uids = uidList || [generateUid()]; this.uids = uidList || [ generateUid() ];
this.entries = []; this.entries = [];
this.fields = {};
this.logLevel = logLevel; this.logLevel = logLevel;
this.dumpThreshold = dumpThreshold; this.dumpThreshold = dumpThreshold;
this.endLevel = endLevel; this.bLogger = logger;
this.endLogger = new EndLogger(this);
this.sLogger = logger;
this.startTime = process.hrtime();
this.elapsedTime = null;
} }
/** /**
@ -208,8 +75,8 @@ class RequestLogger {
* @note The call to the map function is to make sure we get a copy of the * @note The call to the map function is to make sure we get a copy of the
* array instead of a reference to it. * array instead of a reference to it.
* *
* @returns {string[]} A copy of the internal array of UIDs. It shall * @returns A copy of the internal array of UIDs. It shall never be empty,
* never be empty, null or undefined. * null or undefined.
*/ */
getUids() { getUids() {
return this.uids.map(uid => uid); return this.uids.map(uid => uid);
@ -222,166 +89,35 @@ class RequestLogger {
* file-format between two components of a distributed architecture, that * file-format between two components of a distributed architecture, that
* would use the UID namespacing to track a request. * would use the UID namespacing to track a request.
* *
* @returns {string} The serialized data of the UID List contained by the * @returns {string} The serialized data of the UID List contained by the
* Request Logger. * Request Logger.
*/ */
getSerializedUids() { getSerializedUids() {
return serializeUids(this.uids); return serializeUids(this.uids);
} }
/** trace(message) {
* This function allows the user to add default fields to include into all return this.log('trace', message);
* JSON log entries generated through this request logger. As this function
* attempt not to modify the provided fields object, it copies the field
* into a new object for safe keeping.
*
* @param {object} fields The dictionnary of additional fields to include
* by default for this instance of the
* RequestLogger.
*
* @returns {object} The previous set of default fields (can be
* safely ignored).
*/
addDefaultFields(fields) {
const oldFields = this.fields;
this.fields = objectCopy({}, this.fields, fields);
return oldFields;
} }
/** debug(message) {
* Logging function to write a trace-level log entry. return this.log('debug', message);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
trace(msg, data) {
return this.log('trace', msg, data);
} }
/** info(message) {
* Logging function to write a debug-level log entry. return this.log('info', message);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
debug(msg, data) {
return this.log('debug', msg, data);
} }
/** warn(message) {
* Logging function to write a info-level log entry. return this.log('warn', message);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
info(msg, data) {
return this.log('info', msg, data);
} }
/** error(message) {
* Logging function to write a warn-level log entry. return this.log('error', message);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
warn(msg, data) {
return this.log('warn', msg, data);
} }
/** fatal(message) {
* Logging function to write a error-level log entry. return this.log('fatal', message);
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
error(msg, data) {
return this.log('error', msg, data);
}
/**
* Logging function to write a fatal-level log entry.
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
fatal(msg, data) {
return this.log('fatal', msg, data);
}
/**
* @version 1.1
* @method RequestLogger#end
*
* @description
* Function returning a wrapped RequestLogger to be used at the end of a
* given request. It will automatically include an elapsed_ms data field
* in the associated log entry.
*
* @returns {EndLogger} The wrapped RequestLogger fit to actually do the
* last logging operation, whatever logging level it
* will be.
*/
/**
* @deprecated since version 1.1
*
* @version 1.0
* @method RequestLogger#end
*
* @description
* Logging function to write the last log entry for the given RequestLogger
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
end(msg, data) {
if (msg === undefined && data === undefined) {
return this.endLogger;
}
return this.log(this.endLevel, msg, data, true);
}
/**
* @deprecated since version 1.1
*
* Logging function to finish an elapsed counter in error level
*
* @param {string} msg - The message string to include in the log entry.
* @param {object} [data] - The object providing additional JSON fields
* for the log entry. This is how to provide
* metadata for a specific log entry.
*
* @returns {undefined}
*/
errorEnd(msg, data) {
return this.log('error', msg, data, true);
} }
/** /**
@ -390,76 +126,30 @@ class RequestLogger {
* the dump threshold level, every past entry is dumped before this one, * the dump threshold level, every past entry is dumped before this one,
* no matter what their log levels were and what is the configured log * no matter what their log levels were and what is the configured log
* level. * level.
* This function also takes care of properly passing the input parameters
* to bunyan, to comply with its API as much as is reasonable.
* In order to avoid dumping multiple times the same entries, whenever the * In order to avoid dumping multiple times the same entries, whenever the
* dump threshold is reached, this function will be flushing its past * dump threshold is reached, this function will be flushing its past
* history after the dump operation is done. * history after the dump operation is done.
* *
* If the upperlaying API happens to be misused, this function will log a * @method
* fatal message including the parameters to the logging system, for the * @name log
* developer to find out what he did wrong.
* *
* @private * @param level {string} The log level of the log entry. It is
* * assumed that the level name has already
* @param {string} level - The log level of the log entry. It is * been checked and is valid.
* assumed that the level name has already * @param message {string} The arguments sent to the function calling this
* been checked and is valid. * utility one.
* @param {string} msg - The message to be printed out in the logs
* @param {object} [logFields] - The additional JSON fields to include for
* this specific log entry.
* @param {boolean} isEnd - The flag to tell whether the log entry is
* the 'end' log entry that must provide
* additional fields or not (elapsedTime)
*
* @returns {undefined}
*/ */
log(level, msg, logFields, isEnd) { log(level, msg) {
if (logFields !== undefined && typeof logFields !== 'object') {
this.log(
'fatal',
'Werelogs API was mis-used.'
+ ' This development error should be fixed ASAP.',
{
callparams: [msg, logFields],
},
);
return;
}
const fields = objectCopy({}, this.fields, logFields || {});
const endFlag = isEnd || false;
/*
* using Date.now() as it's faster than new Date(). logstash component
* uses this field to generate ISO 8601 timestamp
*/
if (fields.time === undefined) {
fields.time = Date.now();
}
// eslint-disable-next-line camelcase
fields.req_id = serializeUids(this.uids);
if (endFlag) {
if (this.elapsedTime !== null) {
// reset elapsedTime to avoid an infinite recursion
// while logging the error
this.elapsedTime = null;
this.error('RequestLogger.end() has been called more than once');
}
this.elapsedTime = process.hrtime(this.startTime);
// eslint-disable-next-line camelcase
fields.elapsed_ms = this.elapsedTime[0] * 1000
+ this.elapsedTime[1] / 1000000;
}
const logEntry = { const logEntry = {
level, level,
fields,
msg, msg,
hrtime: process.hrtime(),
req_id: this.uids.join(':'),
}; };
this.entries.push(logEntry); this.entries.push(logEntry);
if (LogLevel.shouldLog(level, this.dumpThreshold)) { if (LogLevel.shouldLog(level, this.dumpThreshold)) {
this.entries.forEach(entry => { this.entries.forEach((entry) => {
this.doLogIO(entry); this.doLogIO(entry);
}); });
this.entries = []; this.entries = [];
@ -468,45 +158,34 @@ class RequestLogger {
} }
} }
/**
* This function transmits a log entry to the configured bunyan logger
* instance. This way, we can rely on bunyan for actual logging operations,
* and logging multiplexing, instead of managing that ourselves.
*
* @private
*
* @param {object} logEntry - The Logging entry to be passed to
* bunyan
* @param {string} logEntry.msg - The message to be logged
* @param {object} logEntry.fields - The data fields to associate to the
* log entry.
*
* @returns {undefined}
*/
doLogIO(logEntry) { doLogIO(logEntry) {
switch (logEntry.level) { switch (logEntry.level) {
case 'trace': case 'trace':
this.sLogger.trace(logEntry.fields, logEntry.msg); this.bLogger.trace({ hrtime: logEntry.hrtime,
req_id: logEntry.req_id }, logEntry.msg);
break; break;
case 'debug': case 'debug':
this.sLogger.debug(logEntry.fields, logEntry.msg); this.bLogger.debug({ hrtime: logEntry.hrtime,
req_id: logEntry.req_id }, logEntry.msg);
break; break;
case 'info': case 'info':
this.sLogger.info(logEntry.fields, logEntry.msg); this.bLogger.info({ hrtime: logEntry.hrtime,
req_id: logEntry.req_id }, logEntry.msg);
break; break;
case 'warn': case 'warn':
this.sLogger.warn(logEntry.fields, logEntry.msg); this.bLogger.warn({ hrtime: logEntry.hrtime,
req_id: logEntry.req_id }, logEntry.msg);
break; break;
case 'error': case 'error':
this.sLogger.error(logEntry.fields, logEntry.msg); this.bLogger.error({ hrtime: logEntry.hrtime,
req_id: logEntry.req_id }, logEntry.msg);
break; break;
case 'fatal': case 'fatal':
this.sLogger.fatal(logEntry.fields, logEntry.msg); this.bLogger.fatal({ hrtime: logEntry.hrtime,
req_id: logEntry.req_id }, logEntry.msg);
break; break;
default: default:
throw new Error(`Unexpected log level: ${logEntry.level}`); throw new Error(`Unexpected log level: ${logEntry.level}`);
} }
} }
} }
module.exports = RequestLogger;

View File

@ -1,112 +0,0 @@
// eslint-disable-line strict
const os = require('os');
const safeJSONStringify = require('safe-json-stringify');
const fastJSONStringify = require('fast-safe-stringify')
function errorStackReplacer(key, value) {
if (value instanceof Error) {
return value.stack;
}
return value;
}
/*
* This function safely stringifies JSON. If an exception occcurs (due to
* circular references, exceptions thrown from object getters etc.), the module
* safe-json-stringify is used to remove the offending property and return a
* stringified object. This approach is ideal as it does not tax every log entry
* to use the module and has been tested using Cosbench to verify that it doesn't
* introduce any regression in performance.
*/
function safeStringify(obj) {
let str;
try {
// Try to stringify the object (fast version)
str = fastJSONStringify(obj, errorStackReplacer);
} catch (e) {
// fallback to remove circular object references or other exceptions
// eslint-disable-next-line no-param-reassign
obj.unsafeJSON = true;
return safeJSONStringify(obj, errorStackReplacer);
}
return str;
}
function isWriteableStream(s) {
if (!s.stream) {
return false;
}
// duck typing to check if the obect is a writeable stream
return s.stream.writable;
}
class SimpleLogger {
constructor(name, streams) {
this.name = name;
this.streams = [{ level: 'trace', stream: process.stdout }];
if (streams) {
if (!Array.isArray(streams)) {
throw new Error('Invalid streams. streams must be an array'
+ ' list of writeable streams');
}
/*
* This is for backwards compatibility. current config in projects
* create a bunyan-logstash stream which is not a compatible
* writeable stream. Any non-writable streams will be ignored.
* This will be changed to throw an error if non-writable stream is
* encountered.
*/
this.streams = streams.filter(isWriteableStream);
}
this.hostname = os.hostname();
}
log(level, fields, message) {
let logFields;
let logMsg;
if (message === undefined && typeof fields === 'string') {
logMsg = fields;
logFields = {};
} else {
logMsg = message;
logFields = fields || {};
}
// TODO - Protect these fields from being overwritten
logFields.level = level;
logFields.message = logMsg;
logFields.hostname = this.hostname;
logFields.pid = process.pid;
const safeString = safeStringify(logFields);
this.streams.forEach(s => s.stream
.write(`${safeString}\n`));
}
info(fields, message) {
this.log('info', fields, message);
}
debug(fields, message) {
this.log('debug', fields, message);
}
trace(fields, message) {
this.log('trace', fields, message);
}
warn(fields, message) {
this.log('warn', fields, message);
}
error(fields, message) {
this.log('error', fields, message);
}
fatal(fields, message) {
this.log('fatal', fields, message);
}
}
module.exports = SimpleLogger;

View File

@ -1,50 +1,26 @@
// eslint-disable-line strict
/**
* @constant
* @type {String[]} - The lookup table to generate the UID
*/
const lut = [];
for (let i = 0; i < 256; i++) {
lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
}
/** /**
* This function generates a string uid. * This function generates a string uid.
* *
* The base algorithm is taken from here: http://jcward.com/UUID.js * @return uid {string} An hexadecimal string representation of an unique id
* And is explained here: http://stackoverflow.com/a/21963136 * made of 80 bits.of entropy.
*
* @returns {string} An hexadecimal string representation of an unique
* id made of 80 bits.of entropy.
*/ */
function generateUid() { export function generateUid() {
const d0 = Math.random() * 0xffffffff | 0; function s4() {
const d1 = Math.random() * 0xffffffff | 0; return Math.floor((1 + Math.random()) * 0x10000)
const d2 = Math.random() * 0xffffffff | 0; .toString(16)
return lut[d0 & 0xff] .substring(1);
+ lut[d0 >> 8 & 0xff] }
+ lut[d0 >> 16 & 0xff] return s4() + s4() + s4() + s4() + s4();
+ lut[d1 & 0xff]
+ lut[d1 >> 8 & 0xff]
+ lut[d1 >> 16 & 0x0f | 0x40]
+ lut[d2 & 0x3f | 0x80]
+ lut[d2 >> 8 & 0xff]
+ lut[d2 >> 16 & 0xff]
+ lut[d2 >> 24 & 0xff];
} }
/** /**
* This function serializes an array of UIDs into a format suitable for any * This function serializes an array of UIDs into a format suitable for any
* text-based protocol or storage. * text-based protocol or storage.
* *
* @param {string[]} uidList - The array of string UIDs to serialize * @param {Array[string]} The array of string UIDs
*
* @returns {string} The serialized UID array in a string form
* *
*/ */
function serializeUids(uidList) { export function serializeUids(uidList) {
return uidList.join(':'); return uidList.join(':');
} }
@ -52,41 +28,10 @@ function serializeUids(uidList) {
* This function unserializes an array of UIDs from a string and returns the * This function unserializes an array of UIDs from a string and returns the
* generated Array. * generated Array.
* *
* @param {string} stringdata - The string data of the serialized array of UIDs * @param {string} The string data of the serialized array of UIDs
* *
* @returns {string[]} - The unserialized array of string UIDs * @returns {Array[string]} The unserialized array of string UIDs
*/ */
function unserializeUids(stringdata) { export function unserializeUids(stringdata) {
return stringdata.split(':'); return stringdata.split(':');
} }
/**
* This function copies the properties from the source object to the target
* object.
*
* @param {...object} target - object to be copied to
* @returns {object} - target object
*/
function objectCopy(target) {
const result = target;
/* eslint-disable prefer-rest-params */
const nb = arguments.length;
for (let i = 1; i < nb; i++) {
const source = arguments[i];
const keys = Object.keys(source);
const keysNb = keys.length;
for (let j = 0; j < keysNb; j++) {
const key = keys[j];
result[key] = source[key];
}
}
/* eslint-enable prefer-rest-params */
return result;
}
module.exports = {
generateUid,
serializeUids,
unserializeUids,
objectCopy,
};

View File

@ -1,72 +0,0 @@
// eslint-disable-line strict
const Config = require('./Config.js');
const Logger = require('./Logger.js');
class API {
/**
* This is the constructor of the Logger class. It takes optional
* configuration parameters, that allow to modify its behavior.
*
* @param {object} config - A configuration object for werelogs.
* @param {string} config.level - The name of the logging level ('trace',
* 'debug', 'info', 'warn', 'error' and
* 'fatal' in order of importance.)
* @param {string} config.dump - The name of the log dumping level
* ('trace', 'debug', 'info', 'warn',
* 'error' and 'fatal' in order of
* importance.)
* @param {object[]} config.streams - The streams into which to log. This
* is an Array of objects which have a
* field named 'stream', which is
* writeable.
*/
constructor(config) {
this.config = new Config(config);
this.preboundLogger = Logger.bind(null, this.config);
}
/**
* This is a thunk function that allows reconfiguring the streams and log
* levels of all Logger and future RequestLogger objects. Note that
* existing RequestLogger will live their lifespan retaining the old
* configuration.
* If the provided configuration is erroneous, the function may throw
* exceptions depending on the detected configuration error. Please see the
* Config class's documentation about that.
*
* @throws {TypeError} - One of the provided arguments is not of the
* expected type
* @throws {RangeError} - The logging level provided is not part of the
* supported logging levels
* @throws {Error} - A human-readable message providing details about
* a logic error due to the input parameters
* provided.
*
* @param {object} config - A configuration object for werelogs.
* @param {string} config.level - The name of the logging level ('trace',
* 'debug', 'info', 'warn', 'error' and
* 'fatal' in order of importance.)
* @param {string} config.dump - The name of the log dumping level
* ('trace', 'debug', 'info', 'warn',
* 'error' and 'fatal' in order of
* importance.)
* @param {object[]} config.streams - The streams into which to log. This
* is an Array of objects which have a
* field named 'stream', which is
* writeable.
*
* @returns {undefined}
*
*/
reconfigure(config) {
this.config.update(config);
}
get Logger() {
return this.preboundLogger;
}
}
module.exports = API;

View File

@ -1,106 +0,0 @@
/**
* @returns {string} a timestamp in ISO format YYYY-MM-DDThh:mm:ss.sssZ
*/
const defaultTimestamp = () => new Date().toISOString();
/**
* Prints on stderr a timestamp, the origin and the error
*
* If no other instructions are needed on uncaughtException,
* consider using `catchAndTimestampStderr` directly.
*
* @example
* process.on('uncaughtException', (err, origin) => {
* printErrorWithTimestamp(err, origin);
* // server.close();
* // file.close();
* process.nextTick(() => process.exit(1));
* });
* // Don't forget to timestamp warning
* catchAndTimestampWarning();
* @param {Error} err see process event uncaughtException
* @param {uncaughtException|unhandledRejection} origin see process event
* @param {string} [date=`defaultTimestamp()`] Date to print
* @returns {boolean} see process.stderr.write
*/
function printErrorWithTimestamp(
err, origin, date = defaultTimestamp(),
) {
return process.stderr.write(`${date}: ${origin}:\n${err.stack}\n`);
}
/**
* Prefer using `catchAndTimestampStderr` instead of this function.
*
* Adds listener for uncaughtException to print with timestamp.
*
* If you want to manage the end of the process, you can set exitCode to null.
* Or use `printErrorWithTimestamp` in your own uncaughtException listener.
*
* @param {Function} [dateFct=`defaultTimestamp`] Fct returning a formatted date
* @param {*} [exitCode=1] On uncaughtException, if not null, `process.exit`
* will be called with this value
* @returns {undefined}
*/
function catchAndTimestampUncaughtException(
dateFct = defaultTimestamp, exitCode = 1,
) {
process.on('uncaughtException', (err, origin) => {
printErrorWithTimestamp(err, origin, dateFct());
if (exitCode !== null) {
process.nextTick(() => process.exit(exitCode));
}
});
}
/**
* Forces the use of `--trace-warnings` and adds a date in warning.detail
* The warning will be printed by the default `onWarning`
*
* @param {string} [dateFct=`defaultTimestamp`] Fct returning a formatted date
* @returns {undefined}
*/
function catchAndTimestampWarning(dateFct = defaultTimestamp) {
process.traceProcessWarnings = true;
// must be executed first, before the default `onWarning`
process.prependListener('warning', warning => {
if (warning.detail) {
// eslint-disable-next-line no-param-reassign
warning.detail += `\nAbove Warning Date: ${dateFct()}`;
} else {
// eslint-disable-next-line no-param-reassign
warning.detail = `Above Warning Date: ${dateFct()}`;
}
});
}
/**
* Adds listener for uncaughtException and warning to print them with timestamp.
*
* If you want to manage the end of the process, you can set exitCode to null.
* Or use `printErrorWithTimestamp` in your own uncaughtException listener.
*
* @example
* const { stderrUtils } = require('werelogs');
* // first instruction in your index.js or entrypoint
* stderrUtils.catchAndTimestampStderr();
*
* @param {Function} [dateFct=`defaultTimestamp`] Fct returning a formatted date
* @param {*} [exitCode=1] On uncaughtException, if not null, `process.exit`
* will be called with this value
* @returns {undefined}
*/
function catchAndTimestampStderr(
dateFct = defaultTimestamp, exitCode = 1,
) {
catchAndTimestampUncaughtException(dateFct, exitCode);
catchAndTimestampWarning(dateFct);
}
module.exports = {
defaultTimestamp,
printErrorWithTimestamp,
catchAndTimestampUncaughtException,
catchAndTimestampWarning,
catchAndTimestampStderr,
};

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 E-conomic
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,18 +1,17 @@
{ {
"name": "werelogs", "name": "werelogs",
"engines": { "version": "0.0.1",
"node": ">=10"
},
"version": "8.1.5",
"description": "An efficient raw JSON logging library aimed at micro-services architectures.", "description": "An efficient raw JSON logging library aimed at micro-services architectures.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"gendoc": "jsdoc $(git ls-files 'lib/*.js') -d doc", "compile": "babel -d target/lib/ lib/ && babel -d target/tests/ tests/",
"lint": "eslint $(git ls-files '*.js')", "lint": "eslint $(git ls-files '*.js')",
"lint_md": "markdownlint $(git ls-files '*.md')", "lint_md": "mdlint $(git ls-files '*.md')",
"test": "mocha tests/unit/", "postinstall": "npm run compile",
"ft_test": "(npm pack && cp werelogs-*.tgz tests/functional && cd tests/functional && cp -R ../../node_modules/ node_modules/ && npm install werelogs-*.tgz && ./node_modules/.bin/mocha . multi-modules/ && rm -rf tests/functional/node_modules tests/functional/werelogs-*.tgz tests/functional/*lock*)", "prepublish": "npm run compile",
"coverage": "nyc ./node_modules/.bin/_mocha tests/unit" "pretest": "npm run compile",
"test": "mocha target/tests/unit/",
"coverage": "babel-node ./node_modules/istanbul/lib/cli.js cover ./node_modules/.bin/_mocha tests/unit"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,27 +25,29 @@
"library", "library",
"JSON" "JSON"
], ],
"author": "Giorgio Regni", "author": "David Pineau",
"license": "Apache-2.0", "license": "ISC",
"bugs": { "bugs": {
"url": "https://github.com/scality/werelogs/issues" "url": "https://github.com/scality/werelogs/issues"
}, },
"homepage": "https://github.com/scality/werelogs#readme", "homepage": "https://github.com/scality/werelogs#readme",
"dependencies": { "dependencies": {
"fast-safe-stringify": "^2.1.1", "babel-core": "^6.1.21",
"safe-json-stringify": "^1.2.0" "babel-plugin-transform-es2015-destructuring": "^6.1.18",
"babel-plugin-transform-es2015-modules-commonjs": "^6.2.0",
"babel-plugin-transform-es2015-parameters": "^6.1.18",
"babel-cli": "^6.1.21",
"bunyan": "GiorgioRegni/node-bunyan"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.32.0", "babel-cli": "^6.1.21",
"eslint-config-airbnb": "^18.2.1", "babel-eslint": "^4.1.6",
"eslint-config-scality": "git+https://git.yourcmc.ru/vitalif/zenko-eslint-config-scality.git", "eslint": "^1.10.1",
"eslint-plugin-import": "^2.22.1", "eslint-config-airbnb": "^1.0.2",
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-react": "^3.10.0",
"eslint-plugin-react": "^7.26.0", "istanbul": "^1.0.0-alpha",
"eslint-plugin-react-hooks": "^4.2.0", "istanbul-api": "==1.0.0-alpha.9",
"jsdoc": "^3.4.3", "mdlint": "^0.1.0",
"markdownlint-cli": "^0.27.1", "mocha": "^2.3.4"
"mocha": ">=3.1.2",
"nyc": "^15.1.0"
} }
} }

View File

@ -1,50 +1,61 @@
import assert from 'assert';
// eslint-disable-line strict import * as LogLevel from '../lib/LogLevel.js';
const assert = require('assert'); export class DummyLogger {
const LogLevel = require('../lib/LogLevel.js');
class DummyLogger {
constructor() { constructor() {
this.ops = []; this.ops = [];
this.counts = { this.counts = {
trace: 0, 'trace': 0,
debug: 0, 'debug': 0,
info: 0, 'info': 0,
warn: 0, 'warn': 0,
error: 0, 'error': 0,
fatal: 0, 'fatal': 0,
}; };
} }
trace(obj, msg) { trace(obj, ...params) {
this._doLog('trace', obj, msg); this.ops.push(['trace',
[obj, Array.prototype.splice.apply(params, [0])],
]);
this.counts.trace += 1;
} }
debug(obj, msg) { debug(obj, ...params) {
this._doLog('debug', obj, msg); this.ops.push(['debug',
[obj, Array.prototype.splice.apply(params, [0])],
]);
this.counts.debug += 1;
} }
info(obj, msg) { info(obj, ...params) {
this._doLog('info', obj, msg); this.ops.push(['info',
[obj, Array.prototype.splice.apply(params, [0])],
]);
this.counts.info += 1;
} }
warn(obj, msg) { warn(obj, ...params) {
this._doLog('warn', obj, msg); this.ops.push(['warn',
[obj, Array.prototype.splice.apply(params, [0])],
]);
this.counts.warn += 1;
} }
error(obj, msg) { error(obj, ...params) {
this._doLog('error', obj, msg); this.ops.push(['error',
[obj, Array.prototype.splice.apply(params, [0])],
]);
this.counts.error += 1;
} }
fatal(obj, msg) { fatal(obj, ...params) {
this._doLog('fatal', obj, msg); this.ops.push(['fatal',
} [obj, Array.prototype.splice.apply(params, [0])],
]);
_doLog(level, obj, msg) { this.counts.fatal += 1;
this.ops.push([level, [obj, msg]]);
this.counts[level] += 1;
} }
} }
@ -57,66 +68,41 @@ function computeBehavior(filterLevel, logLevel, testLevel) {
return { return {
value, value,
msg: `Expected ${logLevel} to be called ${value} times with ` 'msg': `Expected ${logLevel} to be called ${value} times with filter level ${filterLevel}.`,
+ `filter level ${filterLevel}.`,
}; };
} }
function genericFilterGenerator(filterLevel, testLevel, createLogger) { export default function filterGenerator(filterLevel, testLevel, createLogger) {
return function testFilter(done) { return function testFilter(done) {
let retObj; let value;
let msg;
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const logger = createLogger(dummyLogger, filterLevel); const logger = createLogger(dummyLogger, filterLevel);
switch (testLevel) { switch (testLevel) {
/* eslint-disable no-multi-spaces */
case 'trace': logger.trace('test trace'); break; case 'trace': logger.trace('test trace'); break;
case 'debug': logger.debug('test debug'); break; case 'debug': logger.debug('test debug'); break;
case 'info': logger.info('test info'); break; case 'info': logger.info('test info'); break;
case 'warn': logger.warn('test warn'); break; case 'warn': logger.warn('test warn'); break;
case 'error': logger.error('test error'); break; case 'error': logger.error('test error'); break;
case 'fatal': logger.fatal('test fatal'); break; case 'fatal': logger.fatal('test fatal'); break;
/* eslint-enable no-multi-spaces */
default: default:
done(new Error('Unexpected testLevel name: ', testLevel)); done(new Error('Unexpected testLevel name: ', testLevel));
} }
retObj = computeBehavior(filterLevel, 'trace', testLevel); ({ value, msg } = computeBehavior(filterLevel, 'trace', testLevel));
assert.strictEqual(dummyLogger.counts.trace, retObj.value, retObj.msg); assert.strictEqual(dummyLogger.counts.trace, value, msg);
retObj = computeBehavior(filterLevel, 'debug', testLevel); ({ value, msg } = computeBehavior(filterLevel, 'debug', testLevel));
assert.strictEqual(dummyLogger.counts.debug, retObj.value, retObj.msg); assert.strictEqual(dummyLogger.counts.debug, value, msg);
retObj = computeBehavior(filterLevel, 'info', testLevel); ({ value, msg } = computeBehavior(filterLevel, 'info', testLevel));
assert.strictEqual(dummyLogger.counts.info, retObj.value, retObj.msg); assert.strictEqual(dummyLogger.counts.info, value, msg);
retObj = computeBehavior(filterLevel, 'warn', testLevel); ({ value, msg } = computeBehavior(filterLevel, 'warn', testLevel));
assert.strictEqual(dummyLogger.counts.warn, retObj.value, retObj.msg); assert.strictEqual(dummyLogger.counts.warn, value, msg);
retObj = computeBehavior(filterLevel, 'error', testLevel); ({ value, msg } = computeBehavior(filterLevel, 'error', testLevel));
assert.strictEqual(dummyLogger.counts.error, retObj.value, retObj.msg); assert.strictEqual(dummyLogger.counts.error, value, msg);
retObj = computeBehavior(filterLevel, 'fatal', testLevel); ({ value, msg } = computeBehavior(filterLevel, 'fatal', testLevel));
assert.strictEqual(dummyLogger.counts.fatal, retObj.value, retObj.msg); assert.strictEqual(dummyLogger.counts.fatal, value, msg);
done(); done();
}; };
} }
function loggingMisuseGenerator(test, createLogger) {
return function generatedLogAPIMisuseTest(done) {
const dummyLogger = new DummyLogger();
const logger = createLogger(dummyLogger);
assert.doesNotThrow(
() => {
logger.info.apply(logger, test.args);
},
Error,
`Werelogs should not throw with ${test.desc}`,
);
assert(dummyLogger.ops[0][0], 'fatal',
'Expected the Module Logger to have logged a fatal message.');
done();
};
}
module.exports = {
DummyLogger,
genericFilterGenerator,
loggingMisuseGenerator,
};

View File

@ -1,118 +0,0 @@
// eslint-disable-line strict
const assert = require('assert');
const { PassThrough } = require('stream');
const pass = new PassThrough();
const werelogs = require('werelogs'); // eslint-disable-line
// With PassThrough, SimpleLogger can use it as Writeable stream and all the
// data being written can be read into a variable
const logBuffer = {
records: [],
};
pass.on('data', data => {
logBuffer.records.push(data.toString());
});
werelogs.configure({
level: 'info',
dump: 'error',
streams: [{
stream: pass,
type: 'raw',
}],
});
function createModuleLogger() {
return new werelogs.Logger('FT-test');
}
function checkFields(fields) {
const record = JSON.parse(logBuffer.records[0].trim());
Object.keys(fields).forEach(k => {
if (Object.prototype.hasOwnProperty.call(fields, k)) {
assert.deepStrictEqual(record[k], fields[k]);
}
});
}
function parseLogEntry() {
return JSON.parse(logBuffer.records[0].trim());
}
describe('Werelogs is usable as a dependency', () => {
describe('Usage of the ModuleLogger', () => {
afterEach(() => {
logBuffer.records = [];
});
it('Should be able to create a logger', done => {
assert.doesNotThrow(
createModuleLogger,
Error,
'Werelogs threw an exception trying to create a ModuleLogger.',
);
done();
});
it('Should be able to log a simple message', done => {
const logger = createModuleLogger();
const msg = 'This is a simple message';
logger.info(msg);
assert.strictEqual(parseLogEntry().message, msg);
done();
});
it('Should be able to log a message and additional fields', done => {
const logger = createModuleLogger();
const msg = 'This is a message with added fields';
const fields = {
errorCode: 9,
description: 'TestError',
options: { dump: false },
};
logger.info(msg, fields);
assert.strictEqual(parseLogEntry().message, msg);
checkFields(fields);
done();
});
});
describe('Usage of the RequestLogger', () => {
afterEach(() => {
logBuffer.records = [];
});
it('Should be able to create a logger', done => {
assert.doesNotThrow(
() => createModuleLogger().newRequestLogger(),
Error,
'Werelogs threw an exception trying to create a ModuleLogger.',
);
done();
});
it('Should be able to log a simple message', done => {
const logger = createModuleLogger().newRequestLogger();
const msg = 'This is a simple message';
logger.info(msg);
assert.strictEqual(parseLogEntry().message, msg);
done();
});
it('Should be able to log a message and additional fields', done => {
const logger = createModuleLogger().newRequestLogger();
const msg = 'This is a message with added fields';
const fields = {
errorCode: 9,
description: 'TestError',
options: { dump: false },
};
logger.info(msg, fields);
assert.strictEqual(parseLogEntry().message, msg);
checkFields(fields);
done();
});
});
});

View File

@ -1,57 +0,0 @@
const assert = require('assert');
const { PassThrough } = require('stream');
const Werelogs = require('werelogs'); // eslint-disable-line
const modules = [
require('./module1.js'),
require('./module2.js'),
require('./module3.js'),
];
const pass = new PassThrough();
const logBuffer = {
records: [],
};
pass.on('data', data => {
logBuffer.records.push(JSON.parse(data.toString().trim()));
});
describe('Config is shared and unique within one API', () => {
it('should find all log entries in the RingBuffer with the right '
+ 'module name', done => {
Werelogs.configure({
level: 'debug',
dump: 'fatal',
streams: [{
type: 'raw',
stream: pass,
}],
});
const log = new Werelogs.Logger('test-index');
modules.forEach(mod => { mod(); });
log.warn('Logging as warn');
const rLog = log.newRequestLogger();
rLog.info('Logging request as info');
/* eslint-disable max-len */
assert.deepStrictEqual(logBuffer.records.length, 5, 'Expected to see 5 log entries in the ring buffer.');
assert.deepStrictEqual(logBuffer.records[0].message, 'Logging as info');
assert.deepStrictEqual(logBuffer.records[0].name, 'test-mod1');
assert.deepStrictEqual(logBuffer.records[0].level, 'info');
assert.deepStrictEqual(logBuffer.records[1].message, 'Logging as debug');
assert.deepStrictEqual(logBuffer.records[1].name, 'test-mod2');
assert.deepStrictEqual(logBuffer.records[1].level, 'debug');
assert.deepStrictEqual(logBuffer.records[2].message, 'Logging as error');
assert.deepStrictEqual(logBuffer.records[2].name, 'test-mod3');
assert.deepStrictEqual(logBuffer.records[2].level, 'error');
assert.deepStrictEqual(logBuffer.records[3].message, 'Logging as warn');
assert.deepStrictEqual(logBuffer.records[3].name, 'test-index');
assert.deepStrictEqual(logBuffer.records[3].level, 'warn');
assert.deepStrictEqual(logBuffer.records[4].message, 'Logging request as info');
assert.deepStrictEqual(logBuffer.records[4].name, 'test-index');
assert.deepStrictEqual(logBuffer.records[4].level, 'info');
assert.notStrictEqual(logBuffer.records[4].req_id, undefined);
/* eslint-enable max-len */
done();
});
});

View File

@ -1,9 +0,0 @@
const Werelogs = require('werelogs').Logger; // eslint-disable-line
const log = new Werelogs('test-mod1');
function test() {
log.info('Logging as info');
}
module.exports = test;

View File

@ -1,9 +0,0 @@
const Werelogs = require('werelogs').Logger; // eslint-disable-line
const log = new Werelogs('test-mod2');
function test() {
log.debug('Logging as debug');
}
module.exports = test;

View File

@ -1,9 +0,0 @@
const Werelogs = require('werelogs').Logger; // eslint-disable-line
const log = new Werelogs('test-mod3');
function test() {
log.error('Logging as error');
}
module.exports = test;

View File

@ -1,58 +0,0 @@
/* eslint-disable max-len */
const assert = require('assert');
const Config = require('../../lib/Config.js');
describe('Config', () => {
const config = new Config();
beforeEach(() => {
config.reset();
});
it('should work with default configuration', done => {
assert.doesNotThrow(
() => {
config.logger.info('test message');
},
Error,
);
done();
});
it('log level should be updateable', done => {
config.update({ level: 'debug' });
assert.strictEqual(config.level, 'debug', 'Expected config\'s log level to be updated.');
done();
});
it('dump threshold should be updateable', done => {
const origDump = config.dump;
assert.notStrictEqual(origDump, 'warn', 'Expected original config.dump to differ from value to update.');
config.update({ dump: 'warn' });
assert.strictEqual(config.dump, 'warn', 'Expected config\'s dump threshold to be updated.');
done();
});
it('end logging level should be updateable', done => {
const origEnd = config.end;
assert.notStrictEqual(origEnd, 'trace', 'Expected original config.end to differ from value to update.');
config.update({ end: 'trace' });
assert.strictEqual(config.end, 'trace', 'Expected config\'s end log level to be updated.');
done();
});
it('should not be modified by an empty config object', done => {
const origLevel = config.level;
const origDump = config.dump;
const origLogger = config.logger;
const origStreams = config.streams;
config.update({});
assert.deepStrictEqual(origLevel, config.level, 'Expected logging level not to have changed.');
assert.deepStrictEqual(origDump, config.dump, 'Expected dump threshold not to have changed.');
assert.strictEqual(origLogger, config.logger, 'Expected logger not to have changed.');
assert.deepStrictEqual(origStreams, config.streams, 'Expected streams not to have changed.');
done();
});
});

View File

@ -1,9 +1,6 @@
import assert from 'assert';
// eslint-disable-line strict import * as LogLevel from '../../lib/LogLevel.js';
const assert = require('assert');
const LogLevel = require('../../lib/LogLevel.js');
function generateValidThrowTest(level) { function generateValidThrowTest(level) {
return function validTest(done) { return function validTest(done) {
@ -13,97 +10,89 @@ function generateValidThrowTest(level) {
}, },
Error, Error,
'Expected level to be valid and ' 'Expected level to be valid and '
+ 'the function not to throw an Error.', + 'the function not to throw an Error.');
);
done(); done();
}; };
} }
describe('LogLevel', () => { describe('LogLevel', () => {
describe('throwIfInvalid(level)', () => { describe('throwIfInvalid(level)', () => {
it('should throw on invalid string', done => { it('should throw on invalid string', (done) => {
assert.throws( assert.throws(
() => { () => {
LogLevel.throwIfInvalid('invalid'); LogLevel.throwIfInvalid('invalid');
}, },
RangeError, RangeError,
'Expected function to throw an Error instance due to ' 'Expected function to throw an Error instance due to '
+ 'invalid log level.', + 'invalid log level.');
);
done(); done();
}); });
it('should not throw on "trace" level', it('should not throw on "trace" level',
generateValidThrowTest('trace')); generateValidThrowTest('trace'));
it('should not throw on "debug" level', it('should not throw on "debug" level',
generateValidThrowTest('debug')); generateValidThrowTest('debug'));
it('should not throw on "info" level', it('should not throw on "info" level',
generateValidThrowTest('info')); generateValidThrowTest('info'));
it('should not throw on "warn" level', it('should not throw on "warn" level',
generateValidThrowTest('warn')); generateValidThrowTest('warn'));
it('should not throw on "error" level', it('should not throw on "error" level',
generateValidThrowTest('error')); generateValidThrowTest('error'));
it('should not throw on "fatal" level', it('should not throw on "fatal" level',
generateValidThrowTest('fatal')); generateValidThrowTest('fatal'));
}); });
describe('shouldLog(level, floor)', () => { describe('shouldLog(level, floor)', () => {
it('should return true on "trace" parameters', done => { it('should return true on "trace" parameters', (done) => {
assert.strictEqual( assert.strictEqual(
LogLevel.shouldLog('trace', 'trace'), LogLevel.shouldLog('trace', 'trace'),
true, true,
'Expected trace floor to allow logging trace level.', 'Expected trace floor to allow logging trace level.');
);
done(); done();
}); });
it('should return true on "debug" parameters', done => { it('should return true on "debug" parameters', (done) => {
assert.strictEqual( assert.strictEqual(
LogLevel.shouldLog('debug', 'debug'), LogLevel.shouldLog('debug', 'debug'),
true, true,
'Expected debug floor to allow logging debug level.', 'Expected debug floor to allow logging debug level.');
);
done(); done();
}); });
it('should return true on "info" parameters', done => { it('should return true on "info" parameters', (done) => {
assert.strictEqual( assert.strictEqual(
LogLevel.shouldLog('info', 'info'), LogLevel.shouldLog('info', 'info'),
true, true,
'Expected info floor to allow logging info level.', 'Expected info floor to allow logging info level.');
);
done(); done();
}); });
it('should return true on "warn" parameters', done => { it('should return true on "warn" parameters', (done) => {
assert.strictEqual( assert.strictEqual(
LogLevel.shouldLog('warn', 'warn'), LogLevel.shouldLog('warn', 'warn'),
true, true,
'Expected warn floor to allow logging warn level.', 'Expected warn floor to allow logging warn level.');
);
done(); done();
}); });
it('should return true on "error" parameters', done => { it('should return true on "error" parameters', (done) => {
assert.strictEqual( assert.strictEqual(
LogLevel.shouldLog('error', 'error'), LogLevel.shouldLog('error', 'error'),
true, true,
'Expected error floor to allow logging error level.', 'Expected error floor to allow logging error level.');
);
done(); done();
}); });
it('should return true on "fatal" parameters', done => { it('should return true on "fatal" parameters', (done) => {
assert.strictEqual( assert.strictEqual(
LogLevel.shouldLog('fatal', 'fatal'), LogLevel.shouldLog('fatal', 'fatal'),
true, true,
'Expected fatal floor to allow logging fatal level.', 'Expected fatal floor to allow logging fatal level.');
);
done(); done();
}); });
}); });

View File

@ -1,160 +1,144 @@
import assert from 'assert';
// eslint-disable-line strict import { default as genericFilterGenerator } from '../Utils.js';
const assert = require('assert'); import RequestLogger from '../../lib/RequestLogger.js';
import Logger from '../../lib/Logger.js';
const { genericFilterGenerator, loggingMisuseGenerator, DummyLogger } = require('../Utils'); /**
const Config = require('../../lib/Config.js');
const RequestLogger = require('../../lib/RequestLogger.js');
const Logger = require('../../lib/Logger.js');
const config = new Config();
/*
* This function is a thunk-function calling the Utils' filterGenerator with * This function is a thunk-function calling the Utils' filterGenerator with
* the right createLogger function, while seemlessly passing through its * the right createLogger function, while seemlessly passing through its
* arguments. * arguments.
*/ */
function filterGenerator(logLevel, callLevel) { function filterGenerator(...params) {
function createModuleLogger(dummyLogger, filterLevel) { function createModuleLogger(dummyLogger, filterLevel) {
const logger = new Logger('TestModuleLogger',
{
level: filterLevel,
dump: 'fatal',
});
/* /*
* Here, patch the config by setting a specifically designed dummyLogger * Here, patch the logger by setting a specificly designed dummyLogger
* for testing purposes that will help us collect runtime data. * for testing purposes that will help us collect runtime data.
*/ */
const testConfig = new Config({ level: filterLevel, dump: 'fatal' }); logger.bLogger = dummyLogger;
testConfig.simpleLogger = dummyLogger;
return new Logger(testConfig, 'TestModuleLogger'); return logger;
} }
return genericFilterGenerator(logLevel, callLevel, createModuleLogger); /*
* Array-ify the arguments object, and append the specificly-added argument
* to it.
*/
const args = Array.prototype.splice.apply(params, [0]);
args.push(createModuleLogger);
return genericFilterGenerator.apply({}, args);
} }
function checkFields(src, result) { describe('WereLogs Logger is usable:', () => {
Object.keys(src).forEach(k => { it('Can be instanciated with only a name', (done) => {
if (Object.prototype.hasOwnProperty.call(src, k)) { assert.doesNotThrow(
assert.deepStrictEqual(result[k], src[k]); () => {
} return new Logger('WereLogsTest');
}); },
assert.ok(Object.prototype.hasOwnProperty.call(result, 'time')); Error,
// Time field should be current give or take 1s 'WereLogs Instanciation should not throw any kind of error.');
assert.ok((Date.now() - result.time) < 1000);
}
describe('Logger is usable:', () => {
beforeEach(() => {
config.reset();
});
it('Cannot be instanciated without parameters', done => {
assert.throws(
() => new Logger(),
TypeError,
'Logger Instanciation should not succeed without parameter.',
);
done(); done();
}); });
it('Cannot be instanciated with only a config', done => { it('Cannot be instanciated with invalid log level', (done) => {
assert.throws( assert.throws(
() => new Logger(config), () => {
TypeError, return new Logger('test', {level: 'invalidlevel'});
'Logger Instanciation should not be succeed without a name.', },
); RangeError,
'WereLogs should not be instanciable without the proper logging levels.');
done(); done();
}); });
it('Cannot be instanciated with a bad config type', done => { it('Cannot be instanciated with invalid dump threshold level', (done) => {
assert.throws( assert.throws(
() => new Logger({ level: 'info' }, 'WereLogsTest'), () => {
TypeError, return new Logger('test', {level: 'trace', dump: 'invalidlevel'});
'Logger Instanciation should not succeed with a bad config type.', },
); RangeError,
'WereLogs should not be instanciable without the proper dumping threshold levels.');
done(); done();
}); });
it('Cannot be instanciated with only a name', done => { it('Cannot be instanciated with a non-Array in config.streams', (done) => {
assert.throws( assert.throws(
() => new Logger('WereLogsTest'), () => {
TypeError, return new Logger('test', {streams: process.stdout});
'Logger Instanciation should not succeed with only a name.', },
); Error,
'Werelogs should not be instanciable with a stream option that is not an array.');
done(); done();
}); });
it('Can create Per-Request Loggers', done => { it('Cannot be instanciated with an empty Array in config.streams', (done) => {
const logger = new Logger(config, 'test'); assert.throws(
() => {
return new Logger('test', {streams: []});
},
Error,
'Werelogs should not be instanciable with an empty array for the streams option.');
done();
});
it('Cannot set logging level to invalid level at runtime', (done) => {
const logger = new Logger('test');
assert.throws(
() => {
logger.setLevel('invalidLevel');
},
RangeError,
'WereLogs should not be able to set log level to an invalid level.');
done();
});
it('Can set logging level at runtime', (done) => {
const logger = new Logger('test');
assert.doesNotThrow(
() => {
logger.setLevel('fatal');
},
RangeError,
'WereLogs should be able to set log level at runtime.');
done();
});
it('Can create Per-Request Loggers', (done) => {
const logger = new Logger('test');
assert.doesNotThrow( assert.doesNotThrow(
() => { () => {
logger.newRequestLogger(); logger.newRequestLogger();
}, },
Error, Error,
'Werelogs should not throw when creating a request logger.', 'Werelogs should not throw when creating a request logger.');
); const reqLogger = logger.newRequestLogger();
done();
});
it('Can create Per-Request Loggers from a Serialized UID Array', done => {
const logger = new Logger(config, 'test');
assert.doesNotThrow(
() => {
logger.newRequestLogger();
},
Error,
// eslint-disable-next-line max-len
'Werelogs should not throw when creating a request logger from a Serialized UID Array.',
);
const reqLogger = logger.newRequestLoggerFromSerializedUids(
'OneUID:SecondUID:TestUID:YouWinUID',
);
assert(reqLogger instanceof RequestLogger, 'RequestLogger'); assert(reqLogger instanceof RequestLogger, 'RequestLogger');
assert.deepStrictEqual(reqLogger.getUids().slice(0, -1),
['OneUID', 'SecondUID', 'TestUID', 'YouWinUID']);
done(); done();
}); });
it('Uses the additional fields as expected', done => { it('Can create Per-Request Loggers from a Serialized UID Array', (done) => {
const dummyLogger = new DummyLogger(); const logger = new Logger('test');
config.simpleLogger = dummyLogger; assert.doesNotThrow(
const logger = new Logger(config, 'test'); () => {
const fields = { logger.newRequestLogger();
ip: '127.0.0.1', },
method: 'GET', Error,
count: 23, 'Werelogs should not throw when creating a request logger from a Serialized UID Array.');
}; const reqLogger = logger.newRequestLoggerFromSerializedUids('OneUID:SecondUID:TestUID:YouWinUID');
logger.info('message', fields); assert(reqLogger instanceof RequestLogger, 'RequestLogger');
checkFields(fields, dummyLogger.ops[0][1][0]); assert.deepStrictEqual(reqLogger.getUids().slice(0, -1), ['OneUID', 'SecondUID', 'TestUID', 'YouWinUID']);
assert.strictEqual(dummyLogger.ops[0][1][1], 'message');
done(); done();
}); });
/* eslint-disable max-len */
describe('Does not crash and logs a fatal message when mis-using its logging API', () => {
const testValues = [
{ desc: 'a string as second argument', args: ['test', 'second-param-string'] },
{ desc: 'a function as second argument', args: ['test', () => { }] }, // eslint-disable-line arrow-body-style
{ desc: 'a Number as second argument', args: ['test', 1] },
{ desc: 'more than 2 arguments', args: ['test', 2, 3, 4] },
];
/* eslint-enable max-len */
function createMisusableLogger(dummyLogger) {
config.simpleLogger = dummyLogger;
const logger = new Logger(config, 'test');
return logger;
}
for (let i = 0; i < testValues.length; ++i) {
const test = testValues[i];
it(`Does not crash with ${test.desc}`,
loggingMisuseGenerator(test, createMisusableLogger));
}
});
}); });
/* eslint-disable no-multi-spaces, max-len */ describe('Werelogs Module-level Logger can log as specified by the log level', () => {
describe('Logger can log as specified by the log level', () => {
it('Trace level does not filter trace level out', filterGenerator('trace', 'trace')); it('Trace level does not filter trace level out', filterGenerator('trace', 'trace'));
it('Trace level does not filter debug level out', filterGenerator('trace', 'debug')); it('Trace level does not filter debug level out', filterGenerator('trace', 'debug'));
it('Trace level does not filter info level out', filterGenerator('trace', 'info')); it('Trace level does not filter info level out', filterGenerator('trace', 'info'));
@ -197,4 +181,3 @@ describe('Logger can log as specified by the log level', () => {
it('Fatal level filters error level out', filterGenerator('fatal', 'error')); it('Fatal level filters error level out', filterGenerator('fatal', 'error'));
it('Fatal level does not filter fatal level out', filterGenerator('fatal', 'fatal')); it('Fatal level does not filter fatal level out', filterGenerator('fatal', 'fatal'));
}); });
/* eslint-enable no-multi-spaces, max-len */

View File

@ -1,41 +1,42 @@
import assert from 'assert';
// eslint-disable-line strict import { default as genericFilterGenerator, DummyLogger } from '../Utils.js';
const assert = require('assert'); import RequestLogger from '../../lib/RequestLogger.js';
const { DummyLogger, genericFilterGenerator, loggingMisuseGenerator } = require('../Utils.js'); /**
const RequestLogger = require('../../lib/RequestLogger.js');
/*
* This function is a thunk-function calling the Utils' filterGenerator with * This function is a thunk-function calling the Utils' filterGenerator with
* the right createLogger function, while seemlessly passing through its * the right createLogger function, while seemlessly passing through its
* arguments. * arguments.
*/ */
function filterGenerator(logLevel, callLevel) { function filterGenerator(...params) {
function createRequestLogger(dummyLogger, filterLevel) { function createRequestLogger(dummyLogger, filterLevel) {
return new RequestLogger(dummyLogger, filterLevel, 'fatal', 'info'); return new RequestLogger(dummyLogger, filterLevel, 'fatal');
} }
return genericFilterGenerator(logLevel, callLevel, createRequestLogger); /*
* Array-ify the arguments object, and append the specificly-added argument
* to it.
*/
const args = Array.prototype.splice.apply(params, [0]);
args.push(createRequestLogger);
return genericFilterGenerator.apply({}, args);
} }
function runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts, function runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts, done) {
done) {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'trace', 'error', 'info'); const reqLogger = new RequestLogger(dummyLogger, 'trace', 'error');
commandHistory.every((val, index) => { commandHistory.every(function doLogWithLevel(val, index) {
switch (val) { switch (val) {
/* eslint-disable no-multi-spaces */
case 'trace': reqLogger.trace(index); break; case 'trace': reqLogger.trace(index); break;
case 'debug': reqLogger.debug(index); break; case 'debug': reqLogger.debug(index); break;
case 'info': reqLogger.info(index); break; case 'info': reqLogger.info(index); break;
case 'warn': reqLogger.warn(index); break; case 'warn': reqLogger.warn(index); break;
case 'error': reqLogger.error(index); break; case 'error': reqLogger.error(index); break;
case 'fatal': reqLogger.fatal(index); break; case 'fatal': reqLogger.fatal(index); break;
/* eslint-enable no-multi-spaces */
default: default:
done(new Error('Unexpected logging level name: ', val)); done(new Error('Unexpected logging level name: ', val));
} }
@ -43,90 +44,92 @@ function runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts,
}); });
expectedHistory.every((val, index) => { expectedHistory.every((val, index) => {
assert.strictEqual(dummyLogger.ops[index][0], val[0], assert.strictEqual(dummyLogger.ops[index][0], val[0], 'Expected log entry levels to match.');
'Expected log entry levels to match.'); assert.strictEqual(dummyLogger.ops[index][1][1][0], val[1], 'Expected log entry values to match.');
assert.strictEqual(dummyLogger.ops[index][1][1], val[1],
'Expected log entry values to match.');
return true; return true;
}); });
assert.deepEqual(dummyLogger.counts, expectedCounts); assert.deepEqual(dummyLogger.counts, expectedCounts);
} }
/* eslint-disable no-multi-spaces, max-len */
describe('RequestLogger', () => { describe('RequestLogger', () => {
describe('Object Instanciation', () => { describe('Object Instanciation', () => {
describe('Logging Levels Initialization', () => { describe('Logging Levels Initialization', () => {
it('Throws if LogLevel is higher than dumpThreshold', done => { it('Throws if LogLevel is higher than dumpThreshold', (done) => {
assert.throws( assert.throws(
() => new RequestLogger(undefined, 'fatal', 'debug', 'info'), () => {
return new RequestLogger(undefined, 'fatal', 'debug');
},
Error, Error,
'Dump level "debug" should not be valid with logging level "fatal".', 'Dump level "debug" should not be valid with logging level "fatal".');
);
done(); done();
}); });
it('Works with LogLevel lesser or equal to DumpLevel', done => { it('Works with LogLevel lesser or equal to DumpLevel', (done) => {
assert.doesNotThrow( assert.doesNotThrow(
() => new RequestLogger(undefined, 'debug', 'fatal', 'info'), () => {
return new RequestLogger(undefined, 'debug', 'fatal');
},
Error, Error,
'Dump level "fatal" should be valid with logging level "debug".', 'Dump level "fatal" should be valid with logging level "debug".');
);
done(); done();
}); });
}); });
describe('UID Initialization', () => { describe('UID Initialization', () => {
it('defines an UID when none provided', done => { it('defines an UID when none provided', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'debug', 'fatal', 'info'); const reqLogger = new RequestLogger(dummyLogger, 'debug', 'fatal');
assert.strictEqual(Array.isArray(reqLogger.uids), true, 'Expected uid list to be an Array.'); assert.strictEqual(Array.isArray(reqLogger.uids), true, 'Expected uid list to be an Array.');
assert.strictEqual(reqLogger.uids.length, 1, 'Expected uid list to contain one element.'); assert.strictEqual(reqLogger.uids.length, 1, 'Expected uid list to contain one element.');
done(); done();
}); });
it('creates an UID array out of the provided UID string', done => { it('creates an UID array out of the provided UID string', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const uids = 'BasicUid'; const uids = 'BasicUid';
const reqLogger = new RequestLogger(dummyLogger, 'debug', 'fatal', 'info', uids); const reqLogger = new RequestLogger(dummyLogger, 'debug', 'fatal', uids);
assert.strictEqual(Array.isArray(reqLogger.uids), true, 'Expected uid list to be an Array.'); assert.strictEqual(Array.isArray(reqLogger.uids), true, 'Expected uid list to be an Array.');
assert.strictEqual(reqLogger.uids.length, 1, 'Expected uid list to contain one element.'); assert.strictEqual(reqLogger.uids.length, 1, 'Expected uid list to contain one element.');
assert.strictEqual(reqLogger.uids[0], uids, 'Expected uid list to only contain the value given as argument.'); assert.strictEqual(reqLogger.uids[0], uids, 'Expected uid list to only contain the value given as argument.');
done(); done();
}); });
it('throws when UID string provided contains a colon', done => { it('throws when UID string provided contains a colon', (done) => {
assert.throws( assert.throws(
() => new RequestLogger(undefined, 'debug', 'fatal', 'info', 'pouet:tata'), () => {
return new RequestLogger(undefined, 'debug', 'fatal', 'pouet:tata');
},
Error, Error,
'UID string "pouet:tata" should be rejected by the RequestLogger constructor.', 'UID string "pouet:tata" should be rejected by the RequestLogger constructor.');
);
done(); done();
}); });
it('expands the UID array when one is provided', done => { it('expands the UID array when one is provided', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const uids = ['oneuid', 'twouid', 'threeuids']; const uids = ['oneuid', 'twouid', 'threeuids'];
const reqLogger = new RequestLogger(dummyLogger, 'debug', 'fatal', 'info', uids); const reqLogger = new RequestLogger(dummyLogger, 'debug', 'fatal', uids);
assert.strictEqual(Array.isArray(reqLogger.uids), true, 'Expected uid list to be an Array.'); assert.strictEqual(Array.isArray(reqLogger.uids), true, 'Expected uid list to be an Array.');
assert.strictEqual(reqLogger.uids.length, 4, 'Expected uid list to contain four elements.'); assert.strictEqual(reqLogger.uids.length, 4, 'Expected uid list to contain four elements.');
assert.strictEqual(uids.indexOf(reqLogger.uids[3]), -1, 'Expected the last uid of the list to be the new one.'); assert.strictEqual(uids.indexOf(reqLogger.uids[3]), -1, 'Expected the last uid of the list to be the new one.');
done(); done();
}); });
it('throws when UID string Array provided contains an UID that contains a colon', done => { it('throws when UID string Array provided contains an UID that contains a colon', (done) => {
assert.throws( assert.throws(
() => new RequestLogger(undefined, 'debug', 'fatal', 'info', ['OneUID', 'SecondUID', 'Test:DashUID']), () => {
return new RequestLogger(undefined, 'debug', 'fatal', ['OneUID', 'SecondUID', 'Test:DashUID']);
},
Error, Error,
'UID string "Test:DashUID" should be rejected by the RequestLogger constructor.', 'UID string "Test:DashUID" should be rejected by the RequestLogger constructor.');
);
done(); done();
}); });
}); });
describe('getUids() method', () => { describe('getUids() method', () => {
it('retrieves a list of string UID', done => { it('retrieves a list of string UID', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', 'info'); const reqLogger = new RequestLogger(dummyLogger, 'info', 'error');
const uidlist = reqLogger.getUids(); const uidlist = reqLogger.getUids();
assert.strictEqual(Array.isArray(uidlist), true, 'Expected UID List to be an Array'); assert.strictEqual(Array.isArray(uidlist), true, 'Expected UID List to be an Array');
assert.strictEqual(typeof uidlist[0], 'string', 'Expected UID items to be strings'); assert.strictEqual(typeof uidlist[0], 'string', 'Expected UID items to be strings');
@ -134,28 +137,28 @@ describe('RequestLogger', () => {
}); });
describe('Length of the UIDs array', () => { describe('Length of the UIDs array', () => {
it('default constructor yields a one-item UID list', done => { it('default constructor yields a one-item UID list', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', 'info'); const reqLogger = new RequestLogger(dummyLogger, 'info', 'error');
const uidlist = reqLogger.getUids(); const uidlist = reqLogger.getUids();
assert.strictEqual(uidlist.length, 1, 'Expected only one item in UID Array'); assert.strictEqual(uidlist.length, 1, 'Expected only one item in UID Array');
done(); done();
}); });
it('manually-set UID constructor yields a one-item UID list', done => { it('manually-set UID constructor yields a one-item UID list', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const myUid = 'ThisIsMyUid'; const myUid = 'ThisIsMyUid';
const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', 'info', myUid); const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', myUid);
const uidlist = reqLogger.getUids(); const uidlist = reqLogger.getUids();
assert.strictEqual(uidlist.length, 1, 'Expected only one item in UID Array'); assert.strictEqual(uidlist.length, 1, 'Expected only one item in UID Array');
assert.strictEqual(uidlist[0], myUid, 'Expected UID to match what was used to set it.'); assert.strictEqual(uidlist[0], myUid, 'Expected UID to match what was used to set it.');
done(); done();
}); });
it('manually-set parent UID List constructor yields a n+1 item UID list', done => { it('manually-set parent UID List constructor yields a n+1 item UID list', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const myParentUidList = ['ThisIsMyOriginUid', 'ThisIsMySecondGenUid', 'ThisIsMyThirdGenUid']; const myParentUidList = [ 'ThisIsMyOriginUid', 'ThisIsMySecondGenUid', 'ThisIsMyThirdGenUid' ];
const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', 'info', myParentUidList); const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', myParentUidList);
const uidlist = reqLogger.getUids(); const uidlist = reqLogger.getUids();
assert.strictEqual(uidlist.length, myParentUidList.length + 1, 'Expected n+1 item in UID Array compared to set UID List array'); assert.strictEqual(uidlist.length, myParentUidList.length + 1, 'Expected n+1 item in UID Array compared to set UID List array');
assert.deepStrictEqual(uidlist.slice(0, -1), myParentUidList, 'Expected UID list[:-1] to match what was used to set it.'); assert.deepStrictEqual(uidlist.slice(0, -1), myParentUidList, 'Expected UID list[:-1] to match what was used to set it.');
@ -163,9 +166,9 @@ describe('RequestLogger', () => {
}); });
}); });
it('internal data cannot be set through returned UID List', done => { it('internal data cannot be set through returned UID List', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', 'info'); const reqLogger = new RequestLogger(dummyLogger, 'info', 'error');
const uidlist = reqLogger.getUids(); const uidlist = reqLogger.getUids();
uidlist.push('Test'); uidlist.push('Test');
assert.notStrictEqual(uidlist.length, reqLogger.getUids().length, 'Expected different number of items in internals and local variable.'); assert.notStrictEqual(uidlist.length, reqLogger.getUids().length, 'Expected different number of items in internals and local variable.');
@ -174,35 +177,17 @@ describe('RequestLogger', () => {
}); });
describe('getSerializedUids()', () => { describe('getSerializedUids()', () => {
it('Should return a properly serialized UID Array', done => { it('Should return a properly serialized UID Array', (done) => {
const dummyLogger = new DummyLogger(); const dummyLogger = new DummyLogger();
const uidList = ['FirstUID', 'SecondUID', 'ThirdUID', 'TestUID']; const uidList = [ 'FirstUID', 'SecondUID', 'ThirdUID', 'TestUID' ];
const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', 'info', uidList); const reqLogger = new RequestLogger(dummyLogger, 'info', 'error', uidList);
const expectedString = `FirstUID:SecondUID:ThirdUID:TestUID:${reqLogger.getUids()[4]}`; const expectedString = 'FirstUID:SecondUID:ThirdUID:TestUID:' + reqLogger.getUids()[4];
assert.strictEqual(reqLogger.getSerializedUids(), expectedString, 'Expected serialized UID List to match expected data.'); assert.strictEqual(reqLogger.getSerializedUids(), expectedString, 'Expected serialized UID List to match expected data.');
done(); done();
}); });
}); });
}); });
describe('Does not crash when mis-using its logging API', () => {
const testValues = [
{ desc: 'a string as second argument', args: ['test', 'second-param-string'] },
{ desc: 'a function as second argument', args: ['test', function f() { }] },
{ desc: 'a Number as second argument', args: ['test', 1] },
{ desc: 'more than 2 arguments', args: ['test', 2, 3, 4] },
];
function createMisusableRequestLogger(dummyLogger) {
return new RequestLogger(dummyLogger, 'info', 'error', 'info');
}
for (let i = 0; i < testValues.length; ++i) {
const test = testValues[i];
it(`Does not crash with ${test.desc}`,
loggingMisuseGenerator(test, createMisusableRequestLogger));
}
});
describe('Logging level dump filtering', () => { describe('Logging level dump filtering', () => {
it('Trace level does not filter trace level out', filterGenerator('trace', 'trace')); it('Trace level does not filter trace level out', filterGenerator('trace', 'trace'));
it('Trace level does not filter debug level out', filterGenerator('trace', 'debug')); it('Trace level does not filter debug level out', filterGenerator('trace', 'debug'));
@ -246,249 +231,38 @@ describe('RequestLogger', () => {
it('Fatal level filters error level out', filterGenerator('fatal', 'error')); it('Fatal level filters error level out', filterGenerator('fatal', 'error'));
it('Fatal level does not filter fatal level out', filterGenerator('fatal', 'fatal')); it('Fatal level does not filter fatal level out', filterGenerator('fatal', 'fatal'));
}); });
/* eslint-enable no-multi-spaces, max-len */
describe('Logging API regression testing', () => {
it('Should not alter the input fields when not actually logging',
done => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
const refFields = { hits: 45, count: 32 };
const usedFields = { ...refFields };
reqLogger.debug('test', usedFields);
assert.deepStrictEqual(usedFields, refFields);
done();
});
it('Should not alter the input fields when actually logging',
done => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
const refFields = { hits: 45, count: 32 };
const usedFields = { ...refFields };
reqLogger.info('test', usedFields);
assert.deepStrictEqual(usedFields, refFields);
done();
});
it('Should not alter the input fields when dumping', done => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
const refFields = { hits: 45, count: 32 };
const usedFields = { ...refFields };
reqLogger.error('test', usedFields);
assert.deepStrictEqual(usedFields, refFields);
done();
});
});
describe('Default Fields', () => {
it('should not modify the object passed as a parameter', done => {
const add1 = {
attr1: 0,
};
const add2 = {
attr2: 'string',
};
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
reqLogger.addDefaultFields(add1);
reqLogger.addDefaultFields(add2);
assert.deepStrictEqual(add1, { attr1: 0 });
assert.deepStrictEqual(add2, { attr2: 'string' });
done();
});
it('should add one added default field to the log entries', done => {
const clientInfo = {
clientIP: '127.0.0.1',
};
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
reqLogger.addDefaultFields(clientInfo);
reqLogger.info('test message');
assert.strictEqual(clientInfo.clientIP,
dummyLogger.ops[0][1][0].clientIP);
done();
});
it('should add multiple added default fields to the log entries',
done => {
const clientInfo = {
clientIP: '127.0.0.1',
clientPort: '1337',
};
const requestInfo = {
object: '/tata/self.txt',
creator: 'Joddy',
};
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
reqLogger.addDefaultFields(clientInfo);
reqLogger.addDefaultFields(requestInfo);
reqLogger.info('test message');
assert.strictEqual(clientInfo.clientIP,
dummyLogger.ops[0][1][0].clientIP);
assert.strictEqual(clientInfo.clientPort,
dummyLogger.ops[0][1][0].clientPort);
assert.strictEqual(requestInfo.object,
dummyLogger.ops[0][1][0].object);
assert.strictEqual(requestInfo.creator,
dummyLogger.ops[0][1][0].creator);
done();
});
});
describe('Automatic Elapsed Time computation', () => {
describe('Deprecated API:', () => {
it('should include an "elapsed_ms" field in the last log entry',
done => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
reqLogger.end('Last message');
assert.strictEqual(dummyLogger.ops[0][1][1], 'Last message');
assert.notStrictEqual(dummyLogger.ops[0][1][0].elapsed_ms,
undefined);
assert.strictEqual(typeof dummyLogger.ops[0][1][0]
.elapsed_ms, 'number');
done();
});
// eslint-disable-next-line max-len
it('should include an "elapsed_ms" field in the last log entry and be error level', () => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger,
'info', 'fatal', 'info');
reqLogger.errorEnd('Last message failed');
assert.strictEqual(dummyLogger.ops[0][1][1],
'Last message failed');
assert.notStrictEqual(dummyLogger.ops[0][1][0].elapsed_ms,
undefined);
assert.strictEqual(typeof dummyLogger.ops[0][1][0].elapsed_ms,
'number');
assert.strictEqual(dummyLogger.ops[0][0], 'error');
});
});
const endLogging = {
trace: endLogger => endLogger.trace.bind(endLogger),
debug: endLogger => endLogger.debug.bind(endLogger),
info: endLogger => endLogger.info.bind(endLogger),
warn: endLogger => endLogger.warn.bind(endLogger),
error: endLogger => endLogger.error.bind(endLogger),
fatal: endLogger => endLogger.fatal.bind(endLogger),
};
/* eslint-disable max-len */
Object.keys(endLogging).forEach(level => {
it(`should include an "elapsed_ms" field in the last log entry with level ${level}`, done => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'trace', 'fatal');
endLogging[level](reqLogger.end())('Last message');
assert.strictEqual(dummyLogger.ops[0][1][1], 'Last message');
assert.notStrictEqual(dummyLogger.ops[0][1][0].elapsed_ms, undefined);
assert.strictEqual(typeof dummyLogger.ops[0][1][0].elapsed_ms, 'number');
assert.strictEqual(dummyLogger.ops[0][0], level);
done();
});
});
/* eslint-enable max-len */
it('should be augmentable through addDefaultFields', done => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'trace', 'fatal');
reqLogger.end().addDefaultFields({ endFlag: true });
// Someone could do multiple operations in the meantime before
// end() logging
reqLogger.end().error('Test Augmented END', { endValue: 42 });
assert.strictEqual(dummyLogger.ops[0][1][1], 'Test Augmented END');
assert.strictEqual(typeof dummyLogger.ops[0][1][0].elapsed_ms,
'number');
assert.strictEqual(dummyLogger.ops[0][1][0].endFlag, true);
assert.strictEqual(dummyLogger.ops[0][1][0].endValue, 42);
done();
});
it('should log an error in addition to request logs when end() called more than once',
done => {
const dummyLogger = new DummyLogger();
const reqLogger = new RequestLogger(dummyLogger, 'trace', 'fatal');
reqLogger.end().info('after first call to end()');
reqLogger.end().debug('after second call to end()');
assert.strictEqual(dummyLogger.ops.length, 3);
assert.strictEqual(dummyLogger.ops[0][0], 'info');
assert.strictEqual(dummyLogger.ops[0][1][1], 'after first call to end()');
assert.strictEqual(dummyLogger.ops[1][0], 'error');
assert.strictEqual(dummyLogger.ops[2][0], 'debug');
assert.strictEqual(dummyLogger.ops[2][1][1], 'after second call to end()');
done();
});
});
describe('Log History dumped when logging floor level reached', () => { describe('Log History dumped when logging floor level reached', () => {
it('Dumping duplicates log entries', done => { it('Dumping duplicates log entries', (done) => {
const commandHistory = ['info', 'error']; const commandHistory = ['info', 'error'];
const expectedHistory = [['info', 0], ['info', 0], ['error', 1]]; const expectedHistory = [['info', 0], ['info', 0], ['error', 1]];
const expectedCounts = { const expectedCounts = { trace: 0, debug: 0, info: 2, warn: 0, error: 1, fatal: 0 };
trace: 0,
debug: 0,
info: 2,
warn: 0,
error: 1,
fatal: 0,
};
runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts, runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts, done);
done);
done(); done();
}); });
it('Dumping Keeps logging history order', done => { it('Dumping Keeps logging history order', (done) => {
const commandHistory = ['trace', 'info', 'debug', 'error']; const commandHistory = ['trace', 'info', 'debug', 'error'];
const expectedHistory = [['trace', 0], ['info', 1], ['debug', 2], const expectedHistory = [['trace', 0], ['info', 1], ['debug', 2], ['trace', 0], ['info', 1], ['debug', 2], ['error', 3]];
['trace', 0], ['info', 1], ['debug', 2], const expectedCounts = { trace: 2, debug: 2, info: 2, warn: 0, error: 1, fatal: 0 };
['error', 3]];
const expectedCounts = {
trace: 2,
debug: 2,
info: 2,
warn: 0,
error: 1,
fatal: 0,
};
runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts, runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts, done);
done);
done(); done();
}); });
it('Dumping multiple times does not re-dump already-dumped entries', it('Dumping multiple times does not re-dump already-dumped entries', (done) => {
done => { const commandHistory = ['trace', 'info', 'debug', 'error',
const commandHistory = ['trace', 'info', 'debug', 'error', 'warn', 'debug', 'fatal'];
'warn', 'debug', 'fatal']; const expectedHistory = [['trace', 0], ['info', 1], ['debug', 2],
const expectedHistory = [['trace', 0], ['info', 1], ['debug', 2], ['trace', 0], ['info', 1], ['debug', 2], ['error', 3],
['trace', 0], ['info', 1], ['debug', 2], ['warn', 4], ['debug', 5],
['error', 3], ['warn', 4], ['debug', 5], ['warn', 4], ['debug', 5], ['fatal', 6]];
['warn', 4], ['debug', 5], const expectedCounts = { trace: 2, debug: 4, info: 2, warn: 2, error: 1, fatal: 1 };
['fatal', 6]];
const expectedCounts = {
trace: 2,
debug: 4,
info: 2,
warn: 2,
error: 1,
fatal: 1,
};
runLoggingDumpTest(commandHistory, expectedHistory, runLoggingDumpTest(commandHistory, expectedHistory, expectedCounts, done);
expectedCounts, done); done();
done(); });
});
}); });
}); });

View File

@ -1,20 +1,16 @@
// eslint-disable-line strict
const assert = require('assert'); const assert = require('assert');
const Utils = require('../../lib/Utils.js');
const { const generateUid = Utils.generateUid;
generateUid, serializeUids, unserializeUids, objectCopy, const serializeUids = Utils.serializeUids;
} = require('../../lib/Utils'); const unserializeUids = Utils.unserializeUids;
describe('Utils: generateUid', () => { describe('Utils: generateUid', () => {
it('generates a string-typed ID', done => { it('generates a string-typed ID', (done) => {
const uid = generateUid(); const uid = generateUid();
assert.strictEqual(typeof uid, 'string', assert.strictEqual(typeof(uid), 'string', 'The generated ID is not a String (' + typeof(uid) + ')');
`The generated ID is not a String (${typeof uid})`);
done(); done();
}); });
it('generate roughly unique IDs', done => { it('generate roughly unique IDs', (done) => {
const generated = {}; const generated = {};
let count = 0; let count = 0;
for (let i = 0; i < 10000; ++i) { for (let i = 0; i < 10000; ++i) {
@ -22,77 +18,25 @@ describe('Utils: generateUid', () => {
count = generated[uid] ? generated[uid] + 1 : 1; count = generated[uid] ? generated[uid] + 1 : 1;
generated[uid] = count; generated[uid] = count;
} }
Object.keys(generated).every(uid => { Object.keys(generated).every((uid) => {
assert.strictEqual(generated[uid], 1, assert.strictEqual(generated[uid], 1, `Uid ${uid} was generated ${generated[uid]} times: It is not even remotely unique.`);
`Uid ${uid} was generated ${generated[uid]} `
+ 'times: It is not even remotely unique.');
return {};
}); });
done(); done();
}); });
}); });
describe('Utils: serializeUids', () => { describe('Utils: serializeUids', () => {
it('serializes to the expected string data', done => { it('serializes to the expected string data', (done) => {
const uidList = ['FirstUID', 'SecondUID', 'ThirdUID']; const uidList = [ 'FirstUID', 'SecondUID', 'ThirdUID'];
const serializedUIDs = serializeUids(uidList); const serializedUIDs = serializeUids(uidList);
assert.strictEqual(serializedUIDs, 'FirstUID:SecondUID:ThirdUID', assert.strictEqual(serializedUIDs, 'FirstUID:SecondUID:ThirdUID', 'Serialized UID List should match expected value.');
'Serialized UID List should match expected value.');
done(); done();
}); });
it('unserializes the expected number of UIDs', done => { it('unserializes the expected number of UIDs', (done) => {
const refUidList = ['FirstUID', 'SecondUID', 'ThirdUID']; const refUidList = [ 'FirstUID', 'SecondUID', 'ThirdUID'];
const unserializedUIDs = unserializeUids('FirstUID:SecondUID:ThirdUID'); const unserializedUIDs = unserializeUids('FirstUID:SecondUID:ThirdUID');
assert.deepStrictEqual(unserializedUIDs, refUidList, assert.deepStrictEqual(unserializedUIDs, refUidList, 'Unserialized UID List should match expected value.');
'Unserialized UID List should match expected value.');
done(); done();
}); });
}); });
describe('Utils: objectCopy', () => {
it('copies all the properties from source to target object', done => {
const target = { foo: 'bar' };
const source = { id: 1, name: 'demo', value: { a: 1, b: 2, c: 3 } };
const result = {
foo: 'bar',
id: 1,
name: 'demo',
value: { a: 1, b: 2, c: 3 },
};
objectCopy(target, source);
assert.deepStrictEqual(target, result,
'target should have the same properties as source');
done();
});
it('copies all the properties from multiple sources to target object',
done => {
const target = { foo: 'bar' };
const source1 = {
id: 1,
name: 'demo1',
value: { a: 1, b: 2, c: 3 },
};
// eslint-disable-next-line camelcase
const source2 = {
req_id: 2,
method: 'test',
err: { code: 'error', msg: 'test' },
};
const result = {
foo: 'bar',
id: 1,
name: 'demo1',
value: { a: 1, b: 2, c: 3 },
// eslint-disable-next-line camelcase
req_id: 2,
method: 'test',
err: { code: 'error', msg: 'test' },
};
objectCopy(target, source1, source2);
assert.deepStrictEqual(target, result,
'target should have the same properties as source');
done();
});
});

View File

@ -1,17 +0,0 @@
#!/usr/bin/env node
// Convert string args into primitive value
const fromStr = (str, primitive) => (str === `${primitive}` ? primitive : str);
const date = fromStr(process.argv[2], undefined);
const exitCode = fromStr(fromStr(process.argv[3], null), undefined);
const { stderrUtils } = require('../../../../index');
stderrUtils.catchAndTimestampStderr(
date ? () => date : undefined,
exitCode,
);
process.emitWarning('TestWarningMessage');
// This will print warning after printing error before exit
throw new Error('TestingError');

View File

@ -1,23 +0,0 @@
#!/usr/bin/env node
// Convert string args into primitive value
const fromStr = (str, primitive) => (str === `${primitive}` ? primitive : str);
const date = fromStr(process.argv[2], undefined);
const exitCode = fromStr(fromStr(process.argv[3], null), undefined);
const promise = fromStr(process.argv[4], true);
const { stderrUtils } = require('../../../../index');
stderrUtils.catchAndTimestampUncaughtException(
date ? () => date : undefined,
exitCode,
);
// Executed if process does not exit, process is in undefined behavior (bad)
// eslint-disable-next-line no-console
setTimeout(() => console.log('EXECUTED AFTER UNCAUGHT EXCEPTION'), 1);
if (promise === true) {
Promise.reject();
} else {
throw new Error('TestingError');
}

View File

@ -1,38 +0,0 @@
#!/usr/bin/env node
// Convert string args into primitive value
const fromStr = (str, primitive) => (str === `${primitive}` ? primitive : str);
const date = fromStr(process.argv[2], undefined);
const name = fromStr(process.argv[3], undefined);
const code = fromStr(process.argv[4], undefined);
const detail = fromStr(process.argv[5], undefined);
const { stderrUtils } = require('../../../../index');
stderrUtils.catchAndTimestampWarning(
date ? () => date : undefined,
);
const warning = new Error('TestWarningMessage');
if (name) warning.name = name;
if (code) warning.code = code;
if (detail) warning.detail = detail;
process.emitWarning(warning);
/*
Examples:
(node:203831) Error: TestWarningMessage
at Object.<anonymous> (catchWarning.js:15:17)
...
at node:internal/main/run_main_module:22:47
Above Warning Date: 2024-06-26T16:32:55.505Z
(node:205151) [TEST01] CUSTOM: TestWarningMessage
at Object.<anonymous> (catchWarning.js:15:17)
...
at node:internal/main/run_main_module:22:47
Some additional detail
Above Warning Date: Tue, 31 Dec 2024 10:20:30 GMT
*/

View File

@ -1,309 +0,0 @@
const assert = require('assert');
const { execFile } = require('child_process');
const stderrUtils = require('../../lib/stderrUtils');
/** Simple regex for ISO YYYY-MM-DDThh:mm:ss.sssZ */
// eslint-disable-next-line max-len
const defaultDateRegex = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)/;
// eslint-disable-next-line valid-jsdoc
/** another format: Tue, 31 Dec 2024 10:20:30 GMT */
const customDate = () => new Date('2024-12-31T10:20:30.444Z').toUTCString();
describe('stderrUtils', () => {
const errStackRegex = /Error: TestingError\n(?:.*\sat\s.*\n)+/;
describe('defaultTimestamp', () => {
it('should match ISO format', () => {
assert.match(stderrUtils.defaultTimestamp(), defaultDateRegex);
});
});
describe('printErrorWithTimestamp', () => {
let stderrText;
const originalStderrWrite = process.stderr.write;
const mockedStderrWrite = text => { stderrText = text; return true; };
const err = new Error('TestingError');
const origin = 'uncaughtException';
beforeEach(() => {
stderrText = undefined;
process.stderr.write = mockedStderrWrite;
});
afterEach(() => {
process.stderr.write = originalStderrWrite;
stderrText = undefined;
});
it(
'should write to stderr with current date, origin and stacktrace',
() => {
const written = stderrUtils
.printErrorWithTimestamp(err, origin);
assert.strictEqual(written, true);
const [firstLine, errStack] = stderrText.split(':\n');
const [errDate, errOrigin] = firstLine.split(': ');
assert.match(errDate, defaultDateRegex);
assert.strictEqual(errOrigin, origin);
assert.strictEqual(errStack, `${err.stack}\n`);
},
);
it(
'should write to stderr with custom date, origin and stacktrace',
() => {
const written = stderrUtils
.printErrorWithTimestamp(err, origin, customDate());
assert.strictEqual(written, true);
const [firstLine, errStack] = stderrText.split(':\n');
const [errDate, errOrigin] = firstLine.split(': ');
assert.strictEqual(errDate, customDate());
assert.strictEqual(errOrigin, origin);
assert.strictEqual(errStack, `${err.stack}\n`);
},
);
});
const execOptions = {
cwd: __dirname,
// Subprocess should always stop alone
// But just in case, kill subprocess after 500ms.
// Leave enough time for `nyc` that runs slower.
timeout: 500,
};
// Execute in another process to notice the process exit
// Therefore, looks more like a functional test
const timeoutHint = (ms, retries) =>
`Test fixture process timed out after ${ms}ms with ${retries} retries.\n` +
'Due to nyc coverage first run slowing down process.\nIncrease execOptions.timeout to fix';
describe('catchAndTimestampUncaughtException', () => {
[
{ desc: 'with default date' },
{ desc: 'with custom date', date: customDate() },
{ desc: 'with custom exitCode 42', exitCode: 42 },
{ desc: 'without exit on uncaught exception', exitCode: null },
{ desc: 'for unhandled promise', promise: true },
].forEach(({
desc, date, exitCode, promise,
}) => describe(desc, () => {
/** for before all hook that doesn't support this.retries */
let retries = 4;
let err;
let stdout;
let stderr;
let errStack;
let errDate;
let errOrigin;
before('run process catchUncaughtException', function beforeAllHook(done) {
execFile(
'./fixtures/stderrUtils/catchUncaughtException.js',
[`${date}`, `${exitCode}`, `${promise}`],
execOptions,
(subErr, subStdout, subStderr) => {
if (subErr?.killed) {
retries--;
if (retries <= 0) {
assert.fail(timeoutHint(execOptions.timeout, retries));
}
execOptions.timeout *= 2;
return beforeAllHook(done);
}
err = subErr;
stdout = subStdout;
stderr = subStderr;
let firstLine;
[firstLine, errStack] = stderr.split(':\n');
[errDate, errOrigin] = firstLine.split(': ');
done();
},
);
});
if (exitCode === null) {
it('should not be an error (or timeout)',
() => assert.ifError(err));
it('should have stdout (printed after uncaught exception)',
() => assert.match(stdout,
/^.*EXECUTED AFTER UNCAUGHT EXCEPTION(?:.|\n)*$/));
} else {
it('should be an error',
() => assert.ok(err));
it(`should have exitCode ${exitCode || 1}`,
() => assert.strictEqual(err.code, exitCode || 1));
it('should have empty stdout',
() => assert.strictEqual(stdout, ''));
}
it('should have stderr',
() => assert.ok(stderr));
it('should have date in stderr first line',
() => (date
? assert.strictEqual(errDate, date)
: assert.match(errDate, defaultDateRegex)));
it('should have origin in stderr first line',
() => (promise === true
? assert.strictEqual(errOrigin, 'unhandledRejection')
: assert.strictEqual(errOrigin, 'uncaughtException')));
if (!promise) {
it('should have stack trace on stderr',
() => assert.match(errStack, errStackRegex));
}
}));
});
describe('catchAndTimestampWarning (also tests node onWarning)', () => {
[
{ desc: 'with default date' },
{ desc: 'with custom date', date: customDate() },
{ desc: 'with deprecation warning', name: 'DeprecationWarning' },
{
desc: 'with custom warning',
name: 'CUSTOM',
code: 'TEST01',
detail: 'Some additional detail',
},
].forEach(({
desc, date, name, code, detail,
}) => describe(desc, () => {
/** for before all hook that doesn't support this.retries */
let retries = 4;
let err;
let stdout;
let stderr;
before('run process catchWarning', function beforeAllHook(done) {
execFile(
'./fixtures/stderrUtils/catchWarning.js',
[`${date}`, `${name}`, `${code}`, `${detail}`],
execOptions,
(subErr, subStdout, subStderr) => {
if (subErr?.killed) {
retries--;
if (retries <= 0) {
assert.fail(timeoutHint(execOptions.timeout, retries));
}
execOptions.timeout *= 2;
return beforeAllHook(done);
}
err = subErr;
stdout = subStdout;
stderr = subStderr;
done();
},
);
});
it('should not be an error (or timeout)',
() => assert.ifError(err));
it('should have empty stdout',
() => assert.strictEqual(stdout, ''));
it('should have stderr',
() => assert.ok(stderr));
it('should have message on stderr first line, then stack trace',
() => assert.match(stderr,
/^.*TestWarningMessage\n(?:\s+at\s.*\n)+/));
if (code) {
it('should have code on stderr first line',
() => assert.match(stderr, new RegExp(`^.*[${code}]`)));
}
if (name) {
it('should have name on stderr first line',
() => assert.match(stderr, new RegExp(`^.*${name}:`)));
}
if (detail) {
it('should have detail on stderr',
() => assert.match(stderr, new RegExp(`.*${detail}.*`)));
}
it(`should have ${date ? 'custom' : 'default'} date on stderr`,
() => assert.match(stderr, new RegExp(
`\nAbove Warning Date: ${
date || defaultDateRegex.source}\n`,
)));
}));
});
describe('catchAndTimestampStderr', () => {
[
{ desc: 'with default date' },
{ desc: 'with custom date', date: customDate() },
{ desc: 'with exit code', exitCode: 42 },
].forEach(({
desc, date, exitCode,
}) => describe(desc, () => {
/** for before all hook that doesn't support this.retries */
let retries = 4;
let err;
let stdout;
let stderr;
before('run process catchStderr', function beforeAllHook(done) {
execFile(
'./fixtures/stderrUtils/catchStderr.js',
[`${date}`, `${exitCode}`],
execOptions,
(subErr, subStdout, subStderr) => {
if (subErr?.killed) {
retries--;
if (retries <= 0) {
assert.fail(timeoutHint(execOptions.timeout, retries));
}
execOptions.timeout *= 2;
return beforeAllHook(done);
}
err = subErr;
stdout = subStdout;
stderr = subStderr;
done();
},
);
});
it('should be an error',
() => assert.ok(err));
it(`should have exitCode ${exitCode || 1}`,
() => assert.strictEqual(err.code, exitCode || 1));
it('should have empty stdout',
() => assert.strictEqual(stdout, ''));
it('should have stderr',
() => assert.ok(stderr));
// 2024-06-26T15:04:55.364Z: uncaughtException:
// Error: TestingError
// at Object.<anonymous> (catchStderr.js:16:7)
// at node:internal/main/run_main_module:22:47
it('should have error date, origin and stacktrace in stderr',
() => assert.match(stderr,
new RegExp(`${date || defaultDateRegex.source
}: uncaughtException:\n${errStackRegex.source}`)));
// (node:171245) Warning: TestWarningMessage
// at Object.<anonymous> (catchStderr.js:14:9)
// at node:internal/main/run_main_module:22:47
// Above Warning Date: 2024-06-26T15:04:55.365Z
it('should have warning with stacktrace in stderr', () => {
const trace = 'Warning: TestWarningMessage\n(?:\\s+at\\s.*\n)+';
const detail = `(?:.|\n)*?(?<=\n)Above Warning Date: ${
date || defaultDateRegex.source}\n`;
assert.match(stderr,
new RegExp(`${trace}${detail}`));
});
}));
});
});