├── .gitignore ├── emojilog.png ├── test ├── package.json ├── pipe-test.js ├── child-test.js └── log-test.js ├── tsconfig.json ├── mochify.safari.json ├── tsconfig.pack.json ├── examples ├── cause.js └── demo.js ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── package.json ├── lib └── log.js ├── README.md └── CHANGES.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /emojilog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascript-studio/studio-log/HEAD/emojilog.png -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser": { 3 | "stream": "@studio/browser-stream" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@studio/tsconfig", 3 | "compilerOptions": { 4 | "lib": ["es2022"], 5 | "types": ["node", "mocha"] 6 | }, 7 | "include": ["**/*.js"], 8 | "exclude": ["node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /mochify.safari.json: -------------------------------------------------------------------------------- 1 | { 2 | "driver": "webdriver", 3 | "driver_options": { 4 | "hostname": "localhost", 5 | "path": "/", 6 | "port": 4444, 7 | "capabilities": { 8 | "browserName": "safari" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.pack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@studio/tsconfig", 3 | "compilerOptions": { 4 | "types": ["node", "mocha"], 5 | "declaration": true, 6 | "noEmit": false, 7 | "emitDeclarationOnly": true 8 | }, 9 | "include": ["lib/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/cause.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const logger = require('..'); 4 | const Stringify = require('@studio/ndjson/stringify'); 5 | 6 | logger.pipe(new Stringify()).pipe(process.stdout); 7 | 8 | const log = logger('Studio'); 9 | 10 | /** @type {Object} */ 11 | const error = new Error('Ouch!'); 12 | error.code = 'E_IMPORTANT'; 13 | error.cause = new Error('Where this is coming from'); 14 | 15 | log.issue('You should look at this', error); 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Maximilian Antoni 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: ['16.x', '18.x', '20.x'] 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'npm' 26 | - name: Install 27 | run: npm ci 28 | env: 29 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1 30 | - name: Lint 31 | if: matrix.node-version == '20.x' 32 | run: npm run lint 33 | - name: Types 34 | if: matrix.node-version == '20.x' 35 | run: node_modules/.bin/tsc 36 | - name: Test Node 37 | run: npm run test:node 38 | - name: Test Browser 39 | if: matrix.node-version == '20.x' 40 | run: | 41 | export PUPPETEER_EXECUTABLE_PATH=$(which google-chrome-stable) 42 | npm run test:browser 43 | -------------------------------------------------------------------------------- /test/pipe-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/no-extraneous-require */ 2 | 'use strict'; 3 | 4 | const { assert, refute, sinon } = require('@sinonjs/referee-sinon'); 5 | const Stringify = require('@studio/ndjson/stringify'); 6 | const logger = require('..'); 7 | 8 | describe('logger pipe', () => { 9 | let clock; 10 | let log; 11 | 12 | beforeEach(() => { 13 | clock = sinon.useFakeTimers(); 14 | clock.tick(123); 15 | log = logger('test'); 16 | }); 17 | 18 | afterEach(() => { 19 | sinon.restore(); 20 | logger.reset(); 21 | }); 22 | 23 | it('does not log to stdout by default', () => { 24 | logger.pipe(new Stringify()); 25 | 26 | log.error(new Error('If you can see this, the test failed!')); 27 | }); 28 | 29 | it('allows to pass "null" as output stream', () => { 30 | refute.exception(() => { 31 | logger.pipe(null); 32 | }); 33 | }); 34 | 35 | it('returns the stream', () => { 36 | const stream = new Stringify(); 37 | 38 | const r = logger.pipe(stream); 39 | 40 | assert.same(r, stream); 41 | }); 42 | 43 | describe('hasStream', () => { 44 | 45 | it('returns false initially', () => { 46 | assert.isFalse(logger.hasStream()); 47 | }); 48 | 49 | it('returns true after pipe was set', () => { 50 | logger.pipe(new Stringify()); 51 | 52 | assert.isTrue(logger.hasStream()); 53 | }); 54 | 55 | it('returns false after pipe was set to null', () => { 56 | logger.pipe(new Stringify()); 57 | logger.pipe(null); 58 | 59 | assert.isFalse(logger.hasStream()); 60 | }); 61 | 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /examples/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Maximilian Antoni 3 | * 4 | * @license MIT 5 | */ 6 | 'use strict'; 7 | 8 | const logger = require('..'); 9 | const Stringify = require('@studio/ndjson/stringify'); 10 | 11 | logger.pipe(new Stringify()).pipe(process.stdout); 12 | 13 | const log = logger('Studio'); 14 | 15 | log.ok('Hello emoji log!'); 16 | log.warn('This might come at a surprise', { ms_timeout: 15000 }); 17 | log.warn('Shit happens', new Error('Oh noes!')); 18 | log.error('Or just a string', {}, 'Oh noes!'); 19 | log.error(new Error('Or only an error')); 20 | const error = new Error('Or an error with a cause'); 21 | error.cause = new Error('That caused the error'); 22 | log.issue(error); 23 | log.issue('This might be an issue', { ms_slow: 567 }); 24 | log.ignore('Yeah, whaterver ...', { some: 'random stuff' }); 25 | log.input('Input received', { headers: { 'Content-Length': 12 } }); 26 | log.output('Output sent', { body: { answer: 42, status: 'OK' } }); 27 | log.send('Sending things', { bytes_size: 45643 }); 28 | log.receive('Receiving things', { bytes_size: 2000000 }); 29 | log.fetch('Fetched', { ms: 42 }); 30 | log.finish('Done'); 31 | log.launch('Starting service', { name: 'Studio', ts_down_since: Date.now() }); 32 | log.terminate('Killed service', { name: 'Studio', ts_started: Date.now() }); 33 | log.spawn('Exciting things'); 34 | log.broadcast('Let the world know', { list: [1, 2, 3, 5, 8, 13, 21] }); 35 | log.broadcast({ just: 'the', data: '!' }); 36 | log.disk('Writing file', { path: '/foo/bar.txt' }); 37 | log.timing('Roundtrip', { ms: 789 }); 38 | log.money('Received', { amount: 95 }); 39 | log.numbers('Some stats', { a: 21, b: 13, c: 8 }); 40 | log.wtf('WTF?!', { 41 | special: '\x00\x07\x08\x09\x0a\x0b\x0c\x0d\x1b', 42 | hex: '\x01\x7f', 43 | emoji: '\ud83c\udf89' 44 | }); 45 | log.wtf(); 46 | -------------------------------------------------------------------------------- /test/child-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/no-extraneous-require */ 2 | 'use strict'; 3 | 4 | const { Writable } = require('stream'); 5 | const { assert, sinon } = require('@sinonjs/referee-sinon'); 6 | const Stringify = require('@studio/ndjson/stringify'); 7 | const logger = require('..'); 8 | 9 | describe('child', () => { 10 | let clock; 11 | let out; 12 | 13 | beforeEach(() => { 14 | out = ''; 15 | clock = sinon.useFakeTimers(); 16 | logger.pipe(new Stringify()).pipe(new Writable({ 17 | write(chunk, enc, done) { 18 | out += chunk; 19 | done(); 20 | } 21 | })); 22 | clock.tick(123); 23 | }); 24 | 25 | afterEach(() => { 26 | sinon.restore(); 27 | logger.reset(); 28 | }); 29 | 30 | it('logs a line', () => { 31 | logger('test').child('child').ok('Hi'); 32 | 33 | assert.equals(out, 34 | '{"ts":123,"ns":"test child","topic":"ok","msg":"Hi"}\n'); 35 | }); 36 | 37 | it('logs child data', () => { 38 | logger('test').child('child', { is: 7 }).ok('Hi'); 39 | 40 | assert.equals(out, '{"ts":123,"ns":"test child","topic":"ok","msg":"Hi",' 41 | + '"data":{"is":7}}\n'); 42 | }); 43 | 44 | it('logs parent data', () => { 45 | logger('test', { is: 7 }).child('child').ok('Hi'); 46 | 47 | assert.equals(out, '{"ts":123,"ns":"test child","topic":"ok","msg":"Hi",' 48 | + '"data":{"is":7}}\n'); 49 | }); 50 | 51 | it('logs child and parent data', () => { 52 | logger('test', { is: 7 }).child('child', { or: 3 }).ok('Hi'); 53 | 54 | assert.equals(out, '{"ts":123,"ns":"test child","topic":"ok","msg":"Hi",' 55 | + '"data":{"is":7,"or":3}}\n'); 56 | }); 57 | 58 | it('logs child and log data', () => { 59 | logger('test').child('child', { is: 7 }).ok('Hi', { or: 3 }); 60 | 61 | assert.equals(out, '{"ts":123,"ns":"test child","topic":"ok","msg":"Hi",' 62 | + '"data":{"is":7,"or":3}}\n'); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studio/log", 3 | "version": "2.1.3", 4 | "description": "A tiny streaming ndJSON logger", 5 | "main": "lib/log.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "test": "npm run test:node && npm run test:browser", 9 | "test:node": "mocha", 10 | "test:browser": "mochify", 11 | "test:safari": "mochify --config mochify.safari.json", 12 | "watch": "mocha --watch", 13 | "build": "tsc --project tsconfig.pack.json", 14 | "clean": "rimraf --glob 'lib/*.d.ts'", 15 | "prepack": "npm run build", 16 | "postpack": "npm run clean", 17 | "preversion": "npm run lint && tsc && npm test", 18 | "version": "changes --commits --footer", 19 | "postversion": "git push --follow-tags && npm publish" 20 | }, 21 | "keywords": [ 22 | "log", 23 | "stream", 24 | "format", 25 | "json", 26 | "ndjson" 27 | ], 28 | "author": "Maximilian Antoni ", 29 | "homepage": "https://github.com/javascript-studio/studio-log", 30 | "eslintConfig": { 31 | "extends": "@studio" 32 | }, 33 | "mochify": { 34 | "driver": "puppeteer", 35 | "bundle": "esbuild --color --bundle --sourcemap=inline --define:global=window --define:process.env.NODE_DEBUG=\"\"", 36 | "bundle_stdin": "require" 37 | }, 38 | "dependencies": { 39 | "@studio/log-topics": "^1.0.0" 40 | }, 41 | "devDependencies": { 42 | "@mochify/cli": "^0.4.1", 43 | "@mochify/driver-puppeteer": "^0.3.1", 44 | "@mochify/driver-webdriver": "^0.2.1", 45 | "@sinonjs/referee-sinon": "^12.0.0", 46 | "@studio/browser-stream": "^1.0.0", 47 | "@studio/changes": "^3.0.0", 48 | "@studio/eslint-config": "^6.0.0", 49 | "@studio/ndjson": "^2.0.0", 50 | "@studio/tsconfig": "^1.3.0", 51 | "esbuild": "^0.20.0", 52 | "eslint": "^8.56.0", 53 | "mocha": "^10.2.0", 54 | "rimraf": "^5.0.5", 55 | "typescript": "^5.3.3" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/javascript-studio/studio-log.git" 60 | }, 61 | "files": [ 62 | "lib", 63 | "LICENSE", 64 | "README.md" 65 | ], 66 | "license": "MIT" 67 | } 68 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Maximilian Antoni 3 | * 4 | * @license MIT 5 | */ 6 | 'use strict'; 7 | 8 | const topics = require('@studio/log-topics'); 9 | 10 | /** 11 | * @typedef {import('stream').Writable} Writable 12 | */ 13 | 14 | /** 15 | * @typedef {undefined | null | boolean | number | string | LogArray | LogData} LogValue 16 | * @typedef {LogValue[]} LogArray 17 | * @typedef {{ [k: string]: LogValue }} LogData 18 | */ 19 | 20 | /** 21 | * @typedef {Object} LogError 22 | * @property {string} [name] 23 | * @property {string} [message] 24 | * @property {string} [stack] 25 | * @property {string} [code] 26 | * @property {Error | LogError} [cause] 27 | */ 28 | 29 | /** 30 | * @typedef {Object} LogEntry 31 | * @property {number} ts 32 | * @property {string} ns 33 | * @property {string} topic 34 | * @property {string} [msg] 35 | * @property {LogData} [data] 36 | * @property {string} [stack] 37 | * @property {string} [cause] 38 | */ 39 | 40 | // Make it work with local symlinks: 41 | let state = global['@studio/log.2']; 42 | if (!state) { 43 | state = global['@studio/log.2'] = { 44 | loggers: {}, 45 | stream: null 46 | }; 47 | } 48 | 49 | /** 50 | * @param {string} key 51 | * @returns {boolean} 52 | */ 53 | function isCauseProperty(key) { 54 | return key !== 'name' && key !== 'message' && key !== 'stack'; 55 | } 56 | 57 | /** 58 | * @param {string | Error | LogError} error 59 | * @returns {string} 60 | */ 61 | function stack(error) { 62 | if (typeof error === 'string') { 63 | return error; 64 | } 65 | const msg = error.name && error.message 66 | ? `${error.name}: ${error.message}` 67 | : error.message || error.name || String(error); 68 | if (msg === '[object Object]') { 69 | return error.stack || ''; 70 | } 71 | const s = error.stack; 72 | return !s ? msg : s.indexOf(msg) >= 0 ? s : `${msg}\n${s}`; 73 | } 74 | 75 | /** 76 | * @param {LogEntry} entry 77 | * @param {LogError} error 78 | */ 79 | function addError(entry, error) { 80 | const cause = error.cause; 81 | const cause_props = typeof cause === 'object' 82 | ? Object.keys(cause).filter(isCauseProperty) 83 | : []; 84 | if (error.code || cause_props.length) { 85 | entry.data = entry.data ? Object.assign({}, entry.data) : {}; 86 | entry.data.code = error.code; 87 | if (cause && cause_props.length) { 88 | entry.data.cause = cause_props.reduce((o, k) => { 89 | o[k] = cause[k]; 90 | return o; 91 | }, {}); 92 | } 93 | } 94 | entry.stack = stack(error); 95 | if (cause) { 96 | entry.cause = stack(cause); 97 | } 98 | } 99 | 100 | /** 101 | * @param {Object} err 102 | * @returns {err is LogError} 103 | */ 104 | function errorLike(err) { 105 | if (typeof err !== 'object') { 106 | return false; 107 | } 108 | if (typeof err.stack === 'string') { 109 | return true; 110 | } 111 | return err.toString !== Object.prototype.toString 112 | && typeof err.name === 'string' 113 | && typeof err.message === 'string'; 114 | } 115 | 116 | /** 117 | * @param {string} ns 118 | * @param {LogData | null} base_data 119 | * @param {string} topic 120 | * @param {unknown} [msg] 121 | * @param {unknown} [data] 122 | * @param {unknown} [error] 123 | */ 124 | function write(ns, base_data, topic, msg, data, error) { 125 | const entry = { ts: Date.now(), ns, topic }; 126 | if (typeof msg === 'string') { 127 | entry.msg = msg; 128 | } else { 129 | error = data; 130 | data = msg; 131 | } 132 | if (data) { 133 | if (errorLike(data)) { 134 | if (base_data) { 135 | entry.data = Object.assign({}, base_data); 136 | } 137 | addError(entry, data); 138 | } else { 139 | entry.data = base_data ? Object.assign({}, base_data, data) : data; 140 | if (error) { 141 | addError(entry, error); 142 | } 143 | } 144 | } else if (base_data) { 145 | entry.data = base_data; 146 | } 147 | state.stream.write(entry); 148 | } 149 | 150 | /** 151 | * @typedef {(function(): void) & (function(unknown): void) & (function(string | LogData, unknown): void) & (function(string, LogData, unknown): void)} LogTopic 152 | */ 153 | 154 | /** 155 | * @param {string} topic 156 | * @returns {LogTopic} 157 | */ 158 | function log(topic) { 159 | /** 160 | * @param {unknown} [msg] 161 | * @param {unknown} [data] 162 | * @param {unknown} [error] 163 | * @this {LoggerBase} 164 | */ 165 | return function (msg, data, error) { 166 | if (state.stream) { 167 | write(this.ns, this.data, topic, msg, data, error); 168 | } 169 | }; 170 | } 171 | 172 | class LoggerBase { 173 | /** 174 | * @param {string} ns 175 | */ 176 | constructor(ns) { 177 | /** @type {string} */ 178 | this.ns = ns; 179 | /** @type {LogData | null} */ 180 | this.data = null; 181 | } 182 | 183 | /** 184 | * @param {string} ns 185 | * @param {LogData} [data] 186 | * @returns {Logger} 187 | */ 188 | child(ns, data) { 189 | if (this.data) { 190 | data = data ? Object.assign({}, this.data, data) : this.data; 191 | } 192 | return module.exports(`${this.ns} ${ns}`, data); 193 | } 194 | } 195 | 196 | /** 197 | * @typedef {Object} LoggerAPI 198 | * @property {LogTopic} ok 199 | * @property {LogTopic} warn 200 | * @property {LogTopic} error 201 | * @property {LogTopic} issue 202 | * @property {LogTopic} ignore 203 | * @property {LogTopic} input 204 | * @property {LogTopic} output 205 | * @property {LogTopic} send 206 | * @property {LogTopic} receive 207 | * @property {LogTopic} fetch 208 | * @property {LogTopic} finish 209 | * @property {LogTopic} launch 210 | * @property {LogTopic} terminate 211 | * @property {LogTopic} spawn 212 | * @property {LogTopic} broadcast 213 | * @property {LogTopic} disk 214 | * @property {LogTopic} timing 215 | * @property {LogTopic} money 216 | * @property {LogTopic} numbers 217 | * @property {LogTopic} wtf 218 | */ 219 | 220 | for (const topic of Object.keys(topics)) { 221 | LoggerBase.prototype[topic] = log(topic); 222 | } 223 | 224 | /** 225 | * @typedef {LoggerBase & LoggerAPI} Logger 226 | */ 227 | 228 | /** 229 | * @param {string} ns 230 | * @param {LogData} [data] 231 | * @returns {Logger} 232 | */ 233 | function logger(ns, data) { 234 | const l = state.loggers[ns] || (state.loggers[ns] = new LoggerBase(ns)); 235 | l.data = data || null; 236 | return l; 237 | } 238 | logger.pipe = pipe; 239 | logger.hasStream = hasStream; 240 | logger.reset = reset; 241 | logger.stack = stack; 242 | 243 | /** 244 | * @template {Writable | null} T 245 | * @param {T} stream 246 | * @returns {T} 247 | */ 248 | function pipe(stream) { 249 | state.stream = stream; 250 | return stream; 251 | } 252 | 253 | /** 254 | * @returns {boolean} 255 | */ 256 | function hasStream() { 257 | return Boolean(state.stream); 258 | } 259 | 260 | function reset() { 261 | state.loggers = {}; 262 | state.stream = null; 263 | } 264 | 265 | module.exports = logger; 266 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Studio Log 2 2 | 3 | 👻 Log [ndjson][1] to an output stream, pretty print the output with emoji ✨ 4 | 5 | ![](https://github.com/javascript-studio/studio-log/raw/master/emojilog.png) 6 | 7 | > __Note!__ Version 2 has significantly changed compared to the [original 8 | > announcement][medium]. Make sure to read the release notes for migration 9 | > instructions! 10 | 11 | [medium]: https://medium.com/javascript-studio/introducing-a-new-ndjson-logger-with-7bb5b95e3b 12 | 13 | ## Features 14 | 15 | - API designed to produce expressive source code. 16 | - Uses topics instead of log levels for more fine grained filtering. 17 | - Uses object streams to avoid serialize -> parse -> serialize when used in a 18 | command line application. 19 | - Disabled by default. If no output stream is specified, no logs are written. 20 | 21 | ## Usage 22 | 23 | Log output is disabled by default to ensure logs don't get in the way when 24 | writing unit tests. Therefore you want to set this up as the first thing in 25 | your main: 26 | 27 | ```js 28 | // Sending raw ndJSON logs to stdout, e.g. in a server application: 29 | const Stringify = require('@studio/ndjson/stringify'); 30 | require('@studio/log') 31 | .pipe(new Stringify()) 32 | .pipe(process.stdout); 33 | 34 | // Sending fancy formatted logs to stdout, e.g. in a command line tool: 35 | const Format = require('@studio/log-format/fancy'); 36 | require('@studio/log') 37 | .pipe(new Format()) 38 | .pipe(process.stdout); 39 | 40 | // Sending logs to console.log, e.g. in a browser: 41 | const Format = require('@studio/log-format/console'); 42 | require('@studio/log') 43 | .pipe(new Format()) 44 | ``` 45 | 46 | Next, create a logger instance in a module and start writing logs: 47 | 48 | ```js 49 | const logger = require('@studio/log'); 50 | 51 | const log = logger('app'); 52 | 53 | exports.startService = function (port) { 54 | log.launch('my service', { port: 433 }); 55 | }; 56 | 57 | ``` 58 | 59 | In the server example above, this output is produced: 60 | 61 | ```json 62 | {"ts":1486630378584,"ns":"app","topic":"launch","msg":"my service","data":{"port":433}} 63 | ``` 64 | 65 | Send your logs to the [emojilog][4] CLI for pretty printing: 66 | 67 | ```bash 68 | ❯ cat logs.ndjson | emojilog 69 | 09:52:58 🚀 app my service port=433 70 | ``` 71 | 72 | ## Install 73 | 74 | ```bash 75 | ❯ npm i @studio/log 76 | ``` 77 | 78 | ## Topics 79 | 80 | Instead of log levels, this logger uses a set of topics. Unlike log levels, 81 | topics are not ordered by severity. 82 | 83 | These topics are available: `ok`, `warn`, `error`, `issue`, `ignore`, `input`, 84 | `output`, `send`, `receive`, `fetch`, `finish`, `launch`, `terminate`, `spawn`, 85 | `broadcast`, `disk`, `timing`, `money`, `numbers` and `wtf`. 86 | 87 | Topics and their mapping to emojis are defined in the [Studio Log Topics][8] 88 | project. 89 | 90 | ## Log format 91 | 92 | - `ns`: The logger instance namespace. 93 | - `ts`: The timestamp as returned by `Date.now()`. 94 | - `topic`: The topic name. 95 | - `msg`: The message. 96 | - `data`: The data. 97 | - `stack`: The stack of error object. 98 | - `cause`: The cause stack of `error.cause` object, if available. 99 | 100 | ## API 101 | 102 | ### Creating a logger 103 | 104 | - `log = logger(ns[, data])`: Creates a new logger with the given namespace. 105 | The namespace is added to each log entry as the `ns` property. If `data` is 106 | provided, it is added to each log entry. Multiple calls with the same `ns` 107 | property return the same logger instance while data is replaced. 108 | - `log.child(ns[, data])`: Creates a child logger of a log instance. The 109 | namespaces are joined with a blank and `data` is merged. Multiple calls with 110 | the same `ns` property return the same logger instance while data is 111 | replaced. 112 | 113 | ### Log instance API 114 | 115 | - `log.{topic}([message][, data][, error])`: Create a new log entry with these 116 | behaviors: 117 | - The `topic` is added as the `"topic"`. 118 | - If `message` is present, it's added as the `"msg"`. 119 | - If `data` is present, it's added as the `"data"`. 120 | - If `error` is present, the `stack` property of the error is added as the 121 | `"stack"`. If no `stack` is present, the `toString` representation of the 122 | error is used. 123 | - If `error.code` is present, it is added to the `"data"` without modifying 124 | the original object. 125 | - If `error.cause` is present, the `stack` property of the cause is added 126 | as the `"cause"`. If no `stack` is present, the `toString` representation 127 | of the cause is used. 128 | - If `error.cause.code` is present, a `cause` object is added to the 129 | `"data"` with `{ code: cause.code }` and without modifying the original 130 | object. 131 | 132 | ### Module API 133 | 134 | - `logger.pipe(stream)`: Configure the output stream to write logs to. If not 135 | specified, no logs are written. Returns the stream. 136 | - `logger.hasStream()`: Whether a stream was set. 137 | - `logger.reset()`: Resets the internal state. 138 | 139 | ## Transform streams 140 | 141 | Transform streams can be used to alter the data before passing it on. For 142 | example, [Studio Log X][7] is a Transform stream that can remove confidential 143 | data from the log data and [Studio Log Format][6] project implements the 144 | `basic`, `fancy` and `console` pretty printers. 145 | 146 | Format transforms are [node transform streams][3] in `writableObjectMode`. Here 147 | is an example implementation, similar to the [ndjson stringify transform][5]: 148 | 149 | ```js 150 | const { Transform } = require('stream'); 151 | 152 | const ndjson = new Transform({ 153 | writableObjectMode: true, 154 | 155 | transform(entry, enc, callback) { 156 | const str = JSON.stringify(entry); 157 | callback(null, `${str}\n`); 158 | } 159 | }); 160 | ``` 161 | 162 | ## Related modules 163 | 164 | - 🌈 [Studio emojilog][4] is a command line tool that parses and pretty prints 165 | the Studio Log ndjson format. 166 | - ☯️ [Studio ndjson][5] can be used to parse the ndjson produced by Studio log. 167 | - 🎩 [Studio Log Format][6] pretty prints Studio Log streams. 168 | - ❎ [Studio Log X][7] x-out confidential data in log entries. 169 | - 🏷 [Studio Log Topics][8] defines the topics used by Studio Log. 170 | - 📦 [Studio Changes][9] is used to create the changelog for this module. 171 | 172 | ## License 173 | 174 | MIT 175 | 176 |
Made with ❤️ on 🌍
177 | 178 | [1]: http://ndjson.org/ 179 | [2]: https://github.com/javascript-studio/studio-log/blob/master/examples/demo.js 180 | [3]: https://nodejs.org/api/stream.html#stream_implementing_a_transform_stream 181 | [4]: https://github.com/javascript-studio/studio-emojilog 182 | [5]: https://github.com/javascript-studio/studio-ndjson 183 | [6]: https://github.com/javascript-studio/studio-log-format 184 | [7]: https://github.com/javascript-studio/studio-log-x 185 | [8]: https://github.com/javascript-studio/studio-log-topics 186 | [9]: https://github.com/javascript-studio/studio-changes 187 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 2.1.3 4 | 5 | - 🐛 [`3dbde57`](https://github.com/javascript-studio/studio-log/commit/3dbde57028ee262a35adc43d3671dbd920c5df55) 6 | Allow stream to be null 7 | - 🐛 [`d23708a`](https://github.com/javascript-studio/studio-log/commit/d23708abe22b4744b6af537d1e8400f2e6a71f8b) 8 | Improve log error typing 9 | - 🐛 [`de9d125`](https://github.com/javascript-studio/studio-log/commit/de9d12587c57a7733d38eaf80761a7fbdcc9070c) 10 | Fix missing base data when logging error only 11 | 12 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2024-01-31._ 13 | 14 | ## 2.1.2 15 | 16 | - 🐛 [`b786cbd`](https://github.com/javascript-studio/studio-log/commit/b786cbdffe9b064a200e21337e41fa8f1b4dae39) 17 | Handle weird errors in stack helper 18 | - 🐛 [`112e615`](https://github.com/javascript-studio/studio-log/commit/112e615f0d9179993b99128de79ba543def341d3) 19 | Fix test in Safari 20 | 21 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2024-01-29._ 22 | 23 | ## 2.1.1 24 | 25 | - 🐛 [`88a50b7`](https://github.com/javascript-studio/studio-log/commit/88a50b7bc02b9e19dd73e4264445371b6cdee3b7) 26 | Include error message in stack if name is missing 27 | - ✨ [`8510c8f`](https://github.com/javascript-studio/studio-log/commit/8510c8fe0c3da4777809d3919736453358e52364) 28 | Run tests with local safari 29 | - ✨ [`318c73b`](https://github.com/javascript-studio/studio-log/commit/318c73bbdc16e681a77e6f1fb77521da6ddc8e34) 30 | Expose stack helper on logger 31 | 32 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2024-01-29._ 33 | 34 | ## 2.1.0 35 | 36 | - 🍏 [`45073bc`](https://github.com/javascript-studio/studio-log/commit/45073bc3f379cbe08985a559ce938de831bf7f79) 37 | Always include error name and message in stack 38 | - 🍏 [`7b5934f`](https://github.com/javascript-studio/studio-log/commit/7b5934f403b03b2c7cddb30ad74777071b7fabe9) 39 | Add typescript 40 | - 🐛 [`03e6a5f`](https://github.com/javascript-studio/studio-log/commit/03e6a5f1e3e681fcddd413e7cd3fe284c14c020f) 41 | Remove broken error handler 42 | - ✨ [`dff39b6`](https://github.com/javascript-studio/studio-log/commit/dff39b62adcbb9a17752fbc70a2995df966b6c58) 43 | Use new mochify with esbuild and upgrade mocha 44 | - ✨ [`2b22838`](https://github.com/javascript-studio/studio-log/commit/2b22838e00b7502295b7a2ec3eed1cf9f3b79218) 45 | Add GitHub action 46 | - ✨ [`05886d9`](https://github.com/javascript-studio/studio-log/commit/05886d9e2cc6a7fb9ee0746b7f9c50443045011b) 47 | Upgrade Studio Changes 48 | - ✨ [`770dc13`](https://github.com/javascript-studio/studio-log/commit/770dc138d3b0b2189aa0548905899cc5da6f8d95) 49 | Upgrade referee-sinon 50 | - ✨ [`eb54bac`](https://github.com/javascript-studio/studio-log/commit/eb54bac12923f67cd155cc463209366392e7c3e2) 51 | Upgrade eslint config and update eslint 52 | 53 | _Released by [Maximilian Antoni](https://github.com/mantoni) on 2024-01-28._ 54 | 55 | ## 2.0.0 56 | 57 | With this release, Studio Log becomes a tiny 3.3KB library. Formatters and the 58 | CLI have been moved to separate modules and with the new `console` format, 59 | Studio Log can be used in browsers too. 60 | 61 | The most important API change is the removal of the default transform. 62 | Updated examples of how to configure the logger can be found in the README. 63 | 64 | - 💥 [`3750908`](https://github.com/javascript-studio/studio-log/commit/37509087ea324ed19158431bd3eebf748c0b919b) 65 | __BREAKING__: Slim down API 66 | 67 | > - Change `out` to `pipe` and let it return the stream instead of the 68 | > logger. 69 | > - Remove `transform`. Use stream pipes instead. 70 | > - Remove `mute` and `muteAll`. Use a custom transform instead. 71 | > - Remove `filter`. Use a custom trnasform instead. 72 | > - Remove default transform. Add a serializing transform like Studio 73 | > ndjson to the pipeline yourself. 74 | 75 | - 💥 [`8da64cc`](https://github.com/javascript-studio/studio-log/commit/8da64cc19b7f36140ce07e456d1080753f41e010) 76 | __BREAKING__: Extract format and CLI modules 77 | 78 | > - Move topics into `@studio/log-topics` module 79 | > - Move format into `@studio/log-format` module 80 | > - Move emojilog into `@studio/emojilog` module 81 | 82 | - 📚 [`612f818`](https://github.com/javascript-studio/studio-log/commit/612f818ddf24c1953068df49497c44a5150ebe47) 83 | Document v2.0 API changes 84 | - 📚 [`eca4548`](https://github.com/javascript-studio/studio-log/commit/eca4548ac425a3905b71a27b6f0068670077a815) 85 | Improve "Transform streams" documentation 86 | - 📚 [`6096722`](https://github.com/javascript-studio/studio-log/commit/6096722f9f1616bf5bb089f7bf7d92a0bca2aef0) 87 | Use new Studio Changes `--commits` feature 88 | - ✨ [`281934c`](https://github.com/javascript-studio/studio-log/commit/281934c63451728c0faca7180cc2e91ca0c014bf) 89 | Add test runner for browser support 90 | - ✨ [`583ed68`](https://github.com/javascript-studio/studio-log/commit/583ed68e631344d40f9cf9c5624e2e6a1bea705c) 91 | Use Sinon + Referee 92 | 93 | ## 1.7.5 94 | 95 | - 🐛 Adjust whitespace after emoji to be consistent 96 | 97 | > With Unicode 9 most emoji are rendered with the correct width now. Some 98 | > still need an extra space though. This changes the spacing to make them 99 | > look consistent. 100 | 101 | ## 1.7.4 102 | 103 | - 🐛 Log all non-error related cause properties 104 | 105 | > Previously, only the `code` property of the cause error was logged. With 106 | > this change any property that is not `name`, `message` or `stack` is 107 | > added to the `data.cause` object. 108 | 109 | ## 1.7.3 110 | 111 | - 🐛 Handle error like objects correctly 112 | 113 | ## 1.7.2 114 | 115 | - 🐛 Fix --map if chunks have multiple lines 116 | 117 | > When passing `--map sourcemaps.map` to `emojilog`, the created transform 118 | > stream expected each chunk to contain a single line. With this change, 119 | > the sourcemaps lookup also works for multiline chunks. 120 | 121 | - ✨ Use Sinon 5 default sandbox 122 | - 📚 Fix typo in message docs 123 | 124 | ## 1.7.1 125 | 126 | - 🐛 Fix unwiring filters 127 | 128 | > Filters must be unwired before re-configuring. This refactoring also 129 | > removes some duplication in reset. 130 | 131 | ## 1.7.0 132 | 133 | - 🍏 Allow to add filters directly to a child namespace 134 | 135 | ## 1.6.0 136 | 137 | - 🍏 Add source maps support 138 | 139 | > Use `--map source.js.map` to specify a source maps file. 140 | 141 | ## 1.5.1 142 | 143 | - 🐛 Restore Node 4 compatibility 144 | - 📚 Add `cause.js` example 145 | - 📚 Move `demo.js` into examples dir 146 | 147 | ## 1.5.0 148 | 149 | - 🍏 Serialize the error `cause` as a new JSON property 150 | - 🍏 Serialize the error `code` into the `data` object 151 | - 🍏 Serialize the error `cause.code` into the `data` object 152 | - 🍏 Support the new `cause` property in the basic and fancy formatters 153 | - 📚 Add new feature to docs and improve usage example and API docs 154 | - 📚 Add cause example to demo 155 | 156 | ## 1.4.1 157 | 158 | - ✨ Add install instructions 159 | 160 | ## 1.4.0 161 | 162 | - 🍏 Add global log filter stream support 163 | 164 | > A global filter stream can be configured which will receive all log 165 | > entries before they are passed to the transform stream. This can be used 166 | > to enrich the log data with generic environment information. 167 | 168 | - 🍏 Add support for logger base data 169 | 170 | > When creating a logger, a `data` property can be passed which will be 171 | > included in the `data` of each log entry. 172 | 173 | - 🍏 Add support for child loggers 174 | 175 | > Child loggers have their namespace joined with their parent by a blank 176 | > and the `data` property of the parent and the child logger are merged. 177 | 178 | - 🍏 Add `mute()` to logger instance 179 | - 🐛 Do not invoke filters if out stream was removed 180 | 181 | ## 1.3.0 182 | 183 | - 🍏 Add log instance filter stream support 184 | 185 | > Filters are object streams to modify the log data before passing it to 186 | > the transform stream. They can be used to x-out confidential information 187 | > or add generated information to log entries. 188 | 189 | - ✨ Add npm 5 `package-lock.json` 190 | 191 | ## 1.2.0 192 | 193 | The ndjson parsing and serialization was refactored into [a separate 194 | module][studio-ndjson]. This enables error handling for serialization failures. 195 | 196 | - 🍏 Use the [Studio ndjson][studio-ndjson] parser transform 197 | - 🍏 Handle transform error events. If a transform error occurs, an error 198 | message is logged instead of throwing up the stack. 199 | - 🍏 Replace the internal default transform with the more robust implementation 200 | from [Studio ndjson][studio-ndjson]. 201 | - ✨ Make log functions no-ops if no output is given. This avoids pointless 202 | `JSON.stringify` invocations and therefore improves performance a tiny bit. 203 | 204 | [studio-ndjson]: https://github.com/javascript-studio/studio-ndjson 205 | 206 | ## 1.1.1 207 | 208 | 🐛 Fix screenshot image to work outside of GitHub 209 | 210 | ## 1.1.0 211 | 212 | 🍏 Add `hasStream()` to the API which returns whether an output stream was set. 213 | 214 | ## 1.0.5 215 | 216 | Fixes and improvements for the fancy format transform. 217 | 218 | - 🐛 Escape all non-printable characters. Print escape sequences, if available, 219 | and fall back to hex values. Do not escape emoji‼️ 220 | - 🐛 Escape newlines and tabs in strings (Fixes #3) 221 | - 🐛 Format empty objects as `{}` without blanks (Fixes #1) 222 | - 🐛 Format primitive data values (Fixes #4) 223 | 224 | ## 1.0.4 225 | 226 | 🙈 Support Node 4 227 | 228 | ## 1.0.3 229 | 230 | ✨ Handle non-json prefix in `emojilog`. Attempt to parse JSON starting from 231 | the first occurrence of the `{` character. Anything before that is forwarded to 232 | stdout. 233 | 234 | ## 1.0.2 235 | 236 | 🐛 Make it work with local symlinks 237 | 238 | ## 1.0.1 239 | 240 | 🙈 Disabled by default 241 | 242 | ## 1.0.0 243 | 244 | ✨ Initial release 245 | -------------------------------------------------------------------------------- /test/log-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable n/no-extraneous-require */ 2 | 'use strict'; 3 | 4 | const { Transform, Writable } = require('stream'); 5 | const { assert, sinon } = require('@sinonjs/referee-sinon'); 6 | const Stringify = require('@studio/ndjson/stringify'); 7 | const logger = require('..'); 8 | 9 | /** 10 | * @typedef {import('..').Logger} Logger 11 | * @typedef {import('..').LogError} LogError 12 | */ 13 | 14 | function MyError(message) { 15 | this.name = 'MyError'; 16 | this.message = message; 17 | } 18 | MyError.prototype.toString = function () { 19 | return `${this.name}: ${this.message}`; 20 | }; 21 | 22 | describe('logger', () => { 23 | let clock; 24 | let out; 25 | /** @type {Logger} */ 26 | let log; 27 | 28 | beforeEach(() => { 29 | out = ''; 30 | clock = sinon.useFakeTimers(); 31 | logger.pipe(new Stringify()).pipe(new Writable({ 32 | write(chunk, enc, done) { 33 | out += chunk; 34 | done(); 35 | } 36 | })); 37 | clock.tick(123); 38 | log = logger('test'); 39 | }); 40 | 41 | afterEach(() => { 42 | sinon.restore(); 43 | logger.reset(); 44 | }); 45 | 46 | it('logs a line', () => { 47 | log.ok('Message'); 48 | 49 | assert.equals(out, '{"ts":123,"ns":"test","topic":"ok","msg":"Message"}\n'); 50 | }); 51 | 52 | it('logs data', () => { 53 | log.output('All', { the: 'things' }); 54 | 55 | assert.equals(out, '{"ts":123,"ns":"test","topic":"output","msg":"All",' 56 | + '"data":{"the":"things"}}\n'); 57 | }); 58 | 59 | it('logs error without message', () => { 60 | const error = new Error(); 61 | 62 | log.error('Oups', error); 63 | 64 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 65 | + `"stack":${JSON.stringify(logger.stack(error))}}\n`); 66 | }); 67 | 68 | it('logs error with message', () => { 69 | const error = new Error('Ouch!'); 70 | 71 | log.error('Oups', error); 72 | 73 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 74 | + `"stack":${JSON.stringify(logger.stack(error))}}\n`); 75 | }); 76 | 77 | it('logs error with cause', () => { 78 | const error = new Error('Ouch!'); 79 | const cause = new Error('Cause'); 80 | error.cause = cause; 81 | 82 | log.error('Oups', error); 83 | 84 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 85 | + `"stack":${JSON.stringify(logger.stack(error))},` 86 | + `"cause":${JSON.stringify(logger.stack(cause))}}\n`); 87 | }); 88 | 89 | it('logs error object toString with numeric cause', () => { 90 | const error = { toString: () => 'Ouch!', cause: 42 }; 91 | 92 | log.error({}, error); 93 | 94 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","data":{},' 95 | + '"stack":"Ouch!","cause":"42"}\n'); 96 | }); 97 | 98 | it('logs error with code', () => { 99 | const error = /** @type {LogError} */ (new Error('Ouch!')); 100 | error.code = 'E_CODE'; 101 | 102 | log.error('Oups', error); 103 | 104 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 105 | + `"data":{"code":"E_CODE"},"stack":${JSON.stringify(logger.stack(error))}}\n`); 106 | }); 107 | 108 | it('logs error cause with code', () => { 109 | const error = new Error('Ouch!'); 110 | const cause = /** @type {LogError} */ (new Error('Cause')); 111 | cause.code = 'E_CODE'; 112 | error.cause = cause; 113 | 114 | log.error('Oups', error); 115 | 116 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 117 | + '"data":{"cause":{"code":"E_CODE"}},' 118 | + `"stack":${JSON.stringify(logger.stack(error))},` 119 | + `"cause":${JSON.stringify(logger.stack(cause))}}\n`); 120 | }); 121 | 122 | it('logs data with error cause with code', () => { 123 | const error = new Error('Ouch!'); 124 | const cause = /** @type {LogError} */ (new Error('Cause')); 125 | cause.code = 'E_CODE'; 126 | error.cause = cause; 127 | 128 | const data = { some: 'data' }; 129 | log.error(data, error); 130 | 131 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 132 | + '"data":{"some":"data","cause":{"code":"E_CODE"}},' 133 | + `"stack":${JSON.stringify(logger.stack(error))},` 134 | + `"cause":${JSON.stringify(logger.stack(cause))}}\n`); 135 | assert.equals(data, { some: 'data' }); // Verify not modified 136 | }); 137 | 138 | it('logs error cause with random properties', () => { 139 | const error = new Error('Ouch!'); 140 | /** @type {Object} */ 141 | const cause = new Error('Cause'); 142 | cause.random = 42; 143 | cause.property = true; 144 | error.cause = cause; 145 | 146 | log.error('Oups', error); 147 | 148 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 149 | + '"data":{"cause":{"random":42,"property":true}},' 150 | + `"stack":${JSON.stringify(logger.stack(error))},` 151 | + `"cause":${JSON.stringify(logger.stack(cause))}}\n`); 152 | }); 153 | 154 | it('logs error cause without name and message properties', () => { 155 | const error = new Error('Ouch!'); 156 | /** @type {Object} */ 157 | const cause = new MyError('Cause'); 158 | cause.random = 42; 159 | cause.property = true; 160 | error.cause = cause; 161 | 162 | log.error('Oups', error); 163 | 164 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 165 | + '"data":{"cause":{"random":42,"property":true}},' 166 | + `"stack":${JSON.stringify(logger.stack(error))},` 167 | + `"cause":"${cause.toString()}"}\n`); 168 | }); 169 | 170 | it('logs error cause without properties other than name and message', () => { 171 | const error = new Error('Ouch!'); 172 | const cause = new MyError('Cause'); 173 | error.cause = cause; 174 | 175 | log.error('Oups', error); 176 | 177 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 178 | // Note: No "data" property 179 | + `"stack":${JSON.stringify(logger.stack(error))},` 180 | + `"cause":"${cause.toString()}"}\n`); 181 | }); 182 | 183 | it('does not screw up if cause is string', () => { 184 | const error = new Error('Ouch!'); 185 | error.cause = 'Simple string cause'; 186 | 187 | log.error('Oups', error); 188 | 189 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Oups",' 190 | + `"stack":${JSON.stringify(logger.stack(error))},` 191 | + '"cause":"Simple string cause"}\n'); 192 | }); 193 | 194 | it('logs message with custom error', () => { 195 | log.error('This went south', new MyError('Cause')); 196 | 197 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 198 | + '"msg":"This went south","stack":"MyError: Cause"}\n'); 199 | }); 200 | 201 | it('logs custom data object', () => { 202 | function MyThing() { 203 | this.is = 42; 204 | } 205 | MyThing.prototype.toString = function () { 206 | return '[object MyThing]'; 207 | }; 208 | 209 | log.ok(new MyThing()); 210 | 211 | assert.equals(out, '{"ts":123,"ns":"test","topic":"ok",' 212 | + '"data":{"is":42}}\n'); 213 | }); 214 | 215 | it('logs message with custom data object', () => { 216 | function MyThing() { 217 | this.is = 42; 218 | } 219 | MyThing.prototype.toString = function () { 220 | return '[object MyThing]'; 221 | }; 222 | 223 | log.ok('Note', new MyThing()); 224 | 225 | assert.equals(out, '{"ts":123,"ns":"test","topic":"ok",' 226 | + '"msg":"Note","data":{"is":42}}\n'); 227 | }); 228 | 229 | it('logs message with data { name, message }', () => { 230 | log.error({ name: 'a', message: 'b' }); 231 | 232 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 233 | + '"data":{"name":"a","message":"b"}}\n'); 234 | }); 235 | 236 | it('logs error-like object { name, message, stack } with error in stack', () => { 237 | const error = { 238 | name: 'SyntaxError', 239 | message: 'Ouch!', 240 | stack: 'SyntaxError: Ouch!\n at xyz:123' 241 | }; 242 | 243 | log.error(error); 244 | 245 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 246 | + `"stack":"SyntaxError: Ouch!\\n at xyz:123"}\n`); 247 | }); 248 | 249 | it('logs error-like object { name, message }', () => { 250 | const error = { name: 'SyntaxError', message: 'Ouch!' }; 251 | 252 | log.error('Test', {}, error); 253 | 254 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error","msg":"Test",' 255 | + `"data":{},"stack":"SyntaxError: Ouch!"}\n`); 256 | }); 257 | 258 | it('logs error-like object { name, message, stack } without error in stack', () => { 259 | const error = { name: 'SyntaxError', message: 'Ouch!', stack: ' at xyz:123' }; 260 | 261 | log.error(error); 262 | 263 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 264 | + `"stack":"SyntaxError: Ouch!\\n at xyz:123"}\n`); 265 | }); 266 | 267 | it('logs error-like cause { name, message, stack }', () => { 268 | const error = new Error('Ouch!'); 269 | error.cause = { name: 'SyntaxError', message: 'Cause', stack: ' at xyz:123' }; 270 | 271 | log.error(error); 272 | 273 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 274 | + `"stack":${JSON.stringify(logger.stack(error))},` 275 | + `"cause":"SyntaxError: Cause\\n at xyz:123"}\n`); 276 | }); 277 | 278 | it('logs data and error object', () => { 279 | const error = new Error('Ouch!'); 280 | 281 | log.issue('Found', { some: 'issue' }, error); 282 | 283 | assert.equals(out, '{"ts":123,"ns":"test","topic":"issue","msg":"Found",' 284 | + `"data":{"some":"issue"},"stack":${JSON.stringify(logger.stack(error))}}\n`); 285 | }); 286 | 287 | it('logs data and error object with cause', () => { 288 | const error = /** @type {LogError} */ (new Error('Ouch!')); 289 | const cause = new Error('Cause'); 290 | error.cause = cause; 291 | 292 | log.issue('Found', { some: 'issue' }, error); 293 | 294 | assert.equals(out, '{"ts":123,"ns":"test","topic":"issue","msg":"Found",' 295 | + `"data":{"some":"issue"},"stack":${JSON.stringify(logger.stack(error))},` 296 | + `"cause":${JSON.stringify(logger.stack(error.cause))}}\n`); 297 | }); 298 | 299 | it('logs data and error object with code', () => { 300 | const error = /** @type {LogError} */ (new Error('Ouch!')); 301 | error.code = 'E_CODE'; 302 | 303 | log.issue('Found', { some: 'issue' }, error); 304 | 305 | assert.equals(out, '{"ts":123,"ns":"test","topic":"issue","msg":"Found",' 306 | + '"data":{"some":"issue","code":"E_CODE"},' 307 | + `"stack":${JSON.stringify(logger.stack(error))}}\n`); 308 | }); 309 | 310 | it('does not modify given data object if error code is present', () => { 311 | const error = /** @type {LogError} */ (new Error('Ouch!')); 312 | error.code = 'E_CODE'; 313 | 314 | const data = { some: 'issue' }; 315 | log.issue('Found', data, error); 316 | 317 | assert.equals(data, { some: 'issue' }); 318 | }); 319 | 320 | it('logs data with custom toJSON', () => { 321 | const data = { toJSON: () => ({ is: 42 }) }; 322 | 323 | log.numbers(data); 324 | 325 | assert.equals(out, '{"ts":123,"ns":"test","topic":"numbers",' 326 | + '"data":{"is":42}}\n'); 327 | }); 328 | 329 | it('logs data and error string', () => { 330 | log.issue('Found', { some: 'issue' }, 'Ouch!'); 331 | 332 | assert.equals(out, '{"ts":123,"ns":"test","topic":"issue","msg":"Found",' 333 | + '"data":{"some":"issue"},"stack":"Ouch!"}\n'); 334 | }); 335 | 336 | it('uses the given transform stream to serialize entries', () => { 337 | const entries = []; 338 | logger.pipe(new Transform({ 339 | writableObjectMode: true, 340 | transform(entry, enc, done) { 341 | entries.push(entry); 342 | done(); 343 | } 344 | })); 345 | 346 | log.input('In'); 347 | log.output('Out'); 348 | 349 | assert.equals(entries, [ 350 | { ts: 123, ns: 'test', topic: 'input', msg: 'In' }, 351 | { ts: 123, ns: 'test', topic: 'output', msg: 'Out' } 352 | ]); 353 | }); 354 | 355 | it('returns same instance when requesting the same logger twice', () => { 356 | const a = logger('foo'); 357 | const b = logger('foo'); 358 | 359 | assert.same(a, b); 360 | }); 361 | 362 | it('logs only data', () => { 363 | log.fetch({ host: 'javascript.studio', path: '/' }); 364 | 365 | assert.equals(out, '{"ts":123,"ns":"test","topic":"fetch","data":' 366 | + '{"host":"javascript.studio","path":"/"}}\n'); 367 | }); 368 | 369 | it('logs only data and error', () => { 370 | log.issue({ id: 'studio' }, 'Oh!'); 371 | 372 | assert.equals(out, '{"ts":123,"ns":"test","topic":"issue","data":' 373 | + '{"id":"studio"},"stack":"Oh!"}\n'); 374 | }); 375 | 376 | it('logs only error', () => { 377 | const error = new Error('Ouch'); 378 | 379 | log.error(error); 380 | 381 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 382 | + `"stack":${JSON.stringify(logger.stack(error))}}\n`); 383 | }); 384 | 385 | it('logs only meta data', () => { 386 | log.timing(); 387 | 388 | assert.equals(out, '{"ts":123,"ns":"test","topic":"timing"}\n'); 389 | }); 390 | 391 | it('logs base data', () => { 392 | logger('base', { base: 'data' }).ok('Text'); 393 | 394 | assert.equals(out, '{"ts":123,"ns":"base","topic":"ok","msg":"Text",' 395 | + '"data":{"base":"data"}}\n'); 396 | }); 397 | 398 | it('replaces base data when creating new logger', () => { 399 | logger('base', { base: 'data' }); 400 | logger('base', { base: 'changed' }).ok('Text'); 401 | 402 | assert.equals(out, '{"ts":123,"ns":"base","topic":"ok","msg":"Text",' 403 | + '"data":{"base":"changed"}}\n'); 404 | }); 405 | 406 | it('mixes base data with log data', () => { 407 | const log_with_data = logger('test', { base: 'data' }); 408 | log_with_data.ok({ and: 7 }); 409 | log_with_data.ok({ or: 42 }); // Verify "and" is not copied into the base data 410 | 411 | assert.equals(out, '' 412 | + '{"ts":123,"ns":"test","topic":"ok","data":{"base":"data","and":7}}\n' 413 | + '{"ts":123,"ns":"test","topic":"ok","data":{"base":"data","or":42}}\n'); 414 | }); 415 | 416 | it('includes base data when logging error only', () => { 417 | const log_with_data = logger('test', { base: 'data' }); 418 | const error = new Error(); 419 | log_with_data.issue(error); 420 | 421 | assert.equals(out, '{"ts":123,"ns":"test","topic":"issue",' 422 | + `"data":{"base":"data"},"stack":${JSON.stringify(logger.stack(error))}}\n`); 423 | }); 424 | 425 | it('mixes base data with error code', () => { 426 | const log_with_data = logger('test', { base: 'data' }); 427 | const error = /** @type {LogError} */ (new Error()); 428 | error.code = 'E_CODE'; 429 | log_with_data.issue(error); 430 | 431 | assert.equals(out, '{"ts":123,"ns":"test","topic":"issue",' 432 | + '"data":{"base":"data","code":"E_CODE"},"stack":' 433 | + `${JSON.stringify(logger.stack(error))}}\n`); 434 | }); 435 | 436 | it('fails type check if first argument is error and second is string', () => { 437 | const error = new Error('Ouch!'); 438 | 439 | // @ts-expect-error 440 | log.error(error, 'Oups'); 441 | 442 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 443 | + `"stack":${JSON.stringify(logger.stack(error))}}\n`); 444 | }); 445 | 446 | it('fails type check if first argument is error and second is data', () => { 447 | const error = new Error('Ouch!'); 448 | 449 | // @ts-expect-error 450 | log.error(error, { the: 'things'}); 451 | 452 | assert.equals(out, '{"ts":123,"ns":"test","topic":"error",' 453 | + `"stack":${JSON.stringify(logger.stack(error))}}\n`); 454 | }); 455 | 456 | context('stack', () => { 457 | it('includes error name', () => { 458 | const stack = logger.stack(new TypeError()); 459 | 460 | assert.match(stack, 'TypeError'); 461 | }); 462 | 463 | it('includes error name and message', () => { 464 | const stack = logger.stack(new TypeError('Ouch!')); 465 | 466 | assert.match(stack, 'TypeError: Ouch!'); 467 | }); 468 | 469 | it('includes error message if name is missing', () => { 470 | const error = new TypeError('Ouch!'); 471 | 472 | const stack = logger.stack({ 473 | message: error.message, 474 | stack: error.stack ? error.stack.replace(`${String(error)}\n`, '') : '' 475 | }); 476 | 477 | assert.isTrue(stack.startsWith('Ouch!\n'), stack); 478 | }); 479 | 480 | it('includes error message if name is missing, but message is in the stack', () => { 481 | const error = new TypeError('Ouch!'); 482 | const lines = error.stack ? error.stack.split('\n').slice(1) : []; 483 | lines.unshift(`Uncaught: ${error.message}`); 484 | 485 | const stack = logger.stack({ 486 | message: error.message, 487 | stack: lines.join('\n') 488 | }); 489 | 490 | assert.isTrue(stack.startsWith('Uncaught: Ouch!\n'), stack); 491 | }); 492 | 493 | it('support error with stack property only', () => { 494 | const error = new TypeError('Ouch!'); 495 | 496 | const stack = logger.stack({ 497 | stack: error.stack 498 | }); 499 | 500 | assert.equals(stack, error.stack); 501 | }); 502 | }); 503 | }); 504 | --------------------------------------------------------------------------------