├── .gitignore ├── .travis.yml ├── example.js ├── History.md ├── Makefile ├── package.json ├── LICENSE ├── README.md ├── index.js └── test └── index.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | coverage.html 17 | dump.rdb 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "7" 5 | - "6" 6 | - "4" 7 | - "0.12" 8 | script: "make test-travis" 9 | after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" 10 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var Logger = require('./'); 2 | 3 | var logger = Logger({ 4 | dir: __dirname + '/logs', 5 | stdout: true, 6 | mkdir: true 7 | }); 8 | 9 | logger.error(new Error('test')); 10 | 11 | // 2014-07-09 09:48:36.951 nodejs.ErrorException: Error: test 12 | // at Object. (/Users/deadhorse/git/mini-logger/example.js:9:14) 13 | // at Module._compile (module.js:449:26) 14 | // at Object.Module._extensions..js (module.js:467:10) 15 | // at Module.load (module.js:349:32) 16 | // at Function.Module._load (module.js:305:12) 17 | // at Function.Module.runMain (module.js:490:10) 18 | // at startup (node.js:124:16) 19 | // at node.js:803:3 20 | // pid: 21588 21 | // domainThrown: false 22 | // Host: dead-horsedeMacBook-Pro.local 23 | // URL: 24 | // Data: "" 25 | // 2014-07-09 09:48:36.951 26 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 1.1.3 / 2017-05-19 3 | ================== 4 | 5 | * fix: package.json to reduce vulnerabilities (#6) 6 | 7 | 1.1.2 / 2017-03-24 8 | ================== 9 | 10 | * no need dir if no file logger (#5) 11 | 12 | 1.1.1 / 2016-06-26 13 | ================== 14 | 15 | * chore: add snyk badge for security check (#4) 16 | 17 | 1.1.0 / 2015-12-11 18 | ================== 19 | 20 | * add timestamp options 21 | 22 | 1.0.0 / 2014-09-25 23 | ================== 24 | 25 | * add coverage 26 | * use sdk-base 27 | * error-formatter 28 | 29 | 0.3.0 / 2014-07-11 30 | ================== 31 | 32 | * support seperator 33 | 34 | 0.2.0 / 2014-07-10 35 | ================== 36 | 37 | * support JSON.stringify(Object) 38 | * fix repo info. 39 | * remove 0.8 40 | * add badge 41 | 42 | 0.1.0 / 2014-07-09 43 | ================== 44 | 45 | * add readme 46 | * init 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TESTS = test/*.test.js 2 | REPORTER = spec 3 | TIMEOUT = 3000 4 | MOCHA_OPTS = 5 | 6 | test: 7 | @NODE_ENV=test ./node_modules/mocha/bin/mocha \ 8 | --harmony \ 9 | --reporter $(REPORTER) \ 10 | --timeout $(TIMEOUT) \ 11 | --require should \ 12 | $(MOCHA_OPTS) \ 13 | $(TESTS) 14 | 15 | test-cov: 16 | @NODE_ENV=test node \ 17 | node_modules/.bin/istanbul cover \ 18 | ./node_modules/.bin/_mocha \ 19 | -- -u exports \ 20 | --reporter $(REPORTER) \ 21 | --timeout $(TIMEOUT) \ 22 | --require should \ 23 | $(MOCHA_OPTS) \ 24 | $(TESTS) 25 | 26 | test-travis: 27 | @NODE_ENV=test node \ 28 | node_modules/.bin/istanbul cover \ 29 | ./node_modules/.bin/_mocha \ 30 | --report lcovonly \ 31 | -- -u exports \ 32 | --reporter $(REPORTER) \ 33 | --timeout $(TIMEOUT) \ 34 | --require should \ 35 | $(MOCHA_OPTS) \ 36 | $(TESTS) 37 | 38 | .PHONY: test 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-logger", 3 | "version": "1.1.3", 4 | "description": "A really simple logger for web server or others", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "keywords": [ 10 | "logger", 11 | "error", 12 | "custom", 13 | "stream", 14 | "file" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:node-modules/mini-logger.git" 19 | }, 20 | "author": "dead_horse ", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "istanbul": "^0.3.0", 24 | "mkdirp": "^0.5.0", 25 | "mm": "^0.2.1", 26 | "mocha": "2", 27 | "rimraf": "2", 28 | "should": "4" 29 | }, 30 | "dependencies": { 31 | "utility": "^1.8.0", 32 | "copy-to": "^1.0.1", 33 | "error-formatter": "^1.0.3", 34 | "iconv-lite": "~0.4.4", 35 | "logfilestream": "^1.0.1", 36 | "ms": "^2.0.0", 37 | "sdk-base": "^1.0.0" 38 | }, 39 | "files": [ 40 | "index.js" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 dead_horse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mini-logger 2 | ------------ 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![build status][travis-image]][travis-url] 6 | [![Coveralls][coveralls-image]][coveralls-url] 7 | [![David deps][david-image]][david-url] 8 | [![Known Vulnerabilities][snyk-image]][snyk-url] 9 | [![npm download][download-image]][download-url] 10 | 11 | [npm-image]: https://img.shields.io/npm/v/mini-logger.svg?style=flat-square 12 | [npm-url]: https://npmjs.org/package/mini-logger 13 | [travis-image]: https://img.shields.io/travis/node-modules/mini-logger.svg?style=flat-square 14 | [travis-url]: https://travis-ci.org/node-modules/mini-logger 15 | [coveralls-image]: https://img.shields.io/coveralls/node-modules/mini-logger.svg?style=flat-square 16 | [coveralls-url]: https://coveralls.io/r/node-modules/mini-logger?branch=master 17 | [david-image]: https://img.shields.io/david/node-modules/mini-logger.svg?style=flat-square 18 | [david-url]: https://david-dm.org/node-modules/mini-logger 19 | [snyk-image]: https://snyk.io/test/npm/mini-logger/badge.svg?style=flat-square 20 | [snyk-url]: https://snyk.io/test/npm/mini-logger 21 | [download-image]: https://img.shields.io/npm/dm/mini-logger.svg?style=flat-square 22 | [download-url]: https://npmjs.org/package/mini-logger 23 | 24 | A really simple logger for web server or others. 25 | 26 | ## Install 27 | 28 | ```bash 29 | npm install mini-logger 30 | ``` 31 | 32 | ## Goal 33 | 34 | Log levels for logger is useless. Just let [debug](https://github.com/visionmedia/debug) module handle the debug log. All you need is error log and some custom categories. `mini-logger` just write logs into different files or stdout, do not care formats(only format Errors and Objects). 35 | 36 | ## Features 37 | 38 | * rolling log files based on datetime 39 | * easy to extended 40 | * custom categories 41 | * encoding support 42 | * support Error / Object format 43 | 44 | ## Usage 45 | 46 | ### Example 47 | 48 | ```js 49 | var path = require('path'); 50 | var Logger = require('mini-logger'); 51 | var logger = Logger({ 52 | dir: path.join(__dirname, 'logs'), 53 | categories: [ 'http' ], 54 | format: '[{category}.]YYYY-MM-DD[.log]' 55 | }); 56 | 57 | logger.error(new Error('error')); 58 | logger.http('http request url: %s', 'https://github.com'); 59 | ``` 60 | 61 | ### Options 62 | 63 | * **dir**: log directory path, required. 64 | * **categories**: custom categories, all categories will add a method to Logger's instance 65 | * **format**: log file name's format, will pase to momentjs to format. `{category}` will replace with logger category, default is `[{category.}]YYYY-MM-DD[.log]` 66 | * **stdout**: write logs into stdout, default is `false` 67 | * **file**: write logs into file, default is `true` 68 | * **errorFormater**: formater for errors, default is [error-formater](https://github.com/node-modules/error-formater) 69 | * **seperator**: the seperator of each line of logs, default is `os.EOL + os.EOL` 70 | * **encoding**: output logs' encoding, default is `utf-8` 71 | * **flushInterval**: all logs will cache in memory first, every `flushInterval` ms flush into files. default is `1s` 72 | * **duration**: cut the logs every `duration` ms. default is `1h` 73 | * **mkdir**: everytime before create a writeStream, will try to `mkdirp` first. useful when format is like `YYYY/MM/DD/[{category}.log]`, default to false 74 | * **timestamp**: write timestamp with format YYYYMMDDHHmmssSSS before every line of logs, default is false 75 | 76 | ### Events 77 | 78 | Logger will emit an error event when any write streams emit an error. If you don't listen this `error` event, it will default hanlde by: 79 | 80 | ```js 81 | function onerror(err) { 82 | console.error(err.stack); 83 | } 84 | ``` 85 | 86 | ## License 87 | 88 | [MIT](LICENSE) 89 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var logfilestream = require('logfilestream'); 4 | var formatter = require('error-formatter'); 5 | var Base = require('sdk-base'); 6 | var assert = require('assert'); 7 | var copy = require('copy-to'); 8 | var util = require('util'); 9 | var ms = require('ms'); 10 | var os = require('os'); 11 | var YYYYMMDDHHmmssSSS = require('utility').YYYYMMDDHHmmssSSS; 12 | 13 | var SEPERATOR = os.EOL + os.EOL; 14 | 15 | /** 16 | * Expose `Logger` 17 | */ 18 | 19 | module.exports = Logger; 20 | 21 | var defaultOptions = { 22 | categories: [], 23 | format: '[{category}.]YYYY-MM-DD[.log]', 24 | stdout: false, 25 | file: true, 26 | errorFormatter: formatter, 27 | seperator: SEPERATOR, 28 | timestamp: false 29 | } 30 | 31 | function Logger(options) { 32 | if (!(this instanceof Logger)) return new Logger(options); 33 | 34 | assert(options, 'options required'); 35 | Base.call(this); 36 | 37 | this._options = {}; 38 | copy(options).and(defaultOptions).to(this._options); 39 | options = this._options; 40 | 41 | if (options.file) { 42 | assert(options.dir, 'options.dir required'); 43 | } 44 | 45 | if (!Array.isArray(options.categories)) options.categories = [ options.categories ]; 46 | options.categories.push('error'); 47 | options.categories = uniq(options.categories); 48 | 49 | options.duration = typeof options.duration === 'string' 50 | ? ms(options.duration) 51 | : options.duration; 52 | 53 | options.flushInterval = typeof options.flushInterval === 'string' 54 | ? ms(options.flushInterval) 55 | : options.flushInterval; 56 | 57 | options.encoding = (options.encoding || 'utf-8').toLowerCase(); 58 | if (options.encoding === 'utf8') options.encoding = 'utf-8'; 59 | 60 | this._init(); 61 | } 62 | 63 | util.inherits(Logger, Base); 64 | 65 | Logger.prototype._init = function() { 66 | var ctx = this; 67 | 68 | // create log functions 69 | this._options.categories.forEach(function (category) { 70 | ctx[category] = function (msg) { 71 | msg = (msg instanceof Error) 72 | ? msg = ctx._options.errorFormatter(msg) 73 | : typeof msg === 'object' 74 | ? JSON.stringify(msg) 75 | : util.format.apply(util, arguments); 76 | msg += ctx._options.seperator; 77 | 78 | ctx._write(category, msg); 79 | }; 80 | }); 81 | this._streams = {}; 82 | 83 | if (!this._options.file) return; 84 | // create log file streams 85 | this._options.categories.forEach(function (category) { 86 | var format = ctx._options.format.replace(/\{category\}/g, category); 87 | var stream = logfilestream({ 88 | logdir: ctx._options.dir, 89 | duration: ctx._options.duration, 90 | nameformat: format, 91 | mkdir: ctx._options.mkdir, 92 | buffer: ctx._options.flushInterval, 93 | mode: ctx._options.mode, 94 | encoding: ctx._options.encoding 95 | }); 96 | 97 | stream.on('error', ctx.emit.bind(ctx, 'error')); 98 | ctx._streams[category] = stream; 99 | }); 100 | }; 101 | 102 | Logger.prototype._write = function (category, msg) { 103 | var now = ''; 104 | if (this._options.timestamp) { 105 | now = YYYYMMDDHHmmssSSS() + ' '; 106 | } 107 | // write to file 108 | if (this._options.file && this._streams[category]) this._streams[category].write(now + msg); 109 | 110 | /* istanbul ignore next */ 111 | // write to stdout 112 | if (this._options.stdout) { 113 | msg = '[' + category + '] ' + msg; 114 | if (this._options.encoding !== 'utf-8') { 115 | msg = require('iconv-lite').encode(msg, this._options.encoding); 116 | } 117 | 118 | category === 'error' 119 | ? process.stderr.write(now + msg) 120 | : process.stdout.write(now + msg); 121 | } 122 | }; 123 | 124 | /** 125 | * flush logs into file immediate 126 | */ 127 | 128 | Logger.prototype.flush = function(category) { 129 | if (category) return this._streams[category].flush(); 130 | for (var category in this._streams) { 131 | this._streams[category].flush(); 132 | } 133 | }; 134 | 135 | Logger.prototype.getPath = function(category) { 136 | if (!category) return; 137 | if (!this._streams[category]) return; 138 | if (!this._streams[category].stream) return; 139 | return this._streams[category].stream.path; 140 | }; 141 | 142 | Logger.prototype.destroy = function (category) { 143 | if (category) return this._destory(category); 144 | this._options.categories.forEach(this._destory.bind(this)); 145 | }; 146 | 147 | Logger.prototype._destory = function (category) { 148 | delete this[category]; 149 | 150 | if (!this._streams[category]) return; 151 | this._streams[category].end(); 152 | this._streams[category].removeAllListeners(); 153 | this._streams[category] = null; 154 | }; 155 | 156 | function uniq(categories) { 157 | var res = {}; 158 | categories.forEach(function (c) { 159 | res[c] = 1; 160 | }); 161 | return Object.keys(res); 162 | } 163 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Logger = require('..'); 4 | var mkdirp = require('mkdirp'); 5 | var fs = require('fs'); 6 | var rm = require('rimraf'); 7 | var path = require('path'); 8 | var iconv = require('iconv-lite'); 9 | var should = require('should'); 10 | 11 | var logdir = path.join(__dirname, 'logs'); 12 | 13 | var err = new Error('mock error'); 14 | err.code = 'LOG'; 15 | err.data = {foo: 'bar'}; 16 | 17 | describe('mini-loger', function () { 18 | before(function() { 19 | rm.sync(logdir); 20 | mkdirp.sync(logdir); 21 | }); 22 | 23 | describe('with out dir', function () { 24 | it('should throw', function () { 25 | (function () { 26 | Logger(); 27 | }).should.throw('options required'); 28 | (function () { 29 | Logger({}); 30 | }).should.throw('options.dir required'); 31 | }); 32 | 33 | it('should ok', function () { 34 | Logger({ 35 | file: false 36 | }); 37 | }); 38 | }); 39 | 40 | describe('with file = false', function () { 41 | it('should not create file', function () { 42 | var logger = Logger({ 43 | dir: logdir, 44 | file: false 45 | }); 46 | logger.error(err); 47 | logger._streams.should.eql({}); 48 | }); 49 | }); 50 | 51 | describe('Logger.[category]()', function () { 52 | it('should format error ok', function (done) { 53 | var logger = Logger({ 54 | dir: logdir, 55 | duration: '1h', 56 | flushInterval: '100ms' 57 | }); 58 | logger.error(err); 59 | setTimeout(function () { 60 | var content = fs.readFileSync(logger.getPath('error'), 'utf-8'); 61 | content.should.containEql('mock error'); 62 | done(); 63 | }, 200); 64 | }); 65 | 66 | it('should format string ok', function (done) { 67 | var logger = Logger({ 68 | dir: logdir, 69 | categories: ['string'] 70 | }); 71 | logger.string('test for %j', ['item', 'item']); 72 | logger.flush(); 73 | setTimeout(function () { 74 | var content = fs.readFileSync(logger.getPath('string'), 'utf-8'); 75 | content.should.equal('test for ["item","item"]\n\n'); 76 | done(); 77 | }, 100); 78 | }); 79 | 80 | it('should format object ok', function (done) { 81 | var logger = Logger({ 82 | dir: logdir, 83 | categories: ['json'] 84 | }); 85 | logger.json({foo: 'bar'}); 86 | logger.flush(); 87 | setTimeout(function () { 88 | var content = fs.readFileSync(logger.getPath('json'), 'utf-8'); 89 | content.should.equal('{"foo":"bar"}\n\n'); 90 | done(); 91 | }, 100); 92 | }); 93 | 94 | describe('encoding support', function () { 95 | it('should gbk ok', function (done) { 96 | var logger = Logger({ 97 | dir: logdir, 98 | categories: ['gbk'], 99 | encoding: 'gbk' 100 | }); 101 | 102 | logger.gbk('中文'); 103 | logger.flush(); 104 | setTimeout(function () { 105 | var content = iconv.decode(fs.readFileSync(logger.getPath('gbk')), 'gbk'); 106 | content.should.equal('中文\n\n'); 107 | done(); 108 | }, 100); 109 | }); 110 | 111 | it('should utf8 ok', function (done) { 112 | var logger = Logger({ 113 | dir: logdir, 114 | categories: ['utf8'], 115 | encoding: 'utf8' 116 | }); 117 | 118 | logger.utf8('中文'); 119 | logger.flush('utf8'); 120 | setTimeout(function () { 121 | var content = fs.readFileSync(logger.getPath('utf8'), 'utf-8'); 122 | content.should.eql('中文\n\n'); 123 | done(); 124 | }, 100); 125 | }); 126 | }); 127 | }); 128 | 129 | describe('Logger.getPath', function () { 130 | it('should get null when no category', function () { 131 | var logger = Logger({dir: logdir}); 132 | should.not.exist(logger.getPath()); 133 | }); 134 | 135 | it('should get null when not exist category', function () { 136 | var logger = Logger({dir: logdir}); 137 | should.not.exist(logger.getPath('not-exist')); 138 | }); 139 | 140 | it('should get path ok', function () { 141 | var logger = Logger({dir: logdir}); 142 | logger.getPath('error').should.equal(logger._streams.error.stream.path); 143 | }); 144 | }); 145 | 146 | describe('destroy()', function () { 147 | it('should destroy category ok', function () { 148 | var logger = Logger({dir: logdir, categories: 'log'}); 149 | logger.destroy('log'); 150 | should.not.exist(logger.log); 151 | should.not.exist(logger._streams.log); 152 | should.exist(logger.error); 153 | should.exist(logger._streams.error); 154 | }); 155 | 156 | it('should destroy not-exist category ok', function () { 157 | var logger = Logger({dir: logdir, categories: 'log'}); 158 | logger.destroy('not-exist'); 159 | should.exist(logger.log); 160 | should.exist(logger._streams.log); 161 | should.exist(logger.error); 162 | should.exist(logger._streams.error); 163 | }); 164 | 165 | it('should destroy all category ok', function () { 166 | var logger = Logger({dir: logdir, categories: 'log'}); 167 | logger.destroy(); 168 | should.not.exist(logger.log); 169 | should.not.exist(logger._streams.log); 170 | should.not.exist(logger.error); 171 | should.not.exist(logger._streams.error); 172 | }); 173 | }); 174 | 175 | describe('Logger.timestamp', function () { 176 | it('should exist timestamp ', function () { 177 | var logger = Logger({dir: logdir, timestamp: true, categories:['timestamp']}); 178 | logger.timestamp('logtext with timestamp'); 179 | logger.flush('timestamp'); 180 | var patt = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3} logtext with timestamp/g; 181 | setTimeout(function () { 182 | var content = fs.readFileSync(logger.getPath('timestamp'), 'utf-8'); 183 | var match = patt.exec(content); 184 | shoudl.notEqual(match, null); 185 | done(); 186 | }, 100); 187 | }); 188 | }); 189 | 190 | }); 191 | --------------------------------------------------------------------------------