├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── category-logger.js ├── custom-timestamp.js ├── domain-logger.js ├── dynamic-rolling-logger.js ├── hourly-logger.js ├── json-appender.js ├── json-file-logger.js ├── log-manager.js ├── logger-config.json ├── rolling-logger.js ├── simple-file-logger.js └── simple-logger.js ├── index.d.ts ├── index.js ├── lib ├── AbstractAppender.js ├── ConsoleAppender.js ├── FileAppender.js ├── Logger.js ├── RollingFileAppender.js └── SimpleLogger.js ├── logo.asc ├── package-lock.json ├── package.json ├── test ├── AbstractAppenderTests.js ├── ConsoleAppenderTests.js ├── FileAppenderTests.js ├── LoggerTests.js ├── RollingFileAppenderTests.js ├── SimpleLoggerTests.js ├── fixtures │ └── logger-config.json ├── integration │ └── run-examples.sh └── mocks │ ├── MockAppender.js │ └── MockLogger.js ├── todo.md └── watcher.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "describe": true, 9 | "it": true, 10 | "before": true 11 | }, 12 | "rules": { 13 | "eqeqeq": "error", 14 | "curly": "error", 15 | "complexity": [ "error", 10 ], 16 | "brace-style": [ "error", "1tbs" ], 17 | "indent": [ "error", 4 ], 18 | "linebreak-style": [ "error", "unix" ], 19 | "no-param-reassign": [ "error", { props: false } ], 20 | "quotes": [ "error", "single" ], 21 | "semi": [ "error", "always" ], 22 | "one-var": ["error", "never"], 23 | "space-before-function-paren": ["error", { 24 | "anonymous": "never", 25 | "named": "never", 26 | "asyncArrow": "always" 27 | }], 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __MAC* 2 | .DS_Store 3 | *.gz 4 | dist 5 | .idea 6 | *.bak 7 | *.log 8 | *.swp 9 | node_modules 10 | jsdoc 11 | query-db 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | file-test.log 3 | logs/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | - "12" 6 | 7 | before_script: 8 | - npm install 9 | 10 | script: make test 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | JSFILES=index.js lib/*.js test/*.js test/mocks/*.js examples/*.js 2 | TESTFILES=test/*.js 3 | ESLINT=node_modules/.bin/eslint 4 | MOCHA=node_modules/.bin/mocha 5 | 6 | all: 7 | @make npm 8 | @make test 9 | 10 | npm: 11 | @npm install 12 | 13 | lint: 14 | @( $(ESLINT) $(JSFILES) && echo "lint tests passed..." ) 15 | 16 | test: 17 | @( [ -d node_modules ] || make npm ) 18 | @( $(MOCHA) $(TESTFILES) && make lint ) 19 | 20 | integration: 21 | @( ./test/integration/run-examples.sh ) 22 | 23 | watch: 24 | @( ./watcher.js ) 25 | 26 | .PHONY: npm 27 | .PHONY: test 28 | .PHONY: jshint 29 | .PHONY: watch 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Node Logger 2 | ``` 3 | __ _ _ __ _ __ 4 | / _(_)_ __ ___ _ __ | | ___ /\ \ \___ __| | ___ / / ___ __ _ __ _ ___ _ __ 5 | \ \| | '_ ` _ \| '_ \| |/ _ \ / \/ / _ \ / _` |/ _ \ / / / _ \ / _` |/ _` |/ _ \ '__| 6 | _\ \ | | | | | | |_) | | __/ / /\ / (_) | (_| | __/ / /__| (_) | (_| | (_| | __/ | 7 | \__/_|_| |_| |_| .__/|_|\___| \_\ \/ \___/ \__,_|\___| \____/\___/ \__, |\__, |\___|_| 8 | |_| |___/ |___/ 9 | ``` 10 | 11 | [![NPM version](https://badge.fury.io/js/simple-node-logger.svg)](http://badge.fury.io/js/simple-node-logger) [![Build Status](https://travis-ci.org/darrylwest/simple-node-logger.svg?branch=master)](https://travis-ci.org/darrylwest/simple-node-logger) [![Dependency Status](https://david-dm.org/darrylwest/simple-node-logger.svg)](https://david-dm.org/darrylwest/simple-node-logger) 12 | 13 | A simple multi-level logger for console, file, and rolling file appenders. Features include: 14 | 15 | - levels: trace, debug, info, warn, error and fatal levels (plus all and off) 16 | - flexible appender/formatters with default to HH:mm:ss.SSS LEVEL message 17 | - add appenders to send output to console, file, rolling file, etc 18 | - change log levels on the fly 19 | - domain and category columns 20 | - overridable format methods in base appender 21 | - stats that track counts of all log statements including warn, error, etc 22 | - ability to configure to emit process error event for central trapping 23 | 24 | ## Installation 25 | 26 | `npm install simple-node-logger --save` 27 | 28 | 29 | ## How to use 30 | ```javascript 31 | // create a stdout console logger 32 | const log = require('simple-node-logger').createSimpleLogger(); 33 | ``` 34 | 35 | or 36 | 37 | ```javascript 38 | // create a stdout and file logger 39 | const log = require('simple-node-logger').createSimpleLogger('project.log'); 40 | ``` 41 | 42 | or 43 | 44 | ```javascript 45 | // create a custom timestamp format for log statements 46 | const SimpleNodeLogger = require('simple-node-logger'), 47 | opts = { 48 | logFilePath:'mylogfile.log', 49 | timestampFormat:'YYYY-MM-DD HH:mm:ss.SSS' 50 | }, 51 | log = SimpleNodeLogger.createSimpleLogger( opts ); 52 | ``` 53 | 54 | or 55 | 56 | ```javascript 57 | // create a file only file logger 58 | const log = require('simple-node-logger').createSimpleFileLogger('project.log'); 59 | ``` 60 | 61 | or 62 | 63 | ```javascript 64 | // create a rolling file logger based on date/time that fires process events 65 | const opts = { 66 | errorEventName:'error', 67 | logDirectory:'/mylogfiles', // NOTE: folder must exist and be writable... 68 | fileNamePattern:'roll-.log', 69 | dateFormat:'YYYY.MM.DD' 70 | }; 71 | const log = require('simple-node-logger').createRollingFileLogger( opts ); 72 | ``` 73 | 74 | or 75 | 76 | ```javascript 77 | // create a log manager 78 | const manager = require('simple-node-logger').createLogManager(); 79 | 80 | manager.createConsoleAppender(); 81 | 82 | const log = manager.createLogger('MyClass'); 83 | // create other logs and appenders... 84 | ``` 85 | 86 | The first use simply logs to the console. The second logs to the console and to the project.log file. The third create a console logger with a custom timestamp format. The fourth logs to the file only. The fifth creates a rolling file log system in the target log folder. The fifth creates a log manager to enable you to add various appenders with multiple levels and create logs for each module or class. 87 | 88 | *See the examples folder for in depth samples...* 89 | 90 | ## Log Levels 91 | 92 | The log levels include the standard set: trace, debug, info, warn, error and fatal. The default level is info. The log level can be set at run-time by doing this: 93 | 94 | ```javascript 95 | log.setLevel('warn'); 96 | ``` 97 | 98 | This sets the log level to warn and suppresses debug and info messages. 99 | 100 | ## Log Statement Formats 101 | 102 | ### Simple Logger 103 | 104 | The default format is HH:mm:ss.SSS LEVEL message. For example, the log message: 105 | 106 | ```javascript 107 | log.info('subscription to ', channel, ' accepted at ', new Date().toJSON()); 108 | ``` 109 | 110 | Yields: 111 | 112 | `14:14:21.363 INFO subscription to /devchannel accepted at 2014-04-10T14:20:52.938Z` 113 | 114 | ### Category Logger 115 | 116 | If you create a logger with a category name, all log statements will include this category. Typically a category is a class or module name. If you create a logger with the category name 'MyCategory', the log statement would format like this: 117 | 118 | `14:14:21.363 INFO MyCategory subscription to /devchannel accepted at 2014-04-10T14:20:52.938Z` 119 | 120 | ## Appenders 121 | 122 | You can create a single logger / log manager and add multiple appenders with different log levels. For example, you can add a console appender that has a log level of warn and a file appender to debug. 123 | 124 | _See examples/category-logger.js for an example_. 125 | 126 | ### Console 127 | 128 | Writes to the console. This is the simplest appender typically used for command line applications or for development. 129 | 130 | ### File 131 | 132 | Writes to the specified file. This appender is typically used for services that periodically start and stop or that have a limited number of log statements. An example would be to log just error & fatal messages separate from other logs. 133 | 134 | ### Rolling File Appender 135 | 136 | The rolling file appender offers a full production logger where files roll based on date and time. The minimum roll time is a single hour. A typical application would be a production environment where log files are rolled throughout the day then archived to a separate location. 137 | 138 | The rolling file appender requires a valid date format and file name pattern. The filename must contain the key word that will be replaced with the formatted date. The configuration must also include a target log directory where the files will be written. 139 | 140 | #### Valid Filename Patterns 141 | 142 | ``` 143 | mylog-.log 144 | ApplicationName.log. 145 | .log 146 | 147 | ``` 148 | 149 | #### Valid Date Formats 150 | 151 | Date formats must map to acceptable file names so have more restrictions than typical dates. If you use delimiters, you are restricted to a dash or dot delimiter to separate year, month, day and hour. Valid examples include: 152 | 153 | ``` 154 | MMDD // simple month day that rolls at midnight (no delimiters) 155 | YYYY.MM.DD-HH // year month day and hour that can roll up to once per hour 156 | YYYY-MM-DD.a // year month day and am/pm that rolls twice per day 157 | YYYY-MMM-DD // year month day where month is the short name (Mar, Apr, etc) 158 | ``` 159 | 160 | The default format YYYY.MM.DD is used if the format is not supplied. 161 | 162 | ## Dynamic Configuration 163 | 164 | Create a javascript configuration that implements 'readConfig' to return configuration details. 165 | 166 | ## Examples 167 | 168 | The examples folder includes a handful of simple to not so simple cases for console, file, multi-appender, category, etc. 169 | 170 | ## Customizations 171 | 172 | ### Appenders 173 | 174 | Adding a new appender is as easy as implementing write( logEntry ). The easiest way to implement is by extending the base class AbstractAppender. You may also easily override the formatting, order, etc by overriding or providing your own abstract or concrete appender. 175 | 176 | For example, you can extend the AbstractAppender to create a JSON appender by doing this: 177 | 178 | ```javascript 179 | const AbstractAppender = require('simple-node-logger').AbstractAppender; 180 | 181 | const JSONAppender = function() { 182 | 'use strict'; 183 | var appender = this; 184 | 185 | var opts = { 186 | typeName:'JSONAppender' 187 | }; 188 | 189 | AbstractAppender.extend( this, opts ); 190 | 191 | // format and write all entry/statements 192 | this.write = function(entry) { 193 | var fields = appender.formatEntry( entry ); 194 | 195 | process.stdout.write( JSON.stringify( entry ) + '\n' ); 196 | }; 197 |    }; 198 | ``` 199 | 200 | ### Overrides 201 | 202 | #### Appenders 203 | 204 | The appenders have formatting messages that can be overridden at the abstract or concrete level. The format methods include: 205 | 206 | - formatEntry(entry) - to override all formatting 207 | - formatMessage(msgList) - to override a list of messages 208 | - formatDate(value) - custom date, defaults to ISO8601 209 | - formatObject(value) - custom object, defaults to json for regular objects 210 | 211 | #### Logger 212 | 213 | It's easy to extend any one of the log methods at the instance level. Here is an example of overriding the error log to send a socket message: 214 | 215 | ```javascript 216 | const log = new require('simple-node-logger').createSimpleLogger(); 217 | const socket = openWebSocket(); 218 | 219 | // override the standard error method to send a socket message 220 | log.error = function() { 221 | var args = Array.prototype.slice.call( arguments ), 222 | entry = log.log('error', args); 223 | 224 | // now do something special with the log entry... 225 | process.nextTick(function() { 226 | socket.send( JSON.stringify( entry )); 227 | }); 228 | }; 229 | ``` 230 | 231 | 232 | ## Tests 233 | 234 | All unit tests are written in mocha/chai/should and can be run from the command line by doing this: 235 | 236 | `make test` 237 | 238 | There is also a file watcher that can be invoked with this: 239 | 240 | `make watch` 241 | 242 | 243 | ## Mocks 244 | 245 | Mocks used for testing include MockLogger and MockAppender. Typically you would use MockLogger for unit tests like this: 246 | 247 | ```javascript 248 | const MockLogger = require('simple-node-logger').mocks.MockLogger; 249 | 250 | const log = MockLogger.createLogger('MyCategory'); 251 | 252 | log.info('this is a log statement'); 253 |    log.getLogEntries().length.should.equal( 1 ); 254 | ``` 255 | 256 | MockLogger extends Logger and uses MockAppender to capture log entries. 257 | 258 | ## License 259 | 260 | Apache 2.0 261 | 262 | ## Recent updates... 263 | 264 | * 0.93.29: when an Error object is logged, the message and stack trace are sent to log targets 265 | * 0.93.30: fixed example/category-logger.js and examples/domain-logger.js to not double-log 266 | * 0.93.31: added thisArg to methods in AbstractAppender to enable proper binding and full override when extending 267 | 268 | - - - 269 |

Copyright © 2014-2019, rain city software | Version 18.12.24

270 | -------------------------------------------------------------------------------- /examples/category-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const filename = '/tmp/file-test.log'; 4 | console.log('a category logger with console and file appenders...\nlog file: ', filename); 5 | 6 | const SimpleLogger = require('../lib/SimpleLogger'); 7 | const manager = new SimpleLogger({errorEventName: 'error'}); 8 | 9 | let log1; 10 | let log2; 11 | 12 | process.on('error', (msg) => { 13 | console.log('Error event caught: ', JSON.stringify(msg)); 14 | }); 15 | 16 | manager.createConsoleAppender(); 17 | manager.createFileAppender({logFilePath: filename}); 18 | 19 | log1 = manager.createLogger('CategoryOne', 'trace'); 20 | log2 = manager.createLogger('CategoryTwo', 'trace'); 21 | 22 | log1.trace('this is a simple trace log statement (should not show)'); 23 | log1.debug('this is a simple debug log statement (should not show)'); 24 | log1.info('this is a simple info log statement/entry'); 25 | log2.warn('this is a simple warn log statement/entry'); 26 | log1.error('this is a simple error log statement/entry'); 27 | log1.error(); 28 | log2.fatal('this is a simple fatal log statement/entry'); 29 | 30 | log2.trace('this is a simple trace log statement (should show)'); 31 | log1.debug('this is a simple debug log statement (should show)'); 32 | 33 | const loggers = manager.getLoggers(); 34 | loggers.forEach(logger => { 35 | console.log('stats: ', logger.getCategory(), logger.getStats()); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/custom-timestamp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const opts = { 4 | timestampFormat: 'mm:ss.SSS' 5 | }; 6 | const log = require('../lib/SimpleLogger').createSimpleLogger(opts); 7 | 8 | log.trace('this is a simple trace log statement (should not show)'); 9 | log.debug('this is a simple debug log statement (should not show)'); 10 | log.info('this is a simple info log statement/entry'); 11 | log.warn('this is a simple warn log statement/entry'); 12 | log.error('this is a simple error log statement/entry'); 13 | log.fatal('this is a simple fatal log statement/entry'); 14 | 15 | log.info('set the level to all'); 16 | log.setLevel('all'); 17 | log.trace('this is a simple trace log statement (should show)'); 18 | log.debug('this is a simple debug log statement (should show)'); 19 | 20 | -------------------------------------------------------------------------------- /examples/domain-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const filename = '/tmp/file-test.log'; 4 | console.log('a category logger with console and file appenders...\nlog file: ', filename); 5 | 6 | const SimpleLogger = require('../lib/SimpleLogger'); 7 | const opts = { 8 | domain: 'MyDomain', 9 | dfltLevel: 'debug' 10 | }; 11 | const manager = new SimpleLogger(opts); 12 | 13 | let log1; 14 | let log2; 15 | 16 | manager.createConsoleAppender(); 17 | manager.createFileAppender({logFilePath: filename}); 18 | 19 | log1 = manager.createLogger('CategoryOne'); 20 | log2 = manager.createLogger('CategoryTwo'); 21 | 22 | log1.trace('this is a simple info log statement/entry: ', opts); 23 | log1.debug('this is a simple debug log statement (should not show)'); 24 | log1.info('this is a simple trace log statement: ', {thedate: new Date()}, ', now: ', new Date(), 1, 2, 3); 25 | log2.trace('this is a simple warn log statement/entry'); 26 | log1.error('this is a simple error log statement/entry: ', manager, 4, 5); 27 | log2.fatal('this is a simple fatal log statement/entry'); 28 | 29 | log2.trace('this is a simple trace log statement (should show)'); 30 | log1.debug('this is a simple debug log statement (should show)'); 31 | 32 | -------------------------------------------------------------------------------- /examples/dynamic-rolling-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const port = 18034; 4 | 5 | // configure to read the config file each 2 minutes... 6 | 7 | const opts = { 8 | logDirectory: __dirname + '/../logs', 9 | fileNamePattern: 'dynamic-.log', 10 | dateFormat: 'YYYY.MM.DD-a', 11 | domain: 'MyApplication-' + port, 12 | level: 'info', 13 | loggerConfigFile: __dirname + '/logger-config.json', 14 | refresh: 10 * 1000 // read/refresh each 60 seconds 15 | }; 16 | 17 | console.log('config file: ', opts.loggerConfigFile); 18 | const log = require('../lib/SimpleLogger').createRollingFileLogger(opts); 19 | 20 | log.setLevel('trace'); 21 | 22 | // write some stuff... 23 | log.trace('this is a simple trace log statement (should not show)'); 24 | log.debug('this is a simple debug log statement (should not show)'); 25 | log.info('this is a simple info log statement/entry'); 26 | log.warn('this is a simple warn log statement/entry'); 27 | 28 | const appender = log.getAppenders()[0]; 29 | console.log('write to file: ', appender.__protected().currentFile); 30 | 31 | // rolling file writer uses interval, so we need to exit 32 | let count = 5; 33 | setInterval(function() { 34 | log.trace('trace time: ', new Date().toJSON()); 35 | log.debug('debug time: ', new Date().toJSON()); 36 | log.info('info time: ', new Date().toJSON()); 37 | log.warn('warn mark tm'); 38 | log.error('error mark tm'); 39 | 40 | console.log(count); 41 | count--; 42 | if (count < 0) { 43 | process.exit(0); 44 | } 45 | }, 1000); 46 | 47 | -------------------------------------------------------------------------------- /examples/hourly-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const opts = { 4 | domain:'Domain-A', 5 | logDirectory: __dirname + '/../logs', 6 | fileNamePattern: 'hourly-test-.log', 7 | dateFormat:'YYYY.MM.DD-HH' 8 | }; 9 | 10 | const log = require('../lib/SimpleLogger').createRollingFileLogger( opts ); 11 | 12 | setInterval(function() { 13 | // write some stuff... 14 | log.trace('this is a simple trace log statement (should not show)'); 15 | log.debug('this is a simple debug log statement (should not show)'); 16 | log.info('this is a simple info log statement/entry'); 17 | log.warn('this is a simple warn log statement/entry'); 18 | log.error('this is a simple error log statement/entry'); 19 | log.fatal('this is a simple fatal log statement/entry'); 20 | 21 | log.info('set the level to all'); 22 | log.setLevel('all'); 23 | log.trace('this is a simple trace log statement (should show)'); 24 | log.debug('this is a simple debug log statement (should show)'); 25 | 26 | }, 800); 27 | 28 | const appender = log.getAppenders()[0]; 29 | console.log('write to file: ', appender.__protected().currentFile ); 30 | 31 | setTimeout(function() { 32 | console.log('exiting...'); 33 | process.exit( 0 ); 34 | }, 2000); 35 | -------------------------------------------------------------------------------- /examples/json-appender.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const SimpleLogger = require('../index.js'); 4 | const Logger = require('../lib/Logger'); 5 | const AbstractAppender = SimpleLogger.AbstractAppender; 6 | const manager = new SimpleLogger(); 7 | 8 | const JSONAppender = function(options = {}) { 9 | const appender = this; 10 | const opts = { 11 | typeName: 'JSONAppender' 12 | }; 13 | 14 | // eslint-disable-next-line no-unused-vars 15 | let level = options.level || Logger.STANDARD_LEVELS[1]; 16 | let levels = options.levels || Logger.STANDARD_LEVELS; 17 | 18 | AbstractAppender.extend(this, opts); 19 | 20 | this.write = function(entry) { 21 | const fields = appender.formatEntry(entry); 22 | process.stdout.write(JSON.stringify(fields) + '\n'); 23 | }; 24 | 25 | this.setLevel = function(level) { 26 | const idx = levels.indexOf(level); 27 | if (idx >= 0) { 28 | // eslint-disable-next-line no-param-reassign 29 | level = idx; 30 | } 31 | }; 32 | }; 33 | 34 | manager.addAppender(new JSONAppender()); 35 | const log = manager.createLogger('JsonTest'); 36 | 37 | log.trace('this is a simple trace log statement (should not show)'); 38 | log.debug('this is a simple debug log statement (should not show)'); 39 | log.info('this is a simple info log statement/entry'); 40 | log.warn('this is a simple warn log statement/entry'); 41 | log.error('this is a simple error log statement/entry'); 42 | log.fatal('this is a simple fatal log statement/entry'); 43 | 44 | log.info('set the level to all'); 45 | log.setLevel('all'); 46 | log.trace('this is a simple trace log statement (should show)'); 47 | log.debug('this is a simple debug log statement (should show)'); 48 | 49 | -------------------------------------------------------------------------------- /examples/json-file-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const filename = './logs/json-test.log'; 4 | console.log('opening log file: ', filename); 5 | 6 | const SimpleLogger = require('../lib/SimpleLogger'); 7 | const manager = new SimpleLogger(); 8 | const opts = { 9 | logFilePath: filename 10 | }; 11 | const appender = manager.createFileAppender(opts); 12 | 13 | appender.formatter = function(entry) { 14 | let fields = appender.formatEntry(entry); 15 | 16 | fields[1] = entry.level; 17 | 18 | return JSON.stringify(fields) + '\n'; 19 | }; 20 | 21 | manager.addAppender(appender); 22 | const log = manager.createLogger('JSON'); 23 | 24 | log.trace('this is a simple trace log statement (should not show)'); 25 | log.debug('this is a simple debug log statement (should not show)'); 26 | log.info('this is a simple info log statement/entry'); 27 | log.warn('this is a simple warn log statement/entry'); 28 | log.error('this is a simple error log statement/entry'); 29 | log.fatal('this is a simple fatal log statement/entry'); 30 | 31 | log.info('set the level to all'); 32 | log.setLevel('all'); 33 | log.trace('this is a simple trace log statement (should show)'); 34 | log.debug('this is a simple debug log statement (should show)'); 35 | 36 | -------------------------------------------------------------------------------- /examples/log-manager.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const manager = require('../lib/SimpleLogger').createLogManager(); 4 | const log = manager.createLogger('MyCategory'); 5 | 6 | // write some stuff... 7 | log.trace('this is a simple trace log statement (should not show)'); 8 | log.debug('this is a simple debug log statement (should not show)'); 9 | log.info('this is a simple info log statement/entry'); 10 | log.warn('this is a simple warn log statement/entry'); 11 | log.error('this is a simple error log statement/entry'); 12 | log.fatal('this is a simple fatal log statement/entry'); 13 | 14 | log.info('set the level to all'); 15 | log.setLevel('all'); 16 | log.trace('this is a simple trace log statement (should show)'); 17 | log.debug('this is a simple debug log statement (should show)'); 18 | 19 | -------------------------------------------------------------------------------- /examples/logger-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders":[ 3 | { 4 | "typeName":"RollingFileAppender", 5 | "level":"debug" 6 | }, 7 | { 8 | "typeName":"ConsoleAppender", 9 | "level":"fatal" 10 | } 11 | ], 12 | "loggers":[ 13 | { 14 | "category":"all", 15 | "level":"debug" 16 | }, 17 | { 18 | "category":"ApplicationFactory", 19 | "level":"warn" 20 | }, 21 | { 22 | "category":"CalculationDelegate", 23 | "level":"debug" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/rolling-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const opts = { 4 | logDirectory: __dirname + '/../logs', 5 | fileNamePattern: 'apptest-.log', 6 | dateFormat:'YYYY.MM.DD-HHa' 7 | }; 8 | 9 | const log = require('../lib/SimpleLogger').createRollingFileLogger( opts ); 10 | 11 | // write some stuff... 12 | log.trace('this is a simple trace log statement (should not show)'); 13 | log.debug('this is a simple debug log statement (should not show)'); 14 | log.info('this is a simple info log statement/entry'); 15 | log.warn('this is a simple warn log statement/entry'); 16 | log.error('this is a simple error log statement/entry'); 17 | log.fatal('this is a simple fatal log statement/entry'); 18 | 19 | log.info('set the level to all'); 20 | log.setLevel('all'); 21 | log.trace('this is a simple trace log statement (should show)'); 22 | log.debug('this is a simple debug log statement (should show)'); 23 | 24 | const appender = log.getAppenders()[0]; 25 | console.log('write to file: ', appender.__protected().currentFile ); 26 | 27 | // rolling file writer uses interval, so we need to exit 28 | setTimeout(function() { 29 | console.log('exiting...'); 30 | process.exit( 0 ); 31 | }, 1000); 32 | 33 | -------------------------------------------------------------------------------- /examples/simple-file-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const filename = './logs/file-test.log'; 4 | console.log('opening log file: ', filename); 5 | 6 | const log = require('../lib/SimpleLogger').createSimpleFileLogger( filename ); 7 | 8 | log.info('this is a simple info log statement/entry'); 9 | log.warn('this is a simple warn log statement/entry'); 10 | log.error('this is a simple error log statement/entry'); 11 | log.trace('this is a simple trace log statement (should not show)'); 12 | log.debug('this is a simple debug log statement (should not show)'); 13 | log.fatal('this is a simple fatal log statement/entry'); 14 | 15 | log.info('set the level to all'); 16 | log.setLevel('all'); 17 | log.trace('this is a simple trace log statement (should show)'); 18 | log.debug('this is a simple debug log statement (should show)'); 19 | 20 | -------------------------------------------------------------------------------- /examples/simple-logger.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const log = require('../lib/SimpleLogger').createSimpleLogger({level: 'info'}); 4 | 5 | log.trace('this is a simple trace log statement (should not show)'); 6 | log.debug('this is a simple debug log statement (should not show)'); 7 | log.info('this is a simple info log statement/entry'); 8 | log.warn('this is a simple warn log statement/entry'); 9 | log.error('this is a simple error log statement/entry'); 10 | log.fatal('this is a simple fatal log statement/entry'); 11 | 12 | // test logging a null 13 | log.error(); 14 | 15 | log.info('set the level to all'); 16 | log.setLevel('all'); 17 | log.trace('this is a simple trace log statement (should show)'); 18 | log.debug('this is a simple debug log statement (should show)'); 19 | 20 | // example of an override 21 | log.warn = function() { 22 | const args = Array.prototype.slice.call(arguments); 23 | const entry = log.log('warn', args); 24 | 25 | process.nextTick(function() { 26 | console.log('custom:', JSON.stringify(entry)); 27 | }); 28 | }; 29 | 30 | log.warn('this is an override test...'); 31 | 32 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import moment = require("moment"); 5 | 6 | type STANDARD_LEVELS = SimpleLogger.STANDARD_LEVELS; 7 | type Logger = SimpleLogger.Logger; 8 | 9 | type IConsoleAppenderOptions = SimpleLogger.IConsoleAppenderOptions; 10 | type ConsoleAppender = SimpleLogger.appenders.ConsoleAppender; 11 | type IFileAppenderOptions = SimpleLogger.IFileAppenderOptions; 12 | type FileAppender = SimpleLogger.appenders.FileAppender; 13 | type IRollingFileAppenderOptions = SimpleLogger.IRollingFileAppenderOptions; 14 | type RollingFileAppender = SimpleLogger.appenders.RollingFileAppender; 15 | type AbstractAppender = SimpleLogger.AbstractAppender; 16 | type ISimpleLoggerOptions = SimpleLogger.ISimpleLoggerOptions; 17 | declare class SimpleLogger 18 | { 19 | constructor(opts?: ISimpleLoggerOptions); 20 | createLogger(options?: SimpleLogger.ILoggerOptions): Logger; 21 | createLogger(category?: string, level?: STANDARD_LEVELS): Logger; 22 | createConsoleAppender(opts?: IConsoleAppenderOptions): ConsoleAppender; 23 | createFileAppender(opts?: IFileAppenderOptions): FileAppender; 24 | createRollingFileAppender(opts?: IRollingFileAppenderOptions): RollingFileAppender; 25 | addAppender(appender: AbstractAppender): T; 26 | getAppenders(): AbstractAppender[]; 27 | getLoggers(): Logger[]; 28 | startRefreshThread(): void; 29 | setAllLoggerLevels(level: STANDARD_LEVELS): void; 30 | readConfig(completeCallback?: (err?: any) => void): void; 31 | } 32 | 33 | 34 | 35 | declare namespace SimpleLogger 36 | { 37 | export type STANDARD_LEVELS = 'all' | 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'; 38 | interface IEntry 39 | { 40 | ts: moment.MomentInput; 41 | pid: number; 42 | domain?: string; 43 | category?: string; 44 | level: STANDARD_LEVELS; 45 | msg: any | any[]; 46 | } 47 | interface ISimpleLoggerOptions 48 | { 49 | domain?: string; 50 | loggers?: Logger[]; 51 | level?: STANDARD_LEVELS; 52 | loggerConfigFile?: string; 53 | refresh?: number; 54 | fs?: any; 55 | createInterval?: typeof setInterval; 56 | minRefresh?: number; 57 | errorEventName?: string; 58 | } 59 | export interface ILoggerOptions 60 | { 61 | pid?: number; 62 | errorEventName?: string; 63 | domain?: string; 64 | category?: string; 65 | level?: STANDARD_LEVELS; 66 | levels?: STANDARD_LEVELS[]; 67 | appenders?: AbstractAppender[] 68 | } 69 | export namespace Logger 70 | { 71 | var STANDARD_LEVELS: STANDARD_LEVELS[]; 72 | var DEFAULT_LEVEL: STANDARD_LEVELS; 73 | } 74 | export class Logger 75 | { 76 | 77 | constructor(options?: ILoggerOptions); 78 | /** 79 | * log the statement message 80 | * 81 | * @param level the level of this message (label, i.e, info, warn, etc) 82 | * @param msg 83 | */ 84 | log(level: STANDARD_LEVELS, msg: any[] | any): void; 85 | 86 | /** 87 | * set the level 88 | * 89 | * @param lvl one of the recognized logger levels 90 | */ 91 | setLevel(lvl: STANDARD_LEVELS): void; 92 | 93 | /** 94 | * return the current level string 95 | */ 96 | getLevel(): STANDARD_LEVELS; 97 | 98 | /** 99 | * set the list of appenders 100 | * @param appenderList 101 | */ 102 | setAppenders(appenderList: AbstractAppender[]): void; 103 | 104 | /** 105 | * add an appender to the list 106 | * 107 | * @param appender - implements write method 108 | */ 109 | addAppender(appender: T): T; 110 | 111 | /** 112 | * remove the appender using the type name 113 | */ 114 | removeAppender(typeName: string): void; 115 | 116 | 117 | getAppenders(): AbstractAppender[]; 118 | isDebug(): boolean; 119 | isInfo(): boolean; 120 | 121 | /** 122 | * return the status map with log counts for each level 123 | */ 124 | getStats(): Map; 125 | 126 | getCategory(): string | undefined; 127 | 128 | getDomain(): string | undefined; 129 | 130 | all(...arr: any[]): void; 131 | trace(...arr: any[]): void; 132 | debug(...arr: any[]): void; 133 | info(...arr: any[]): void; 134 | warn(...arr: any[]): void; 135 | error(...arr: any[]): void; 136 | fatal(...arr: any[]): void; 137 | } 138 | 139 | export interface IAbstractAppenderOptions 140 | { 141 | typeName?: string; 142 | timestampFormat?: string; 143 | prettyPrint?: boolean; 144 | separator?: string; 145 | level?: STANDARD_LEVELS; 146 | levels?: STANDARD_LEVELS[]; 147 | } 148 | export interface IConsoleAppenderOptions extends IAbstractAppenderOptions 149 | { 150 | typeName?: string; 151 | writer?: Function; 152 | } 153 | export interface IFileAppenderOptions extends IAbstractAppenderOptions 154 | { 155 | typeName?: string; 156 | fs?: any; 157 | autoOpen?: boolean; 158 | logFilePath: string; 159 | writer?: any; 160 | } 161 | export interface IRollingFileAppenderOptions extends IAbstractAppenderOptions 162 | { 163 | typeName?: string; 164 | fs?: any; 165 | autoOpen?: boolean; 166 | logDirectory: string; 167 | fileNamePattern: string; 168 | dateFormat?: string; 169 | currentFile?: string; 170 | createInterval?: typeof setInterval; 171 | } 172 | 173 | export abstract class AbstractAppender 174 | { 175 | constructor(options?: IAbstractAppenderOptions); 176 | /** 177 | * format the entry and return the field list 178 | * 179 | * @param entry the log entry 180 | * @param thisArg - use this to override the base object 181 | * 182 | * @returns field array 183 | */ 184 | formatEntry(entry: any, thisArg?: AbstractAppender): string[]; 185 | 186 | /** 187 | * format the message 188 | * 189 | * @param msg the log message 190 | * @param thisArg - use this to override the base object 191 | * 192 | * @returns field array 193 | */ 194 | formatMessage(msg: any | any[], thisArg?: AbstractAppender): string; 195 | 196 | formatDate(value: Date): string; 197 | formatObject(value: any): string; 198 | formatLevel(level: STANDARD_LEVELS): string; 199 | formatTimestamp(ts: moment.MomentInput): string; 200 | getTypeName(): string; 201 | 202 | //formatter(entry: any): string; 203 | abstract write(entry: IEntry): void; 204 | abstract setLevel(level: STANDARD_LEVELS): void; 205 | 206 | __protected(): any; 207 | } 208 | 209 | //export type IConsoleAppenderOptions = appenders.IConsoleAppenderOptions; 210 | //export type IFileAppenderOptions = appenders.IFileAppenderOptions; 211 | //export type IRollingFileAppenderOptions = appenders.IRollingFileAppenderOptions; 212 | 213 | export namespace appenders 214 | { 215 | 216 | 217 | export class ConsoleAppender extends AbstractAppender 218 | { 219 | constructor(options: IAbstractAppenderOptions & IConsoleAppenderOptions); 220 | formatter(entry: IEntry): string; 221 | write(entry: IEntry): void; 222 | setLevel(level: STANDARD_LEVELS): void; 223 | } 224 | 225 | 226 | export class FileAppender extends AbstractAppender 227 | { 228 | constructor(options?: IFileAppenderOptions); 229 | formatter(entry: IEntry): string; 230 | write(entry: IEntry): void; 231 | setLevel(level: STANDARD_LEVELS): void; 232 | } 233 | 234 | 235 | export class RollingFileAppender extends AbstractAppender 236 | { 237 | constructor(options?: IRollingFileAppenderOptions); 238 | formatter(entry: IEntry): string; 239 | write(entry: IEntry): void; 240 | setLevel(level: STANDARD_LEVELS): void; 241 | 242 | checkForRoll(now?: moment.Moment): boolean; 243 | createFileName(now?: moment.Moment): string; 244 | } 245 | } 246 | 247 | 248 | /** 249 | * static convenience method to create a file logger (no console logging); 250 | * 251 | * @param options - if string then it's the logFilePath, else options with the logFilePath 252 | * @returns Logger 253 | */ 254 | export function createSimpleLogger(): Logger; 255 | export function createSimpleLogger(logFilePath: string): Logger; 256 | export function createSimpleLogger(options: ISimpleLoggerOptions & 257 | Partial & 258 | Partial): Logger; 259 | 260 | /** 261 | * create a rolling file logger by passing options to SimpleLogger and Logger. this enables setting 262 | * of domain, category, etc. 263 | * 264 | * @param options 265 | * @returns rolling logger 266 | */ 267 | export function createSimpleFileLogger(logFilePath: string): Logger; 268 | export function createSimpleFileLogger(options: ISimpleLoggerOptions & 269 | IFileAppenderOptions): Logger; 270 | 271 | /** 272 | * create a rolling file appender and add it to the appender list 273 | * 274 | * @param options 275 | * @returns the appender 276 | */ 277 | export function createRollingFileLogger(options: ISimpleLoggerOptions & 278 | IRollingFileAppenderOptions & 279 | { readLoggerConfig?: Function }): Logger; 280 | 281 | /** 282 | * create a log manager 283 | * 284 | * @param options - file or rolling file specs; 285 | */ 286 | export function createLogManager(options?: ISimpleLoggerOptions & 287 | Partial & 288 | Partial & 289 | { readLoggerConfig?: Function }): SimpleLogger; 290 | } 291 | 292 | export = SimpleLogger; 293 | 294 | 295 | 296 | 297 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | /** darryl.west@raincitysoftware.com **/ 3 | 4 | module.exports = require('./lib/SimpleLogger'); 5 | module.exports.AbstractAppender = require('./lib/AbstractAppender'); 6 | module.exports.Logger = require('./lib/Logger'); 7 | 8 | module.exports.appenders = { 9 | ConsoleAppender:require('./lib/ConsoleAppender'), 10 | FileAppender:require('./lib/FileAppender'), 11 | RollingFileAppender:require('./lib/RollingFileAppender') 12 | }; 13 | 14 | module.exports.mocks = { 15 | MockAppender:require('./test/mocks/MockAppender'), 16 | MockLogger:require('./test/mocks/MockLogger') 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /lib/AbstractAppender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class AbstractAppender 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/7/14 5:58 PM 6 | */ 7 | const util = require( 'util' ); 8 | const moment = require( 'moment' ); 9 | const dash = require( 'lodash' ); 10 | 11 | const AbstractAppender = function(options) { 12 | 'use strict'; 13 | 14 | const appender = this; 15 | const typeName = options.typeName; 16 | const timestampFormat = options.timestampFormat || 'HH:mm:ss.SSS'; 17 | const prettyPrint = options.prettyPrint; 18 | 19 | this.separator = options.separator || ' '; 20 | 21 | /** 22 | * format the entry and return the field list 23 | * 24 | * @param entry the log entry 25 | * @param thisArg - use this to override the base object 26 | * 27 | * @returns field array 28 | */ 29 | this.formatEntry = function(entry, thisArg) { 30 | const apdr = thisArg || appender; 31 | 32 | const fields = []; 33 | 34 | if (entry.domain) { 35 | fields.push( entry.domain ); 36 | } 37 | 38 | fields.push( apdr.formatTimestamp( entry.ts ) ); 39 | fields.push( apdr.formatLevel( entry.level ) ); 40 | 41 | if (entry.category) { 42 | fields.push( entry.category ); 43 | } 44 | 45 | fields.push( apdr.formatMessage( entry.msg ) ); 46 | 47 | return fields; 48 | }; 49 | 50 | /** 51 | * format the message 52 | * 53 | * @param msg the log message 54 | * @param thisArg - use this to override the base object 55 | * 56 | * @returns field array 57 | */ 58 | this.formatMessage = function(msg, thisArg) { 59 | const apdr = thisArg || appender; 60 | 61 | if (!msg) { 62 | return ''; 63 | } 64 | 65 | if (util.isArray( msg )) { 66 | const list = msg.map(function(item) { 67 | if (util.isDate( item )) { 68 | return apdr.formatDate( item ); 69 | } else { 70 | return apdr.formatObject( item ); 71 | } 72 | }); 73 | 74 | return list.join(''); 75 | } else { 76 | return msg; 77 | } 78 | }; 79 | 80 | this.formatDate = function(value) { 81 | return value.toJSON(); 82 | }; 83 | 84 | this.formatObject = function(value) { 85 | if (!value) { 86 | return ''; 87 | } 88 | 89 | if (dash.isObject( value )) { 90 | try { 91 | if (value instanceof Error) { 92 | return [ 93 | value.message, 94 | (prettyPrint) ? JSON.stringify( value, null, 2) : JSON.stringify( value ), 95 | value.stack 96 | ].join('\n'); 97 | } 98 | 99 | return (prettyPrint) ? JSON.stringify( value, null, 2) : JSON.stringify( value ); 100 | } catch (ignore) { 101 | return 'json error: ' + value.toString(); 102 | } 103 | } else { 104 | var s = value.toString(); 105 | if (s === '[object Object]') { 106 | return util.inspect( value ); 107 | } else { 108 | return s; 109 | } 110 | } 111 | }; 112 | 113 | /** 114 | * format the level string by forcing to upper case and padding to 5 chars 115 | * 116 | * @param level 117 | * @returns {string} 118 | */ 119 | this.formatLevel = function(level) { 120 | let str = level.toUpperCase(); 121 | if (str.length < 5) { 122 | str += ' '; 123 | } 124 | 125 | return str; 126 | }; 127 | 128 | /** 129 | * format the timestamp to HH:mm:ss.SSS 130 | * 131 | * @param ts the unix milliseconds 132 | * @returns formatted string 133 | */ 134 | this.formatTimestamp = function(ts) { 135 | return moment( ts ).format( timestampFormat ); 136 | }; 137 | 138 | /** 139 | * return the type name of this appender (ConsoleAppender) 140 | */ 141 | this.getTypeName = function() { 142 | return typeName; 143 | }; 144 | 145 | // constructor tests 146 | if (!typeName) { 147 | throw new Error('appender must be constructed with a type name'); 148 | } 149 | }; 150 | 151 | module.exports = AbstractAppender; 152 | 153 | AbstractAppender.extend = function(child, options) { 154 | 'use strict'; 155 | 156 | const parent = new AbstractAppender( options ); 157 | 158 | dash.extend( child, parent ); 159 | 160 | return parent; 161 | }; 162 | -------------------------------------------------------------------------------- /lib/ConsoleAppender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class ConsoleAppender 3 | * @classdesc ConsoleAppender writes to the console all entries at or above the specified level. 4 | * 5 | * @author: darryl.west@raincitysoftware.com 6 | * @created: 7/6/14 12:02 PM 7 | */ 8 | const Logger = require('./Logger' ); 9 | const AbstractAppender = require('./AbstractAppender' ); 10 | 11 | /*eslint no-console: "off"*/ 12 | const ConsoleAppender = function(opts) { 13 | 'use strict'; 14 | 15 | // get a copy of the opts 16 | const options = Object.assign({}, opts); 17 | 18 | const appender = this; 19 | const typeName = options.typeName || 'ConsoleAppender'; 20 | const writer = options.writer || console.log; 21 | 22 | let level = options.level || Logger.STANDARD_LEVELS[0]; 23 | let levels = options.levels || Logger.STANDARD_LEVELS; 24 | let currentLevel = levels.indexOf( level ); 25 | 26 | options.typeName = typeName; 27 | AbstractAppender.extend( this, options ); 28 | 29 | /** 30 | * default formatter for this appender; 31 | * @param entry 32 | */ 33 | this.formatter = function(entry) { 34 | const fields = appender.formatEntry( entry ); 35 | 36 | return fields.join( appender.separator ); 37 | }; 38 | 39 | /** 40 | * call formatter then write the entry to the console output 41 | * @param entry - the log entry 42 | */ 43 | this.write = function(entry) { 44 | if (levels.indexOf( entry.level ) >= currentLevel) { 45 | writer( appender.formatter( entry )); 46 | } 47 | }; 48 | 49 | this.setLevel = function(level) { 50 | const idx = levels.indexOf( level ); 51 | if (idx >= 0) { 52 | currentLevel = idx; 53 | } 54 | }; 55 | }; 56 | 57 | module.exports = ConsoleAppender; 58 | -------------------------------------------------------------------------------- /lib/FileAppender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class FileAppender 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/7/14 5:15 PM 6 | */ 7 | const Logger = require('./Logger' ); 8 | const AbstractAppender = require('./AbstractAppender' ); 9 | const dash = require( 'lodash' ); 10 | const path = require( 'path' ); 11 | 12 | const FileAppender = function(options) { 13 | 'use strict'; 14 | 15 | const appender = this; 16 | const fs = options.fs || require( 'fs' ); 17 | const newline = /^win/.test(process.platform) ? '\r\n' : '\n'; 18 | const typeName = options.typeName || 'FileAppender'; 19 | const autoOpen = dash.isBoolean( options.autoOpen ) ? options.autoOpen : true; 20 | const levels = options.levels || Logger.STANDARD_LEVELS; 21 | 22 | let level = options.level || Logger.DEFAULT_LEVEL; 23 | let currentLevel = levels.indexOf( level ); 24 | let logFilePath = options.logFilePath; 25 | let writer = options.writer; 26 | 27 | options.typeName = typeName; 28 | AbstractAppender.extend( this, options ); 29 | 30 | /** 31 | * default formatter for this appender; 32 | * @param entry 33 | */ 34 | this.formatter = function(entry) { 35 | const fields = appender.formatEntry( entry ); 36 | 37 | // add new line (for linux and windows) 38 | fields.push( newline ); 39 | 40 | return fields.join( appender.separator ); 41 | }; 42 | 43 | /** 44 | * call formatter then write the entry to the console output 45 | * @param entry - the log entry 46 | */ 47 | this.write = function(entry) { 48 | if (levels.indexOf( entry.level ) >= currentLevel) { 49 | writer.write( appender.formatter( entry ) ); 50 | } 51 | }; 52 | 53 | this.setLevel = function(level) { 54 | const idx = levels.indexOf( level ); 55 | if (idx >= 0) { 56 | currentLevel = idx; 57 | } 58 | }; 59 | 60 | // writer is opened on construction 61 | const openWriter = function() { 62 | if (!writer) { 63 | const file = path.normalize( logFilePath ); 64 | const opts = { 65 | flags:'a', 66 | encoding:'utf8' 67 | }; 68 | 69 | writer = fs.createWriteStream( file, opts ); 70 | } 71 | }; 72 | 73 | this.closeWriter = function() { 74 | if (writer) { 75 | writer.end('\n'); 76 | } 77 | }; 78 | 79 | // constructor tests 80 | (function() { 81 | if (!logFilePath) { 82 | throw new Error('appender must be constructed with a log file path'); 83 | } 84 | }()); 85 | 86 | if (autoOpen) { 87 | openWriter(); 88 | } 89 | }; 90 | 91 | module.exports = FileAppender; 92 | -------------------------------------------------------------------------------- /lib/Logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Logger 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/5/14 6:28 PM 6 | */ 7 | 8 | const Logger = function(options) { 9 | 'use strict'; 10 | 11 | const logger = this; 12 | const pid = options.pid || process.pid; 13 | const errorEventName = options.errorEventName; 14 | const stats = new Map(); 15 | 16 | let domain = options.domain; 17 | let category = options.category; 18 | let level = options.level || Logger.DEFAULT_LEVEL; 19 | let levels = options.levels || Logger.STANDARD_LEVELS; 20 | let currentLevel = levels.indexOf(level); 21 | let appenders = options.appenders || []; 22 | 23 | // helper method 24 | const isLevelAt = function(lvl) { 25 | const idx = levels.indexOf(lvl); 26 | 27 | return idx >= currentLevel; 28 | }; 29 | 30 | /** 31 | * log the statement message 32 | * 33 | * @param level the level of this message (label, i.e, info, warn, etc) 34 | * @param msg 35 | */ 36 | this.log = function(level, msg) { 37 | const entry = logger.createEntry(level, msg); 38 | 39 | process.nextTick(function() { 40 | // write the message to the appenders... 41 | appenders.forEach(function(appender) { 42 | appender.write(entry); 43 | }); 44 | 45 | if (level === 'error' && typeof (errorEventName) === 'string' || typeof (errorEventName) === String) { 46 | process.emit(errorEventName, entry); 47 | } 48 | }); 49 | 50 | return entry; 51 | }; 52 | 53 | /** 54 | * create the entry object used to log messages 55 | * 56 | * @param level - info, debug, etc. 57 | * @param messageList - a list of message objects 58 | * @returns then entry object 59 | */ 60 | this.createEntry = function(level, messageList) { 61 | const entry = {}; 62 | 63 | entry.ts = Date.now(); 64 | 65 | entry.pid = pid; 66 | if (domain) { 67 | entry.domain = domain; 68 | } 69 | if (category) { 70 | entry.category = category; 71 | } 72 | 73 | entry.level = level; 74 | entry.msg = messageList; 75 | 76 | return entry; 77 | }; 78 | 79 | /** 80 | * set the level 81 | * 82 | * @param lvl one of the recognized logger levels 83 | */ 84 | this.setLevel = function(lvl) { 85 | currentLevel = levels.indexOf(lvl); 86 | level = lvl; 87 | appenders.forEach(app => { 88 | app.setLevel(lvl); 89 | }); 90 | }; 91 | 92 | /** 93 | * return the current level string 94 | */ 95 | this.getLevel = function() { 96 | return level; 97 | }; 98 | 99 | /** 100 | * set the list of appenders 101 | * @param appenderList 102 | */ 103 | this.setAppenders = function(appenderList) { 104 | appenders = appenderList; 105 | }; 106 | 107 | /** 108 | * add an appender to the list 109 | * 110 | * @param appender - implements write method 111 | */ 112 | this.addAppender = function(appender) { 113 | appenders.push(appender); 114 | }; 115 | 116 | /** 117 | * remove the appender using the type name 118 | */ 119 | this.removeAppender = function(typeName) { 120 | throw new Error(`remove appender ${typeName} is not implemented yet...`); 121 | }; 122 | 123 | this.getAppenders = function() { 124 | return appenders; 125 | }; 126 | 127 | this.isDebug = function() { 128 | return isLevelAt('debug'); 129 | }; 130 | 131 | this.isInfo = function() { 132 | return isLevelAt('info'); 133 | }; 134 | 135 | /** 136 | * return the status map with log counts for each level 137 | */ 138 | this.getStats = function() { 139 | return stats; 140 | }; 141 | 142 | /** 143 | * return the category name 144 | */ 145 | this.getCategory = function() { 146 | return category; 147 | }; 148 | 149 | /** 150 | * return the domain name 151 | */ 152 | this.getDomain = function() { 153 | return domain; 154 | }; 155 | 156 | // now initialize the methods for the standard levels 157 | const init = function() { 158 | levels.forEach(function(lvl) { 159 | stats.set(lvl, 0); 160 | logger[lvl] = function() { 161 | stats.set(lvl, stats.get(lvl) + 1); 162 | if (levels.indexOf(lvl) >= currentLevel) { 163 | const args = Array.prototype.slice.call(arguments); 164 | logger.log(lvl, args); 165 | } 166 | }; 167 | }); 168 | }; 169 | 170 | this.__protected = function() { 171 | return { 172 | pid: pid, 173 | domain: domain, 174 | category: category 175 | }; 176 | }; 177 | 178 | init(); 179 | }; 180 | 181 | Logger.STANDARD_LEVELS = ['all', 'trace', 'debug', 'info', 'warn', 'error', 'fatal']; 182 | Logger.DEFAULT_LEVEL = 'info'; 183 | 184 | module.exports = Logger; 185 | -------------------------------------------------------------------------------- /lib/RollingFileAppender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class RollingFileAppender 3 | * 4 | * roll on size and/or date/time; 5 | * 6 | * @author: darryl.west@raincitysoftware.com 7 | * @created: 7/27/14 9:52 AM 8 | */ 9 | const Logger = require('./Logger'); 10 | const AbstractAppender = require('./AbstractAppender'); 11 | const dash = require('lodash'); 12 | const moment = require('moment'); 13 | const path = require('path'); 14 | 15 | const RollingFileAppender = function(options) { 16 | 'use strict'; 17 | 18 | const appender = this; 19 | const fs = options.fs || require('fs'); 20 | const newline = /^win/.test(process.platform) ? '\r\n' : '\n'; 21 | 22 | let typeName = options.typeName; 23 | let autoOpen = dash.isBoolean(options.autoOpen) ? options.autoOpen : true; 24 | let logDirectory = options.logDirectory; 25 | let fileNamePattern = options.fileNamePattern; 26 | let dateFormat = options.dateFormat || 'YYYY.MM.DD'; 27 | let level = options.level || Logger.DEFAULT_LEVEL; 28 | let levels = options.levels || Logger.STANDARD_LEVELS; 29 | let currentLevel = levels.indexOf(level); 30 | let currentFile = options.currentFile; 31 | let rollTimer; 32 | let createInterval = options.createInterval || setInterval; 33 | let writers = []; 34 | 35 | if (!typeName) { 36 | typeName = options.typeName = 'RollingFileAppender'; 37 | } 38 | 39 | AbstractAppender.extend(this, options); 40 | 41 | const getWriter = function() { 42 | return writers[0]; 43 | }; 44 | 45 | const openWriter = function(fname) { 46 | const filename = fname || appender.createFileName(); 47 | const file = path.join(logDirectory, filename); 48 | const opts = { 49 | flags: 'a', 50 | encoding: 'utf8' 51 | }; 52 | 53 | let writer = fs.createWriteStream(file, opts); 54 | 55 | // make this the current writer... 56 | writers.unshift(writer); 57 | currentFile = file; 58 | 59 | // now close the current logger and remove from the writers list 60 | while (writers.length > 1) { 61 | // close the old writer 62 | writer = writers.pop(); 63 | writer.removeAllListeners(); 64 | writer.end('\n'); 65 | } 66 | }; 67 | 68 | // check once per minute to see if we need to roll 69 | const startRollTimer = function() { 70 | rollTimer = createInterval(function() { 71 | if (appender.checkForRoll()) { 72 | openWriter(); 73 | } 74 | }, 60 * 1000); 75 | }; 76 | 77 | /** 78 | * default formatter for this appender; 79 | * @param entry 80 | */ 81 | this.formatter = function(entry) { 82 | const fields = appender.formatEntry(entry); 83 | 84 | fields.push(newline); 85 | 86 | return fields.join(appender.separator); 87 | }; 88 | 89 | /** 90 | * call formatter then write the entry to the console output 91 | * @param entry - the log entry 92 | */ 93 | this.write = function(entry) { 94 | if (levels.indexOf(entry.level) >= currentLevel) { 95 | const writer = getWriter(); 96 | if (writer) { 97 | writer.write(appender.formatter(entry)); 98 | } else { 99 | /*eslint no-console: "off"*/ 100 | console.log('no writer...'); 101 | } 102 | } 103 | }; 104 | 105 | this.checkForRoll = function(now) { 106 | // check to see if the 107 | const fn = appender.createFileName(now); 108 | const current = path.basename(currentFile); 109 | 110 | return fn !== current; 111 | }; 112 | 113 | this.createFileName = function(now) { 114 | let dt; 115 | 116 | if (now || now instanceof moment) { 117 | dt = now.format(dateFormat); 118 | } else { 119 | dt = moment().format(dateFormat); 120 | } 121 | 122 | return fileNamePattern.replace(//i, dt); 123 | }; 124 | 125 | this.setLevel = function(level) { 126 | const idx = levels.indexOf(level); 127 | if (idx >= 0) { 128 | currentLevel = idx; 129 | } 130 | }; 131 | 132 | this.__protected = function() { 133 | return { 134 | openWriter: openWriter, 135 | currentFile: currentFile, 136 | rollTimer: rollTimer, 137 | writers: writers 138 | }; 139 | }; 140 | 141 | // constructor tests 142 | (function() { 143 | if (!logDirectory) { 144 | throw new Error('appender must be constructed with a log directory'); 145 | } 146 | if (!fileNamePattern) { 147 | throw new Error('appender must be constructed with a file name pattern'); 148 | } 149 | }()); 150 | 151 | 152 | // now validate the date pattern and file format 153 | // date may only contain YMDHAa-. 154 | 155 | if (autoOpen) { 156 | openWriter(); 157 | startRollTimer(); 158 | } 159 | }; 160 | 161 | module.exports = RollingFileAppender; 162 | -------------------------------------------------------------------------------- /lib/SimpleLogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class SimpleLogger 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 2014-07-06 6 | */ 7 | const dash = require('lodash'); 8 | const Logger = require('./Logger'); 9 | const ConsoleAppender = require('./ConsoleAppender'); 10 | const FileAppender = require('./FileAppender'); 11 | const RollingFileAppender = require('./RollingFileAppender'); 12 | 13 | const SimpleLogger = function(opts) { 14 | 'use strict'; 15 | 16 | const options = Object.assign({}, opts); 17 | 18 | const manager = this; 19 | const domain = options.domain; 20 | const appenders = options.appenders || []; 21 | const loggers = options.loggers || []; 22 | 23 | let dfltLevel = options.level || Logger.DEFAULT_LEVEL; 24 | let loggerConfigFile = options.loggerConfigFile; 25 | let refresh = options.refresh; 26 | let fs = options.fs || require('fs'); 27 | let createInterval = options.createInterval || setInterval; 28 | let minRefresh = options.minRefresh || 10 * 1000; 29 | let errorEventName = options.errorEventName; 30 | 31 | /** 32 | * create a logger with optional category and level 33 | * 34 | * @param category 35 | * @param level 36 | * @returns Logger 37 | */ 38 | this.createLogger = function(category, level) { 39 | const opts = Object.prototype.toString.call(category) === '[object String]' ? options : dash.merge({}, options, category); 40 | 41 | opts.category = dash.isString(category) ? category : opts.category; 42 | opts.level = level ? level : opts.level || dfltLevel; 43 | opts.appenders = appenders; 44 | 45 | if (errorEventName) { 46 | opts.errorEventName = errorEventName; 47 | } 48 | 49 | const logger = new Logger(opts); 50 | loggers.push(logger); 51 | 52 | return logger; 53 | }; 54 | 55 | /** 56 | * create the console appender and add it to the appenders list 57 | * 58 | * @param opts - appender settings 59 | * @returns ConsoleAppender - 60 | */ 61 | this.createConsoleAppender = function(opts) { 62 | return manager.addAppender(new ConsoleAppender(Object.assign({}, opts))); 63 | }; 64 | 65 | /** 66 | * create a file appender and add it to the appenders list 67 | * 68 | * @param opts 69 | * @returns a FileAppender object 70 | */ 71 | this.createFileAppender = function(opts) { 72 | if (!opts) { 73 | throw new Error('file appender must be created with log file path set in options'); 74 | } 75 | 76 | return manager.addAppender(new FileAppender(opts)); 77 | }; 78 | 79 | /** 80 | * create a rolling file appender and add it to the appender list 81 | * 82 | * @param opts 83 | * @returns the appender 84 | */ 85 | this.createRollingFileAppender = function(opts) { 86 | return manager.addAppender(new RollingFileAppender(opts)); 87 | }; 88 | 89 | /** 90 | * add the appender to list 91 | * 92 | * @param appender 93 | * @returns the new appender 94 | */ 95 | this.addAppender = function(appender) { 96 | appenders.push(appender); 97 | 98 | return appender; 99 | }; 100 | 101 | this.getAppenders = function() { 102 | return appenders; 103 | }; 104 | 105 | this.getLoggers = function() { 106 | return loggers; 107 | }; 108 | 109 | /** 110 | * start the refresh thread; minimum cycle time = 10 seconds... 111 | */ 112 | this.startRefreshThread = function() { 113 | // TODO replace with watcher thread 114 | if (fs.existsSync(loggerConfigFile) && dash.isNumber(refresh)) { 115 | const t = Math.max(minRefresh, refresh); 116 | createInterval(manager.readConfig, t); 117 | } 118 | }; 119 | 120 | /** 121 | * set the level of all loggers to the specified level 122 | * 123 | * @param level - one of the know levels 124 | */ 125 | this.setAllLoggerLevels = function(level) { 126 | loggers.forEach(function(logger) { 127 | logger.setLevel(level); 128 | }); 129 | }; 130 | 131 | /** 132 | * read and parse the config file; change settings if required 133 | */ 134 | this.readConfig = function(completeCallback) { 135 | // TODO refactor into configuration delegate to read stats and then process file only if stats change 136 | const callback = (err, buf) => { 137 | if (err) { 138 | /*eslint no-console: "off"*/ 139 | console.log(err); 140 | } else { 141 | const conf = JSON.parse(buf.toString()); 142 | if (conf.appenders && conf.appenders.length > 0) { 143 | // find each appender and set the level 144 | conf.appenders.forEach(function(app) { 145 | const level = app.level; 146 | 147 | const appender = dash.find(appenders, (item) => { 148 | if (item.getTypeName() === app.typeName && app.level) { 149 | return item; 150 | } 151 | }); 152 | 153 | if (appender && typeof appender.setLevel === 'function') { 154 | appender.setLevel(level); 155 | } 156 | }); 157 | } 158 | 159 | if (conf.loggers && conf.loggers.length > 0) { 160 | conf.loggers.forEach(item => { 161 | if (item.category === 'all') { 162 | manager.setAllLoggerLevels(item.level); 163 | } 164 | }); 165 | } 166 | } 167 | 168 | if (completeCallback) { 169 | return completeCallback(err); 170 | } 171 | }; 172 | 173 | fs.readFile(loggerConfigFile, callback); 174 | }; 175 | 176 | this.__protected = function() { 177 | return { 178 | domain: domain, 179 | dfltLevel: dfltLevel, 180 | refresh: refresh, 181 | loggerConfigFile: loggerConfigFile 182 | }; 183 | }; 184 | }; 185 | 186 | module.exports = SimpleLogger; 187 | 188 | /** 189 | * static convenience method to create a simple console logger; see options for details 190 | * 191 | * @param options - optional, if present then it could be 1) a string or 2) and object. if it's a string it's assumed 192 | * to be the logFilePath; if it's a string or an object with logFilePath property, then a file appender is created. 193 | * 194 | * Valid options: 195 | * - logFilePath : a path to the file appender 196 | * - domain : the logger domain, e.g., machine or site id 197 | * - dfltLevel : the default log level (overrides info level) 198 | * - timestampFormat : the format used for log entries (see moment date formats for all possibilities) 199 | * 200 | * @returns Logger 201 | */ 202 | SimpleLogger.createSimpleLogger = function(options) { 203 | 'use strict'; 204 | 205 | let opts; 206 | 207 | // if options is a string then it must be the 208 | if (typeof options === 'string') { 209 | opts = { 210 | logFilePath: options 211 | }; 212 | } else { 213 | opts = Object.assign({}, options); 214 | } 215 | 216 | const manager = new SimpleLogger(opts); 217 | 218 | // pass options in to change date formats, etc 219 | manager.createConsoleAppender(opts); 220 | 221 | if (opts.logFilePath) { 222 | manager.createFileAppender(opts); 223 | } 224 | 225 | return manager.createLogger(); 226 | }; 227 | 228 | /** 229 | * static convenience method to create a file logger (no console logging); 230 | * 231 | * @param options - if string then it's the logFilePath, else options with the logFilePath 232 | * @returns Logger 233 | */ 234 | SimpleLogger.createSimpleFileLogger = function(options) { 235 | 'use strict'; 236 | 237 | if (!options) { 238 | throw new Error('must create file logger with a logFilePath'); 239 | } 240 | 241 | let opts; 242 | 243 | // if options is a string then it must be the 244 | if (typeof options === 'string') { 245 | opts = { 246 | logFilePath: options 247 | }; 248 | } else { 249 | opts = Object.assign({}, options); 250 | } 251 | 252 | const manager = new SimpleLogger(opts); 253 | 254 | manager.createFileAppender(opts); 255 | 256 | return manager.createLogger(); 257 | }; 258 | 259 | /** 260 | * create a rolling file logger by passing options to SimpleLogger and Logger. this enables setting 261 | * of domain, category, etc. 262 | * 263 | * @param options 264 | * @returns rolling logger 265 | */ 266 | SimpleLogger.createRollingFileLogger = function(options) { 267 | 'use strict'; 268 | 269 | if (!options) { 270 | throw new Error('createRollingFileLogger requires configuration options for this constructor'); 271 | } 272 | 273 | let opts; 274 | 275 | // read a dynamic config file if available 276 | if (typeof options.readLoggerConfig === 'function') { 277 | opts = options.readLoggerConfig(); 278 | 279 | opts.readLoggerConfig = options.readLoggerConfig; 280 | } else { 281 | opts = options; 282 | } 283 | 284 | const manager = new SimpleLogger(opts); 285 | 286 | manager.createRollingFileAppender(opts); 287 | 288 | if (opts.refresh && opts.loggerConfigFile) { 289 | process.nextTick(manager.startRefreshThread); 290 | } 291 | 292 | return manager.createLogger(opts); 293 | }; 294 | 295 | /** 296 | * create a log manager 297 | * 298 | * @param options - file or rolling file specs; 299 | */ 300 | SimpleLogger.createLogManager = function(options) { 301 | 'use strict'; 302 | 303 | let opts; 304 | 305 | // read a dynamic config file if available 306 | if (options && typeof options.readLoggerConfig === 'function') { 307 | opts = options.readLoggerConfig(); 308 | 309 | opts.readLoggerConfig = options.readLoggerConfig; 310 | } else { 311 | opts = Object.assign({}, options); 312 | } 313 | 314 | const manager = new SimpleLogger(opts); 315 | 316 | if (opts.logDirectory && opts.fileNamePattern) { 317 | manager.createRollingFileAppender(opts); 318 | } 319 | 320 | // create at least one appender 321 | if (manager.getAppenders().length === 0) { 322 | manager.createConsoleAppender(opts); 323 | } 324 | 325 | return manager; 326 | }; 327 | -------------------------------------------------------------------------------- /logo.asc: -------------------------------------------------------------------------------- 1 | _____ _ _ _____ _ __ 2 | | __|_|_____ ___| |___ | | |___ _| |___ | | ___ ___ ___ ___ ___ 3 | |__ | | | . | | -_| | | | | . | . | -_| | |__| . | . | . | -_| _| 4 | |_____|_|_|_|_| _|_|___| |_|___|___|___|___| |_____|___|_ |_ |___|_| 5 | |_| |___|___| 6 | 7 | _____ __ _ __ __ __ 8 | / __(___ _ ___ / ___ / |/ ___ ___/ ___ / / ___ ___ ____ ____ ____ 9 | _\ \/ / ' \/ _ \/ / -_) / / _ / _ / -_) / /_/ _ / _ `/ _ `/ -_/ __/ 10 | /___/_/_/_/_/ .__/_/\__/ /_/|_/\___\_,_/\__/ /____\___\_, /\_, /\__/_/ 11 | /_/ /___//___/ 12 | 13 | ___ _ _ _ _ _ _ 14 | / __(_)_ __ _ __| |___ | \| |___ __| |___ | | ___ __ _ __ _ ___ _ _ 15 | \__ | | ' \| '_ | / -_) | .` / _ / _` / -_) | |__/ _ / _` / _` / -_| '_| 16 | |___|_|_|_|_| .__|_\___| |_|\_\___\__,_\___| |____\___\__, \__, \___|_| 17 | |_| |___/|___/ 18 | 19 | ___ _ _ _ _ _ _ 20 | / __<_._ _ _ ___| |___ | \ |___ _| |___ | | ___ ___ ___ ___ _ _ 21 | \__ | | ' ' | . | / ._> | / . / . / ._> | |_/ . / . / . / ._| '_> 22 | <___|_|_|_|_| _|_\___. |_\_\___\___\___. |___\___\_. \_. \___|_| 23 | |_| <___<___' 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-node-logger", 3 | "version": "21.8.12", 4 | "description": "A node console and file logger suitable for small, medium and large production projects.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/darrylwest/simple-node-logger.git" 9 | }, 10 | "scripts": { 11 | "test": "make test" 12 | }, 13 | "keywords": [ 14 | "log", 15 | "logger", 16 | "multi-appender", 17 | "file logger", 18 | "rolling file logger", 19 | "console logger" 20 | ], 21 | "dependencies": { 22 | "lodash": "^4.17.12", 23 | "moment": "^2.20.1" 24 | }, 25 | "devDependencies": { 26 | "chai": "^4.1.2", 27 | "eslint": "^6.0.1", 28 | "mocha": "^9.0.3", 29 | "random-fixture-data": "^2.0.17" 30 | }, 31 | "files": [ 32 | "index.js", 33 | "lib/", 34 | "test/mocks", 35 | "index.d.ts" 36 | ], 37 | "author": "darryl.west@raincitysoftware.com", 38 | "license": "Apache-2.0", 39 | "homepage": "https://github.com/darrylwest/simple-node-logger", 40 | "typings": "./index.d.ts" 41 | } 42 | -------------------------------------------------------------------------------- /test/AbstractAppenderTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class AbstractAppenderTests 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/7/14 6:27 PM 6 | */ 7 | const should = require('chai').should(); 8 | const dash = require('lodash'); 9 | const Logger = require('../lib/Logger'); 10 | const AbstractAppender = require('../lib/AbstractAppender'); 11 | 12 | describe('AbstractAppender', function() { 13 | 'use strict'; 14 | 15 | const createLogger = function(options) { 16 | const opts = Object.assign({}, options); 17 | 18 | opts.domain = 'MyDomain'; 19 | opts.category = 'MyCategory'; 20 | opts.level = 'debug'; 21 | 22 | return new Logger(opts); 23 | }; 24 | 25 | const createOptions = function(options) { 26 | const opts = Object.assign({}, options); 27 | 28 | opts.typeName = 'FooAppender'; 29 | 30 | return opts; 31 | }; 32 | 33 | describe('#instance', function() { 34 | const appender = new AbstractAppender(createOptions()); 35 | 36 | const methods = [ 37 | 'getTypeName', 38 | 'formatEntry', 39 | 'formatLevel', 40 | 'formatTimestamp', 41 | 'formatMessage', 42 | 'formatDate', 43 | 'formatObject' 44 | ]; 45 | 46 | it('should create an instance of AbstractAppender', function() { 47 | should.exist(appender); 48 | appender.should.be.instanceof(AbstractAppender); 49 | appender.getTypeName().should.equal('FooAppender'); 50 | }); 51 | 52 | it('should have all expected methods by size and type', function() { 53 | dash.functionsIn(appender).length.should.equal(methods.length); 54 | methods.forEach(function(method) { 55 | appender[method].should.be.a('function'); 56 | }); 57 | }); 58 | }); 59 | 60 | describe('formatEntry', function() { 61 | const appender = new AbstractAppender(createOptions()); 62 | const logger = createLogger(); 63 | 64 | it('should create and format fields for a specified log entry', function() { 65 | const entry = logger.createEntry('info', ['this is a test, time: ', new Date()]); 66 | const fields = appender.formatEntry(entry); 67 | 68 | should.exist(fields); 69 | fields.length.should.equal(5); 70 | }); 71 | }); 72 | 73 | describe('formatObject', function() { 74 | const appender = new AbstractAppender(createOptions()); 75 | 76 | it('should format a complex object into human readable output', function() { 77 | const list = [ 78 | { 79 | name: 'flarb', 80 | date: new Date() 81 | }, 82 | appender 83 | ]; 84 | 85 | list.forEach(function(obj) { 86 | const formatted = appender.formatObject(obj); 87 | 88 | // console.log( formatted ); 89 | formatted.should.be.a('string'); 90 | }); 91 | }); 92 | 93 | it('should format an error object with message and stack trace', function() { 94 | const err = new Error('this is my error'); 95 | const fmt = appender.formatObject(err); 96 | fmt.should.be.a('string'); 97 | fmt.should.contain('this is my error'); 98 | fmt.should.contain('at '); 99 | }); 100 | }); 101 | 102 | describe('formatMessage', function() { 103 | const appender = new AbstractAppender(createOptions()); 104 | 105 | it('should format a list of log messages', function() { 106 | const list = ['this is a test, time: ', new Date(), ' ', {name: 'flarb', date: new Date()}, ' ', appender]; 107 | 108 | const formatted = appender.formatMessage(list); 109 | 110 | // console.log( formatted ); 111 | should.exist(formatted); 112 | formatted.should.be.a('string'); 113 | }); 114 | }); 115 | 116 | describe('#timestampFormat', function() { 117 | const ts = 1428516587697; // 2015-04-08T18:09:47.697Z 118 | 119 | it('should have the default format', function() { 120 | const opts = createOptions(); 121 | const appender = new AbstractAppender(opts); 122 | const sdt = appender.formatTimestamp(ts); 123 | const parts = sdt.split('.'); 124 | 125 | // get this to pass for all timezones 126 | parts[0].split(':')[1].should.equal('09'); 127 | parts[0].split(':')[2].should.equal('47'); 128 | parts[1].should.equal('697'); 129 | 130 | // sdt.should.equal( '18:09:47.697'); 131 | }); 132 | 133 | it('should have a custom format from options', function() { 134 | const opts = { 135 | typeName: 'customerTSAppender', 136 | timestampFormat: 'x' // unix timestamp 137 | }; 138 | const appender = new AbstractAppender(opts); 139 | const sdt = appender.formatTimestamp(ts); 140 | 141 | sdt.should.equal(ts.toString()); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/ConsoleAppenderTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class ConsoleAppender 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/6/14 12:18 PM 6 | */ 7 | const should = require('chai').should(); 8 | const dash = require('lodash'); 9 | const Logger = require('../lib/Logger'); 10 | const ConsoleAppender = require('../lib/ConsoleAppender'); 11 | 12 | describe('ConsoleAppender', function() { 13 | 'use strict'; 14 | 15 | const createLogger = function(options) { 16 | const opts = Object.assign({}, options); 17 | 18 | opts.domain = 'MyDomain'; 19 | opts.category = 'MyCategory'; 20 | opts.level = 'debug'; 21 | 22 | return new Logger(opts); 23 | }; 24 | 25 | const createOptions = function(options) { 26 | const opts = Object.assign({}, options); 27 | 28 | opts.level = 'debug'; 29 | 30 | return opts; 31 | }; 32 | 33 | describe('#instance', function() { 34 | const appender = new ConsoleAppender(createOptions()); 35 | const methods = [ 36 | 'formatter', 37 | 'write', 38 | 'setLevel', 39 | 'getTypeName', 40 | 'formatEntry', 41 | 'formatLevel', 42 | 'formatTimestamp', 43 | 'formatMessage', 44 | 'formatDate', 45 | 'formatObject' 46 | ]; 47 | 48 | it('should create an instance of ConsoleAppender', function() { 49 | should.exist(appender); 50 | appender.should.be.instanceof(ConsoleAppender); 51 | appender.getTypeName().should.equal('ConsoleAppender'); 52 | }); 53 | 54 | it('should have all expected methods by size and type', function() { 55 | dash.functionsIn(appender).length.should.equal(methods.length); 56 | methods.forEach(function(method) { 57 | appender[method].should.be.a('function'); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('write/format', function() { 63 | const opts = createOptions(); 64 | const logger = createLogger(); 65 | 66 | it('should write a formatted entry', function(done) { 67 | opts.writer = function(str) { 68 | should.exist(str); 69 | 70 | // console.log( str ); 71 | 72 | str.should.contain('INFO'); 73 | str.should.contain(':'); 74 | 75 | done(); 76 | }; 77 | 78 | const appender = new ConsoleAppender(opts); 79 | const entry = logger.createEntry('info', ['this is a test, time: ', new Date()]); 80 | appender.write(entry); 81 | 82 | }); 83 | 84 | it('should skip log entries less than the specified level', function(done) { 85 | opts.writer = function(str) { 86 | should.not.exist(str); 87 | }; 88 | 89 | opts.level = 'fatal'; 90 | 91 | const appender = new ConsoleAppender(opts); 92 | const entry = logger.createEntry('info', ['this is a test, time: ', new Date()]); 93 | appender.write(entry); 94 | 95 | process.nextTick(function() { 96 | done(); 97 | }); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/FileAppenderTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/7/14 5:15 PM 6 | */ 7 | const should = require('chai').should(); 8 | const dash = require('lodash'); 9 | const Logger = require('../lib/Logger'); 10 | const FileAppender = require('../lib/FileAppender'); 11 | 12 | describe('FileAppender', function() { 13 | 'use strict'; 14 | 15 | const createLogger = function(options) { 16 | const opts = Object.assign({}, options); 17 | 18 | opts.domain = 'MyDomain'; 19 | opts.category = 'MyCategory'; 20 | opts.level = 'debug'; 21 | 22 | return new Logger(opts); 23 | }; 24 | 25 | const createOptions = function(options) { 26 | const opts = Object.assign({}, options); 27 | 28 | opts.level = 'debug'; 29 | opts.logFilePath = '/tmp/log-test.log'; 30 | opts.autoOpen = false; 31 | 32 | return opts; 33 | }; 34 | 35 | describe('#instance', function() { 36 | const appender = new FileAppender(createOptions()); 37 | const methods = [ 38 | 'formatter', 39 | 'write', 40 | 'setLevel', 41 | 'closeWriter', 42 | 'getTypeName', 43 | 'formatEntry', 44 | 'formatLevel', 45 | 'formatTimestamp', 46 | 'formatMessage', 47 | 'formatDate', 48 | 'formatObject' 49 | ]; 50 | 51 | it('should create an instance of FileAppender', function() { 52 | should.exist(appender); 53 | appender.should.be.instanceof(FileAppender); 54 | appender.getTypeName().should.equal('FileAppender'); 55 | }); 56 | 57 | it('should have all expected methods by size and type', function() { 58 | dash.functionsIn(appender).length.should.equal(methods.length); 59 | methods.forEach(function(method) { 60 | appender[method].should.be.a('function'); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('write/format', function() { 66 | const opts = createOptions(); 67 | const logger = createLogger(); 68 | 69 | it('should write a formatted entry', function(done) { 70 | opts.writer = {}; 71 | opts.writer.write = function(str) { 72 | should.exist(str); 73 | 74 | str.should.contain('INFO'); 75 | str.should.contain(':'); 76 | 77 | done(); 78 | }; 79 | 80 | const appender = new FileAppender(opts); 81 | const entry = logger.createEntry('info', ['this is a test, time: ', new Date()]); 82 | appender.write(entry); 83 | 84 | }); 85 | 86 | it('should skip log entries less than the specified level', function(done) { 87 | opts.writer = function(str) { 88 | should.not.exist(str); 89 | }; 90 | 91 | opts.level = 'fatal'; 92 | 93 | const appender = new FileAppender(opts); 94 | const entry = logger.createEntry('info', ['this is a test, time: ', new Date()]); 95 | appender.write(entry); 96 | 97 | process.nextTick(function() { 98 | done(); 99 | }); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/LoggerTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class LoggerTests 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/5/14 6:28 PM 6 | */ 7 | const should = require('chai').should(); 8 | const dash = require('lodash'); 9 | const randomData = require('random-fixture-data'); 10 | const Logger = require('../lib/Logger'); 11 | const MockAppender = require('./mocks/MockAppender'); 12 | 13 | describe('Logger', function() { 14 | 'use strict'; 15 | 16 | const createOptions = function(options) { 17 | const opts = Object.assign({}, options); 18 | 19 | opts.category = 'MyCat'; 20 | opts.appenders = [new MockAppender()]; 21 | 22 | return opts; 23 | }; 24 | 25 | describe('#instance', function() { 26 | const logger = new Logger(createOptions()); 27 | const methods = [ 28 | 'log', 29 | 'createEntry', 30 | 'setLevel', 31 | 'getLevel', 32 | 'setAppenders', 33 | 'addAppender', 34 | 'removeAppender', 35 | 'getAppenders', 36 | 'isDebug', 37 | 'isInfo', 38 | 'getCategory', 39 | 'getDomain', 40 | 'getStats', 41 | '__protected' 42 | ]; 43 | 44 | it('should create an instance of Logger', function() { 45 | should.exist(logger); 46 | logger.should.be.instanceof(Logger); 47 | }); 48 | 49 | it('should have all expected methods by size and type', function() { 50 | const allMethods = methods.concat(Logger.STANDARD_LEVELS); 51 | dash.functionsIn(logger).length.should.equal(allMethods.length); 52 | allMethods.forEach(function(method) { 53 | logger[method].should.be.a('function'); 54 | }); 55 | }); 56 | 57 | it('should have one appender', function() { 58 | const appenders = logger.getAppenders(); 59 | should.exist(appenders); 60 | appenders.length.should.equal(1); 61 | }); 62 | 63 | it('should have a category', function() { 64 | logger.getCategory().should.equal('MyCat'); 65 | }); 66 | }); 67 | 68 | describe('log', function() { 69 | const opts = createOptions(); 70 | 71 | opts.level = Logger.STANDARD_LEVELS[0]; // all 72 | opts.domain = 'MyApp'; 73 | opts.pid = 999; 74 | 75 | it('should respond to all log statements', function(done) { 76 | const logger = new Logger(opts); 77 | const appender = new MockAppender(); 78 | 79 | logger.setAppenders([appender]); 80 | 81 | logger.trace('this is a fallopia japonica test', {n: 'one'}); 82 | logger.debug(randomData.words(3)); 83 | logger.info(randomData.words(3)); 84 | logger.warn(randomData.words(3)); 85 | logger.error(randomData.words(3)); 86 | logger.fatal(randomData.words(3)); 87 | 88 | process.nextTick(function() { 89 | appender.entries.length.should.equal(6); 90 | 91 | done(); 92 | }); 93 | 94 | logger.getDomain().should.equal(opts.domain); 95 | }); 96 | 97 | it('should contain all entry attributes', function(done) { 98 | const logger = new Logger(opts); 99 | const appender = new MockAppender(); 100 | const text = randomData.sentence; 101 | 102 | logger.setAppenders([appender]); 103 | 104 | logger.info(text); 105 | 106 | process.nextTick(function() { 107 | appender.entries.length.should.equal(1); 108 | const entry = appender.entries[0]; 109 | 110 | // console.log( entry ); 111 | 112 | entry.ts.should.be.above(Date.now() - 1000); 113 | entry.pid.should.equal(opts.pid); 114 | entry.domain.should.equal(opts.domain); 115 | entry.category.should.equal(opts.category); 116 | entry.level.should.equal('info'); 117 | entry.msg.length.should.equal(1); 118 | entry.msg[0].should.equal(text); 119 | 120 | done(); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('#isLevel', function() { 126 | it('should report isDebug as true if at or below debug', function() { 127 | const log = new Logger(createOptions()); 128 | 129 | log.setLevel('debug'); 130 | log.getLevel().should.equal('debug'); 131 | log.isDebug().should.equal(true); 132 | log.setLevel('trace'); 133 | log.isDebug().should.equal(true); 134 | log.setLevel('all'); 135 | log.isDebug().should.equal(true); 136 | 137 | }); 138 | it('should report isDebug as false if above debug', function() { 139 | const log = new Logger(createOptions()); 140 | 141 | log.setLevel('info'); 142 | log.getLevel().should.equal('info'); 143 | log.isDebug().should.equal(false); 144 | 145 | }); 146 | 147 | it('should report isInfo as true if at or below info', function() { 148 | const log = new Logger(createOptions()); 149 | 150 | log.setLevel('info'); 151 | log.getLevel().should.equal('info'); 152 | log.isInfo().should.equal(true); 153 | log.setLevel('trace'); 154 | log.isDebug().should.equal(true); 155 | log.setLevel('all'); 156 | log.isDebug().should.equal(true); 157 | }); 158 | 159 | it('should report isInfo as false if above info', function() { 160 | const log = new Logger(createOptions()); 161 | 162 | log.setLevel('warn'); 163 | log.getLevel().should.equal('warn'); 164 | log.isInfo().should.equal(false); 165 | }); 166 | 167 | }); 168 | 169 | describe('errorEventHandler', function() { 170 | it('should emit a process error event when configured for events', function(done) { 171 | const opts = createOptions(); 172 | opts.errorEventName = 'myerrortrap'; 173 | const log = new Logger(opts); 174 | process.on(opts.errorEventName, (msg) => { 175 | should.exist(msg); 176 | msg.category.should.equal('MyCat'); 177 | msg.level.should.equal('error'); 178 | msg.msg.pop().should.equal('my error trap thing'); 179 | done(); 180 | }); 181 | 182 | log.info('this is a test'); 183 | log.warn('anhter'); 184 | log.error('my error trap thing'); 185 | }); 186 | 187 | it('should not emit a process error event when not configured for events', function(done) { 188 | const log = new Logger(createOptions()); 189 | process.on('error', (msg) => { 190 | should.not.exist(msg); 191 | }); 192 | 193 | log.info('this is a test'); 194 | log.warn('anhter'); 195 | log.error('my error trap thing'); 196 | 197 | setTimeout(() => { 198 | done(); 199 | }, 30); 200 | }); 201 | }); 202 | 203 | describe('stats', function() { 204 | it('should report stats with counts for each level', function() { 205 | const log = new Logger(createOptions()); 206 | let stats = log.getStats(); 207 | 208 | should.exist(stats); 209 | Logger.STANDARD_LEVELS.forEach(lvl => { 210 | stats.get(lvl).should.equal(0); 211 | }); 212 | 213 | log.debug('this is one'); 214 | log.getStats().get('debug').should.equal(1); 215 | log.info('this is one'); 216 | log.getStats().get('info').should.equal(1); 217 | log.warn('this is one'); 218 | log.getStats().get('warn').should.equal(1); 219 | log.error('this is one'); 220 | log.getStats().get('error').should.equal(1); 221 | }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /test/RollingFileAppenderTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/27/14 9:53 AM 6 | */ 7 | const should = require('chai').should(); 8 | const dash = require('lodash'); 9 | const path = require('path'); 10 | const moment = require('moment'); 11 | const RollingFileAppender = require('../lib/RollingFileAppender'); 12 | 13 | describe('RollingFileAppender', function() { 14 | 'use strict'; 15 | 16 | const createOptions = function() { 17 | const opts = {}; 18 | 19 | opts.level = 'debug'; 20 | opts.logDirectory = '/tmp/'; 21 | opts.fileNamePattern = 'app-.log'; 22 | opts.autoOpen = false; 23 | 24 | return opts; 25 | }; 26 | 27 | describe('#instance', function() { 28 | const appender = new RollingFileAppender(createOptions()); 29 | const methods = [ 30 | 'formatter', 31 | 'write', 32 | 'setLevel', 33 | 'checkForRoll', 34 | 'createFileName', 35 | '__protected', 36 | 'getTypeName', 37 | 'formatEntry', 38 | 'formatLevel', 39 | 'formatTimestamp', 40 | 'formatMessage', 41 | 'formatDate', 42 | 'formatObject' 43 | ]; 44 | 45 | it('should create an instance of RollingFileAppender', function() { 46 | should.exist(appender); 47 | appender.should.be.instanceof(RollingFileAppender); 48 | appender.getTypeName().should.equal('RollingFileAppender'); 49 | 50 | const p = appender.__protected(); 51 | should.exist(p); 52 | p.writers.length.should.equal(0); 53 | p.openWriter.should.be.a('function'); 54 | }); 55 | 56 | it('should have all expected methods by size and type', function() { 57 | dash.functionsIn(appender).length.should.equal(methods.length); 58 | methods.forEach(function(method) { 59 | appender[method].should.be.a('function'); 60 | }); 61 | }); 62 | 63 | it('should check openWriter can open a new file with default createFileName', function() { 64 | const p = appender.__protected(); 65 | p.openWriter.should.be.a('function'); 66 | p.writers.length.should.equal(0); 67 | const openWriter = p.openWriter; 68 | openWriter.should.be.a('function'); 69 | openWriter(); 70 | p.writers.length.should.equal(1); 71 | }); 72 | 73 | it('should check openWriter can open a new file with filename passed in'); 74 | }); 75 | 76 | describe('checkForRoll', function() { 77 | const opts = createOptions(); 78 | 79 | opts.dateFormat = 'YYYY.MM.DD'; 80 | 81 | it('should return false when the date stays within the same day', function() { 82 | let now = moment('2014-01-01T00:00:00'); 83 | let appender; 84 | const fn = opts.fileNamePattern.replace(//i, now.format(opts.dateFormat)); 85 | 86 | opts.currentFile = path.join(process.env.HOME, fn); 87 | appender = new RollingFileAppender(opts); 88 | const p = appender.__protected(); 89 | 90 | should.exist(p); 91 | 92 | appender.checkForRoll(now).should.equal(false); 93 | 94 | // now add a second 95 | now = now.add(1, 's'); 96 | appender.checkForRoll(now).should.equal(false); 97 | 98 | // now add a few hours 99 | now = now.add(4, 'h'); 100 | appender.checkForRoll(now).should.equal(false); 101 | }); 102 | 103 | it('should return true when the day changes', function() { 104 | let now = moment(); 105 | let appender; 106 | const fn = opts.fileNamePattern.replace(//i, now.format(opts.dateFormat)); 107 | 108 | opts.currentFile = path.join(process.env.HOME, fn); 109 | appender = new RollingFileAppender(opts); 110 | const p = appender.__protected(); 111 | 112 | should.exist(p); 113 | 114 | // now add a few hours 115 | now = now.add(1, 'day'); 116 | appender.checkForRoll(now).should.equal(true); 117 | }); 118 | 119 | }); 120 | 121 | describe('createFileName', function() { 122 | const opts = createOptions(); 123 | const now = moment('2014-02-06T18:00Z').utc(); 124 | const patterns = [ 125 | 'YY.MM.DD', 126 | 'YYYY.MM.DD.HH', 127 | 'YYYY.MM.DD-a', 128 | 'YYYYMMDD', 129 | 'MMM-DD' 130 | ]; 131 | const expected = [ 132 | 'app-14.02.06.log', 133 | 'app-2014.02.06.18.log', 134 | 'app-2014.02.06-pm.log', 135 | 'app-20140206.log', 136 | 'app-Feb-06.log' 137 | ]; 138 | 139 | it('should create a filename based on known pattern and date', function() { 140 | patterns.forEach((pattern, idx) => { 141 | opts.dateFormat = pattern; 142 | const appender = new RollingFileAppender(opts); 143 | const fn = appender.createFileName(now); 144 | fn.should.equal(expected[idx]); 145 | }); 146 | }); 147 | }); 148 | 149 | }); 150 | -------------------------------------------------------------------------------- /test/SimpleLoggerTests.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class SimpleLoggerTests 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/7/14 9:44 AM 6 | */ 7 | const should = require('chai').should(); 8 | const dash = require('lodash'); 9 | const fs = require('fs'); 10 | const Logger = require('../lib/Logger'); 11 | const SimpleLogger = require('../lib/SimpleLogger'); 12 | const MockAppender = require('./mocks/MockAppender'); 13 | 14 | describe('SimpleLogger', function() { 15 | 'use strict'; 16 | 17 | const createOptions = function() { 18 | return {}; 19 | }; 20 | 21 | describe('#instance', function() { 22 | const manager = new SimpleLogger(createOptions()); 23 | const methods = [ 24 | 'createLogger', 25 | 'createConsoleAppender', 26 | 'createFileAppender', 27 | 'createRollingFileAppender', 28 | 'addAppender', 29 | 'getAppenders', 30 | 'getLoggers', 31 | 'setAllLoggerLevels', 32 | 'startRefreshThread', 33 | 'readConfig', 34 | '__protected' 35 | ]; 36 | 37 | it('should create an instance of SimpleLogger', function() { 38 | should.exist(manager); 39 | manager.should.be.instanceof(SimpleLogger); 40 | 41 | manager.getAppenders().length.should.equal(0); 42 | manager.getLoggers().length.should.equal(0); 43 | 44 | const p = manager.__protected(); 45 | 46 | should.exist(p); 47 | p.dfltLevel.should.equal('info'); 48 | }); 49 | 50 | it('should have all expected methods by size and type', function() { 51 | dash.functionsIn(manager).length.should.equal(methods.length); 52 | methods.forEach(function(method) { 53 | manager[method].should.be.a('function'); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('createLogger', function() { 59 | const manager = new SimpleLogger(createOptions()); 60 | 61 | it('should create a basic logger with console appender', function() { 62 | const log = manager.createLogger('MyCategory', 'warn'); 63 | 64 | should.exist(log); 65 | log.__protected().category.should.equal('MyCategory'); 66 | log.getLevel().should.equal('warn'); 67 | 68 | log.should.be.instanceof(Logger); 69 | }); 70 | }); 71 | 72 | describe('#domain', function() { 73 | const opts = createOptions(); 74 | 75 | opts.domain = 'MyDomain'; 76 | opts.level = 'error'; 77 | 78 | const manager = new SimpleLogger(opts); 79 | 80 | it('should create a simple logger with a domain', function() { 81 | const p = manager.__protected(); 82 | p.domain.should.equal(opts.domain); 83 | p.dfltLevel.should.equal(opts.level); 84 | }); 85 | 86 | it('should create a log with specified domain, category and level', function() { 87 | const log = manager.createLogger('MyCat'); 88 | 89 | log.getLevel().should.equal(opts.level); 90 | log.__protected().domain.should.equal(opts.domain); 91 | 92 | // default to a single console appender 93 | log.getAppenders().length.should.equal(0); 94 | }); 95 | }); 96 | 97 | describe('addAppender', function() { 98 | const manager = new SimpleLogger(createOptions()); 99 | 100 | it('should add a new appender to the list', function() { 101 | manager.getAppenders().length.should.equal(0); 102 | 103 | const appender = manager.addAppender(new MockAppender()); 104 | 105 | should.exist(appender); 106 | appender.should.be.instanceof(MockAppender); 107 | manager.getAppenders().length.should.equal(1); 108 | }); 109 | }); 110 | 111 | describe('createConsoleAppender', function() { 112 | const manager = new SimpleLogger(createOptions()); 113 | 114 | it('should create a new console appender and add it to the appenders list', function() { 115 | const appender = manager.createConsoleAppender(); 116 | should.exist(appender); 117 | manager.getAppenders().length.should.equal(1); 118 | }); 119 | }); 120 | 121 | describe('createFileAppender', function() { 122 | const manager = new SimpleLogger(createOptions()); 123 | 124 | it('should create a new file appender and add it to the appenders list', function() { 125 | const appender = manager.createFileAppender({logFilePath: '/dev/null'}); 126 | should.exist(appender); 127 | manager.getAppenders().length.should.equal(1); 128 | }); 129 | }); 130 | 131 | describe('createRollingFileAppender', function() { 132 | const manager = new SimpleLogger(createOptions()); 133 | 134 | it('should create a new rolling file appender and add it to the appenders list', function() { 135 | const opts = {}; 136 | 137 | opts.level = 'debug'; 138 | opts.logDirectory = process.env.HOME + '/logs'; 139 | opts.fileNamePattern = 'app-.log'; 140 | opts.autoOpen = false; 141 | 142 | const appender = manager.createRollingFileAppender(opts); 143 | should.exist(appender); 144 | manager.getAppenders().length.should.equal(1); 145 | }); 146 | }); 147 | 148 | describe('startRefreshThread', function() { 149 | const opts = createOptions(); 150 | 151 | opts.loggerConfigFile = __dirname + '/fixtures/logger-config.json'; 152 | opts.refresh = 2000; 153 | 154 | const manager = new SimpleLogger(opts); 155 | 156 | it('should start refresh thread if config file and refresh are set', function(done) { 157 | manager.startRefreshThread = function() { 158 | if (fs.existsSync(opts.loggerConfigFile) && dash.isNumber(opts.refresh)) { 159 | // console.log('file: ', opts.loggerConfigFile ); 160 | const obj = JSON.parse(fs.readFileSync(opts.loggerConfigFile)); 161 | should.exist(obj); 162 | 163 | done(); 164 | } else { 165 | /*eslint no-console: "off"*/ 166 | console.log(opts.refresh); 167 | console.log('file: ', opts.loggerConfigFile, ' does not exist?'); 168 | } 169 | }; 170 | 171 | process.nextTick(manager.startRefreshThread); 172 | }); 173 | }); 174 | 175 | describe('readConfig', function() { 176 | const opts = createOptions(); 177 | 178 | opts.loggerConfigFile = __dirname + '/fixtures/logger-config.json'; 179 | opts.refresh = 2000; 180 | 181 | const manager = new SimpleLogger(opts); 182 | 183 | it('should read and parse a valid configuration file', function(done) { 184 | const callback = function(err) { 185 | should.not.exist(err); 186 | 187 | // TODO test the appenders to see if at the correct level 188 | 189 | // TODO test the loggers to see if at the correct level 190 | 191 | done(); 192 | }; 193 | 194 | manager.readConfig(callback); 195 | }); 196 | }); 197 | 198 | describe('createSimpleLogger', function() { 199 | it('should create a simple logger with a single console adapter when invoked with null options', function() { 200 | const log = SimpleLogger.createSimpleLogger(); 201 | 202 | should.exist(log); 203 | log.getLevel().should.equal('info'); 204 | const appenders = log.getAppenders(); 205 | appenders.length.should.equal(1); 206 | appenders[0].getTypeName().should.equal('ConsoleAppender'); 207 | }); 208 | 209 | it('should create a simple logger with a single console adapter when invoked with format options', function() { 210 | const opts = { 211 | timestampFormat: 'x' // unix timestamp 212 | }; 213 | const log = SimpleLogger.createSimpleLogger(opts); 214 | 215 | should.exist(log); 216 | log.getLevel().should.equal('info'); 217 | const appenders = log.getAppenders(); 218 | appenders.length.should.equal(1); 219 | const appender = appenders[0]; 220 | appender.getTypeName().should.equal('ConsoleAppender'); 221 | const dt = new Date('2017-05-01T01:01:01Z'); 222 | appender.formatTimestamp(dt).should.equal('1493600461000'); 223 | }); 224 | 225 | it('should create a simple logger with console and file appenders when invoked with a filename', function() { 226 | const log = SimpleLogger.createSimpleLogger('/tmp/mytmpfile.log'); 227 | 228 | should.exist(log); 229 | log.getLevel().should.equal('info'); 230 | const appenders = log.getAppenders(); 231 | appenders.length.should.equal(2); 232 | appenders[0].getTypeName().should.equal('ConsoleAppender'); 233 | appenders[1].getTypeName().should.equal('FileAppender'); 234 | }); 235 | }); 236 | }); 237 | -------------------------------------------------------------------------------- /test/fixtures/logger-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders":[ 3 | { 4 | "typeName":"RollingFileAppender", 5 | "level":"debug" 6 | }, 7 | { 8 | "typeName":"ConsoleAppender", 9 | "level":"fatal" 10 | } 11 | ], 12 | "loggers":[ 13 | { 14 | "category":"ApplicationFactory", 15 | "level":"warn" 16 | }, 17 | { 18 | "category":"CalculationDelegate", 19 | "level":"debug" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/integration/run-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # dpw@seattle.local 3 | # 2017.05.03 4 | # 5 | 6 | pwd=`pwd` 7 | examples="category-logger.js custom-timestamp.js domain-logger.js dynamic-rolling-logger.js hourly-logger.js json-appender.js json-file-logger.js log-manager.js rolling-logger.js simple-file-logger.js simple-logger.js" 8 | 9 | for ex in $examples 10 | do 11 | fn="${pwd}/examples/${ex}" 12 | ls $fn 13 | $fn 14 | done 15 | 16 | -------------------------------------------------------------------------------- /test/mocks/MockAppender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class MockAppender 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/6/14 8:41 AM 6 | */ 7 | const MockAppender = function() { 8 | 'use strict'; 9 | const Logger = require('../../lib/Logger' ); 10 | 11 | let level = Logger.DEFAULT_LEVEL; 12 | let levels = Logger.STANDARD_LEVELS; 13 | let currentLevel = levels.indexOf( level ); 14 | 15 | let appender = this; 16 | 17 | this.entries = []; 18 | 19 | this.setLevel = function(level) { 20 | let idx = levels.indexOf( level ); 21 | if (idx >= 0) { 22 | currentLevel = idx; 23 | } 24 | }; 25 | 26 | this.getCurrentLevel = function() { 27 | return currentLevel; 28 | }; 29 | 30 | this.write = function(entry) { 31 | appender.entries.push( entry ); 32 | }; 33 | }; 34 | 35 | module.exports = MockAppender; 36 | -------------------------------------------------------------------------------- /test/mocks/MockLogger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class MockLogger 3 | * 4 | * @author: darryl.west@raincitysoftware.com 5 | * @created: 7/8/14 5:16 PM 6 | */ 7 | const dash = require('lodash'); 8 | const Logger = require('../../lib/Logger'); 9 | const MockAppender = require('./MockAppender'); 10 | 11 | const MockLogger = function(options) { 12 | 'use strict'; 13 | 14 | const opts = Object.assign({}, options); 15 | 16 | // const mock = this; 17 | const appender = new MockAppender(); 18 | 19 | // set these if not passed in 20 | if (!opts.pid) { 21 | opts.pid = 'test12345'; 22 | } 23 | if (!opts.appenders) { 24 | opts.appenders = [appender]; 25 | } 26 | if (!opts.level) { 27 | opts.level = 'trace'; 28 | } 29 | 30 | dash.extend(this, new Logger(opts)); 31 | 32 | this.getLogEntries = function() { 33 | return appender.entries; 34 | }; 35 | }; 36 | 37 | MockLogger.createLogger = function(category, level) { 38 | 'use strict'; 39 | 40 | const opts = {}; 41 | 42 | if (category) { 43 | opts.category = category; 44 | } 45 | if (level) { 46 | opts.level = level; 47 | } 48 | 49 | return new MockLogger(opts); 50 | }; 51 | 52 | module.exports = MockLogger; 53 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # Simple Node Logger To Do List 2 | - - - 3 | 4 | ## Before next release 5 | 6 | * create functional tests to exersize all static methods 7 | * insure 100% code coverage 8 | 9 | ## General 10 | 11 | * add file purger for rolling file logger 12 | * add logic to archive logs to S3 13 | * bump version to 1.0 14 | * modify abstract appender to output dates and objects with minimal formatting 15 | * add dynamic config capability; invoke conf.readLoggerConfig each 2 minutes 16 | * modify SimpleLogger to act as a log manager capable of managing multiple loggers 17 | 18 | ## Documentation 19 | 20 | 21 | - - - 22 |

last updated March 26, 2017

23 | -------------------------------------------------------------------------------- /watcher.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // dpw@alameda.local 4 | // 2015.03.04 5 | 'use strict'; 6 | 7 | const fs = require('fs'); 8 | const spawn = require('child_process').spawn; 9 | const clearScreen = ''; 10 | 11 | let files = new Set(); 12 | let tid; 13 | 14 | const run = function() { 15 | process.stdout.write( clearScreen ); 16 | console.log('Changed files: ', files); 17 | 18 | let runner = spawn( 'make', [ 'test' ] ); 19 | 20 | runner.stdout.on('data', function( data ) { 21 | process.stdout.write( data ); 22 | }); 23 | 24 | runner.stderr.on('data', function( data ) { 25 | process.stdout.write( data ); 26 | }); 27 | 28 | runner.on('close', function(code) { 29 | tid = null; 30 | files.clear(); 31 | }); 32 | }; 33 | 34 | const changeHandler = function(event, filename) { 35 | if ( filename.endsWith('.js') ) { 36 | files.add( filename ); 37 | 38 | if (!tid) { 39 | tid = setTimeout(function() { 40 | run(); 41 | }, 250); 42 | } 43 | } 44 | }; 45 | 46 | // run(); 47 | fs.watch( './lib', { recursive:true }, changeHandler ); 48 | fs.watch( './test', { recursive:true }, changeHandler ); 49 | 50 | --------------------------------------------------------------------------------