├── .babelrc ├── .coveralls.yml ├── .editorconfig ├── .esdoc.json ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── api.md ├── benchmarks └── basic.bench.js ├── docs ├── configuration │ ├── automatic-log-augmentation.md │ ├── logging-to-multiple-streams.md │ └── readme.md ├── installation │ ├── http.md │ ├── readme.md │ └── stdout.md ├── integrations │ ├── bunyan.md │ ├── console.md │ ├── express.md │ ├── readme.md │ ├── system.md │ └── winston.md ├── readme.md ├── troubleshooting.md └── usage │ ├── basic-logging.md │ ├── custom-events.md │ └── readme.md ├── echo.js ├── package.json ├── src ├── config.js ├── console.js ├── context.js ├── contexts │ ├── http.js │ └── index.js ├── data │ └── errors.js ├── event.js ├── events │ ├── custom.js │ ├── http_request.js │ ├── http_response.js │ └── index.js ├── formatters │ ├── index.js │ └── winston.js ├── index.js ├── install.js ├── log_entry.js ├── middlewares │ ├── express.js │ ├── index.js │ └── koa.js ├── transports │ ├── bunyan.js │ ├── https.js │ └── index.js └── utils │ ├── attach.js │ ├── debug.js │ ├── log.js │ └── metadata.js ├── test ├── attach.test.js ├── console.test.js ├── index.test.js ├── install.test.js ├── log.test.js ├── loggers │ └── winston.test.js ├── middlewares │ └── express.test.js ├── mocks │ └── request.js ├── support │ └── test_streams.js └── transports │ ├── bunyan.test.js │ └── https.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 2NrnUszFUF8rtlZPq83oXagMJ8TYRd192 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_size = 2 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | end_of_line = lf 12 | charset = utf-8 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "experimentalProposal": { 5 | "classProperties": true, 6 | "objectRestSpread": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "xo/esnext", 4 | "prettier" 5 | ], 6 | "rules": { 7 | "camelcase": 0, 8 | "consistent-return": 0, 9 | "indent": [2, 2], 10 | "no-new": 0, 11 | "no-trailing-spaces": 0, 12 | "prefer-reflect": 0, 13 | "semi": [1, "never"], 14 | "space-before-function-paren": [2, "never"], 15 | "capitalized-comments": 0 16 | }, 17 | "parser": "babel-eslint", 18 | "env": { 19 | "node": true, 20 | "es6": true 21 | }, 22 | "emcaFeatures": { 23 | "modules": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .package-lock.json 3 | npm-debug.log 4 | node_modules 5 | logs 6 | *.log 7 | coverage 8 | dist 9 | notes.md 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [3.1.1] - 2017-10-17 11 | 12 | ### Fixed 13 | 14 | - Fixes the key name used for HTTP context to use `http` instead of `http_context`. 15 | 16 | ## [3.1.0] - 2017-10-17 17 | 18 | ### Added 19 | 20 | - Exposes a new `LogEntry` class that can be used to build a structured log directly. 21 | 22 | ## [3.0.4] - 2017-10-08 23 | 24 | ### Changed 25 | 26 | - Revert previous content type change, changing HTTPS stream to use application/json instead of text/plain. 27 | 28 | ## [3.0.3] - 2017-10-08 29 | 30 | ### Changed 31 | 32 | - HTTPS stream uses a text/plain content type instead of an application/json since it 33 | receives strings. 34 | 35 | ## [3.0.2] - 2017-09-28 36 | 37 | ### Fixed 38 | 39 | - Message no longer is included in metadata object 40 | - Winston timestamps are now preserved when using the formatter 41 | 42 | ## [3.0.1] - 2017-09-20 43 | 44 | ### Fixed 45 | 46 | - Resolves crashing issue when failing to connect to ingestion server 47 | 48 | ## [3.0.0] - 2017-09-19 49 | 50 | ### Fixed 51 | 52 | - The built in `console` functions are no longer patched on import 53 | 54 | ### Changed 55 | 56 | - To append metadata without installing a transport, you must set `timber.config.append_metadata = true` 57 | 58 | [Unreleased]: https://github.com/timberio/timber-node/compare/v3.1.1...HEAD 59 | [3.1.1]: https://github.com/timberio/timber-node/compare/v3.0.4...v3.1.1 60 | [3.1.0]: https://github.com/timberio/timber-node/compare/v3.0.4...v3.1.0 61 | [3.0.4]: https://github.com/timberio/timber-node/compare/v3.0.3...v3.0.4 62 | [3.0.3]: https://github.com/timberio/timber-node/compare/v3.0.2...v3.0.3 63 | [3.0.2]: https://github.com/timberio/timber-node/compare/v3.0.1...v3.0.2 64 | [3.0.1]: https://github.com/timberio/timber-node/compare/v3.0.0...v3.0.1 65 | [3.0.0]: https://github.com/timberio/timber-node/compare/v2.1.1...v3.0.0 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Copyright (c) 2017, Timber Technologies, Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose 6 | with or without fee is hereby granted, provided that the above copyright notice 7 | and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 13 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 14 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 15 | THIS SOFTWARE. 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Replaced by timber-js 2 | 3 | Timber has developed a new and improved hybrid javascript library that supports Node. Please use `timber-js`: 4 | 5 | https://github.com/timberio/timber-js 6 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Timber (client, options) 4 | 5 | | Param | Type | Default | Description | 6 | | --- | --- | --- | --- | 7 | | client | Timber.Client | | Timber client | 8 | | options | Object | | Options | 9 | | [options.highWaterMark] | number | 16 | Number of items to buffer before writing. Also the size of the underlying stream buffer. | 10 | | [options.flushTimeout] | number | | Number of ms to flush records after, if highWaterMark hasn't been reached | 11 | | [options.logger] | Object | | Instance of a logger like bunyan or winston | 12 | -------------------------------------------------------------------------------- /benchmarks/basic.bench.js: -------------------------------------------------------------------------------- 1 | // Add some benchmarks here -------------------------------------------------------------------------------- /docs/configuration/automatic-log-augmentation.md: -------------------------------------------------------------------------------- 1 | Timber for Node will automatically augment all log lines with rich structured data. This will turn ordinary log lines like: 2 | 3 | ``` 4 | Sent 200 in 45.ms 5 | ``` 6 | 7 | into this: 8 | 9 | ``` 10 | Sent 200 in 45.2ms @metadata {"dt": "2017-02-02T01:33:21.154345Z", "level": "info", "context": {"http": {"method": "GET", "host": "timber.io", "path": "/path", "request_id": "abcd1234"}}, "event": {"http_response": {"status": 200, "time_ms": 45.2}}} 11 | ``` 12 | 13 | But when using Timber for Node in a development environment, having all that extra noise in your console can make it difficult to read your logs. For this reason, Timber only appends metadata when the `NODE_ENV` is set to `production`. If you want to override this setting, you can use the `append_metadata` configuration option. 14 | 15 | ## How to use it 16 | 17 | ```js 18 | timber.config.append_metadata = true 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/configuration/logging-to-multiple-streams.md: -------------------------------------------------------------------------------- 1 | If you followed the [standard install instructions](../installation), your application will send all logs from `stdout` and `stderr` to Timber. If you prefer to send your logs to multiple destinations Timber has built-in support for this. Using the `attach` function, you can attach multiple writable streams to `stout` and `stderr`. 2 | 3 | **Note:** The `attach()` function is a replacement for `install()`. When manually attaching streams, you no longer need to use `install()`. 4 | 5 | 6 | ## How to use it 7 | 8 | ### Example: Logging to Timber & a file 9 | 10 | ```js 11 | const fs = require('fs') 12 | const timber = require('timber') 13 | 14 | const http_stream = new timber.transports.HTTPS('{{my-timber-api-key}}') 15 | const file_stream = fs.createWriteStream('./app_logs', { flags: 'a' }) 16 | 17 | timber.attach([http_stream, file_stream], process.stdout) 18 | timber.attach([http_stream, file_stream], process.stderr) 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/configuration/readme.md: -------------------------------------------------------------------------------- 1 | [Timber for Node](https://github.com/timberio/timber-node) ships with a variety of configuration options. Below are a few of the popular ones. For a comprehensive list, see the [timber.config documentation](https://timberio.github.io/timber-node/variable/index.html#static-variable-config). 2 | -------------------------------------------------------------------------------- /docs/installation/http.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: HTTP 3 | --- 4 | 1. In your `shell`, *run*: prefer to integrate with your platform instead? 5 | 6 | ```shell 7 | npm install --save timber 8 | ``` 9 | 10 | 2. In your entry file, *add*: 11 | 12 | ```js 13 | const timber = require('timber'); 14 | 15 | const transport = new timber.transports.HTTPS('{{my-timber-api-key}}'); 16 | timber.install(transport); 17 | ``` 18 | 19 | 3. Optionally install middleware: 20 | 21 | - Using [Express](https://github.com/expressjs/express)? ([learn more](../integrations/express)): 22 | 23 | ```js 24 | app.use(timber.middlewares.express()) 25 | ``` 26 | 27 | 4. Optionally integrate with your logger: 28 | 29 | - Using [winston](https://github.com/winstonjs/winston)? ([learn more](../integrations/winston)) 30 | 31 | ```js 32 | winston.remove(winston.transports.Console); 33 | winston.add(winston.transports.Console, { formatter: timber.formatters.Winston }); 34 | ``` 35 | 36 | - Using [bunyan](https://github.com/trentm/node-bunyan)? ([learn more](../integrations/bunyan)) 37 | 38 | ```js 39 | const log = bunyan.createLogger({ 40 | name: 'Logger', 41 | stream: new timber.transports.Bunyan() 42 | }); 43 | ``` 44 | -------------------------------------------------------------------------------- /docs/installation/readme.md: -------------------------------------------------------------------------------- 1 | *Note: We highly recommended [following the instructions within the Timber app](https://app.timber.io), it provides an easy step-by-step guide.* 2 | 3 | Depending on your platform you'll want write logs to STDOUT, a file, or deliver them over HTTP to the Timber service. If you're unaware which method to use, checkout our [HTTP, STDOUT, or log files doc](/guides/http-stdout-or-log-files). 4 | -------------------------------------------------------------------------------- /docs/installation/stdout.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: STDOUT 3 | --- 4 | 1. In your `shell`, *run*: prefer to integrate with your platform instead? 5 | 6 | ```shell 7 | npm install --save timber 8 | ``` 9 | 10 | 2. In your entry file, *add*: 11 | 12 | ```js 13 | const timber = require('timber'); 14 | 15 | timber.config.append_metadata = true 16 | ``` 17 | 18 | 3. Optionally install middleware: 19 | 20 | - Using [Express](https://github.com/expressjs/express)? ([learn more](../integrations/express)): 21 | 22 | ```js 23 | app.use(timber.middlewares.express()) 24 | ``` 25 | 26 | 4. Optionally integrate with your logger: 27 | 28 | - Using [winston](https://github.com/winstonjs/winston)? ([learn more](../integrations/winston)) 29 | 30 | ```js 31 | winston.remove(winston.transports.Console); 32 | winston.add(winston.transports.Console, { formatter: timber.formatters.Winston }); 33 | ``` 34 | 35 | - Using [bunyan](https://github.com/trentm/node-bunyan)? ([learn more](../integrations/bunyan)) 36 | 37 | ```js 38 | const log = bunyan.createLogger({ 39 | name: 'Logger', 40 | stream: timber.transports.Bunyan 41 | }); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/integrations/bunyan.md: -------------------------------------------------------------------------------- 1 | The [Timber for Node](https://github.com/timberio/timber-node) [Bunyan](https://github.com/trentm/node-bunyan) integration works with the Bunyan logger to ensure structured data is properly captured, providing for seamless integration between the bunyan logger and Timber. 2 | 3 | ## Installation 4 | 5 | This integration is setup when you install Timber. Please follow the instructions for [installing Timber](../installation) to use this integration. 6 | -------------------------------------------------------------------------------- /docs/integrations/console.md: -------------------------------------------------------------------------------- 1 | Timber for Node ships with an integration for the standard JavaScript `console` functions. This integration allows you to either output standard logs or append logs with custom metadata or events using the builtin `console.log` functions. See [usage](../../usage) for more details. 2 | 3 | ## Installation 4 | 5 | This integration is automatically setup when you install Timber. Please follow the instructions for [installing Timber](../installation) to use the console integration. 6 | -------------------------------------------------------------------------------- /docs/integrations/express.md: -------------------------------------------------------------------------------- 1 | The [Timber for Node](https://github.com/timberio/timber-node) [Express](http://expressjs.com) integration automatically outputs [augmented](/concepts/structuring-through-augmentation) log lines for all HTTP events. 2 | 3 | |You'll Get| 4 | |:------| 5 | |+[HTTP request event](/concepts/log-event-json-schema/events/http-request)| 6 | |+[HTTP response event](/concepts/log-event-json-schema/events/http-response)| 7 | 8 | 9 | ## What you can do 10 | 11 | 1. [**Trace HTTP requests**](/app/console/trace-http-requests) 12 | 2. [**Inspect HTTP requests & their parameters**](/app/console/inspect-http-requests) 13 | 3. [**Inspect Express logs and view their associated metadata**](/app/console/view-a-logs-metadata-and-context) 14 | 4. [**Search on Express structured data**](/app/console/searching) 15 | 5. [**Alert on Express structured data**](/app/alerts) 16 | 17 | 18 | ## Installation 19 | 20 | ```js 21 | const express = require('express') 22 | const timber = require('timber') 23 | 24 | const transport = new timber.transports.HTTPS('your-timber-api-key'); 25 | timber.install(transport); 26 | 27 | const app = express() 28 | 29 | app.use(timber.middlewares.express()) 30 | 31 | app.get('/', function (req, res) { 32 | res.send('hello, world!') 33 | }) 34 | 35 | // Output when loading index route: 36 | // => Started GET "/" @metadata {"level": "error", "context": {...}} 37 | // => Outgoing HTTP response 200 in 2ms @metadata {"level": "error", ... } 38 | ``` 39 | 40 | ## Configuration 41 | 42 | You can pass a configuration object as an argument to the middleware if you want to use a custom configuration. The available options are: 43 | 44 | - `capture_request_body` (`boolean`): Enables capturing of the http request body data (`false` by default) 45 | - `combine_http_events` (`boolean`): If enabled, the HTTPRequest and HTTPResponse events will be combined in a single log message (`false` by defaut) 46 | - `logger` (`object`): Pass a reference of your logger if you want the express logs sent to multiple destinations (read the below section for more information) 47 | 48 | ## Using with a custom logger 49 | 50 | If you're using winston or bunyan for logging, it's possible to route the express event logs through your preferred logger. This is not required if you're only sending your logs to Timber, but may be desired if you want the express event logs sent to multiple transports streams. To enable this, pass a reference of your logger to the middleware: 51 | 52 | ```js 53 | // If you're using winston: 54 | const winston = require('winston') 55 | app.use(timber.middlewares.express({ logger: winston })) 56 | 57 | // If you're using bunyan: 58 | const bunyan = require('bunyan') 59 | const logger = bunyan.createLogger({ name: 'Custom Logger' }) 60 | app.use(timber.middlewares.express({ logger: winston })) 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/integrations/readme.md: -------------------------------------------------------------------------------- 1 | [Timber for Node](https://github.com/timberio/timber-node) extends beyond your basic logging functionality and integrates with popular libraries and frameworks. This makes structured quality logging effortless. Below is a list of integrations we offer and the various events and contexts they create. 2 | -------------------------------------------------------------------------------- /docs/integrations/system.md: -------------------------------------------------------------------------------- 1 | The System / Server integration captures system level context upon initialization, augmenting your logs with structured data like `hostname`, `ip`, `pid`, and more. 2 | 3 | |You'll Get| 4 | |:------| 5 | |+[System context](/concepts/log-event-json-schema/contexts/system)| 6 | 7 | 8 | ## What you can do 9 | 10 | 1. [**Search your logs with this data**](/app/console/searching) - `system.hostname:server123.myhost.com` 11 | 2. [**Access this data by viewing a log's metadata & context**](/app/console/view-a-logs-metadata-and-context) 12 | 13 | 14 | ## Installation 15 | 16 | This integration is installed automatically upon initialization. There is nothing you need to do. 17 | -------------------------------------------------------------------------------- /docs/integrations/winston.md: -------------------------------------------------------------------------------- 1 | The [Timber for Node](https://github.com/timberio/timber-node) [Winston](https://github.com/winstonjs/winston) integration works with the Winston logger to ensure structured data is properly captured, providing for seamless integration between the Winston logger and Timber. 2 | 3 | ## Installation 4 | 5 | This integration is setup when you install Timber. Please follow the instructions for [installing Timber](../installation) to use this integration. 6 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | items: 3 | - installation 4 | - usage 5 | - configuration 6 | - integrations 7 | - troubleshooting.md 8 | - component: Divider 9 | - title: Library Documentation 10 | url: https://timberio.github.io/timber-node/ 11 | - title: Github Repo 12 | url: https://github.com/timberio/timber-node 13 | --- 14 | [Timber for Node](https://github.com/timberio/timber-node) is a drop-in upgrade for your Node logs that works with the `console` and [other popular loggers](integrations) to [transparently augment](/concepts/structuring-through-augmentation) your logs with critical [metadata and context](/concepts/metadata-context-and-events); turning them into rich, useful events. It pairs with the 15 | [Timber console](#the-timber-console) to deliver a tailored Node logging experience designed to make 16 | you more productive. 17 | 18 | [Start here. Hook into the `console` with one line.](installation) 19 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | If you ever run into an issue while using Timber for Node, you can use the built-in debug logger to help identify the issue. When enabling the debug logger, Timber will write verbose debug logs to any stream you supply. 2 | 3 | 4 | ## How to use it 5 | 6 | The most common way to use the debug logger is with a file stream, this can be done like this: 7 | 8 | ```js 9 | const timber = require('timber') 10 | const fs = require('fs') 11 | 12 | timber.config.debug_logger = fs.createWriteStream('./debug.log', {flags: 'a'}) 13 | ``` 14 | 15 | You can supply the `debug_logger` to any writeable stream. It's not recommended to set the `debug_logger` to `stdout` or `stderr` since Timber attaches to those streams by default. 16 | -------------------------------------------------------------------------------- /docs/usage/basic-logging.md: -------------------------------------------------------------------------------- 1 | Timber integrates directly into stdout, so there's no special syntax required. Once you've installed Timber, you can continue using your logger as you normally would. 2 | 3 | 4 | ## How to use it 5 | 6 | ```js 7 | // If you're using the standard js console logger: 8 | console.info("Info message") 9 | console.warn("Warn message") 10 | console.error("Error message") 11 | 12 | // If you're using winston: 13 | winston.info("Info message") 14 | winston.warn("Warn message") 15 | winston.error("Error message") 16 | 17 | // If you're using bunyan: 18 | logger.info("Info message") 19 | logger.warn("Warn message") 20 | logger.error("Error message") 21 | ``` 22 | 23 | We encourage standard / traditional log messages for non-meaningful events. And because Timber [_augments_](/concepts/structuring-through-augmentation) your logs with metadata, you don't have to worry about making every log structured! 24 | -------------------------------------------------------------------------------- /docs/usage/custom-events.md: -------------------------------------------------------------------------------- 1 | Custom events allow you to extend beyond events already defined in [`timber.events`](https://timberio.github.io/timber-node/class/src/event.js~Event.html). If you aren't sure what an event is, please read the ["Metdata, Context, and Events" doc](/concepts/metadata-context-and-events). 2 | 3 | 4 | ## How to use it 5 | 6 | ```js 7 | // Using console.log: 8 | console.warn("Payment rejected", { 9 | event: { 10 | payment_rejected: { customer_id: "abcd1234", amount: 100, reason: "Card expired" } 11 | } 12 | }); 13 | 14 | // Using winston: 15 | winston.warn("Payment rejected", { 16 | event: { 17 | payment_rejected: { customer_id: "abcd1234", amount: 100, reason: "Card expired" } 18 | } 19 | }); 20 | 21 | // Using bunyan: 22 | logger.warn({ 23 | event: { 24 | payment_rejected: { customer_id: "abcd1234", amount: 100, reason: "Card expired" } 25 | } 26 | }, "Payment rejected"); 27 | ``` 28 | 29 | 1. [Search it](/app/console/searching) with queries like: `type:payment_rejected` or `payment_rejected.amount:>100` 30 | 2. [Alert on it](/app/alerts) with threshold based alerts 31 | 3. [View this event's data and context](/app/console/view-a-logs-metadata-and-context) 32 | 33 | 34 | ## How it works 35 | 36 | When this event is received by the Timber service we'll define a namespaced schema based on the event name. In this case, the namespace would be `payment_rejected`. The data structure of your log will look like: 37 | 38 | ```json 39 | { 40 | "message": "Payment rejected", 41 | "level": "warn", 42 | "event": { 43 | "custom": { 44 | "payment_rejected": { 45 | "customer_id": "abcd1234", 46 | "amount": 100, 47 | "reason": "Card expired" 48 | } 49 | } 50 | } 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/usage/readme.md: -------------------------------------------------------------------------------- 1 | Beyond your traditional logging statements, Timber offers a variety of ways to improve the quality of your Node logs. 2 | -------------------------------------------------------------------------------- /echo.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | http.createServer(function (request, response) { 4 | 5 | response.writeHead(200); 6 | 7 | var body = []; 8 | request.on('data', function (chunk) { 9 | body.push(chunk); 10 | }) 11 | .on('end', function () { 12 | body = Buffer.concat(body); 13 | console.log(JSON.parse(body)); 14 | }); 15 | 16 | request.pipe(response); 17 | 18 | }).listen(8080); 19 | 20 | console.log('listening on 8080'); 21 | 22 | // test it out with curl -H "Content-Type: application/json" -X POST -d 23 | // '{"username":"xyz","password":"xyz"}' http://localhost:8080 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "timber", 3 | "version": "3.1.3", 4 | "description": "Timber.io node client.", 5 | "license": "ISC", 6 | "main": "dist/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+ssh://git@github.com/timberio/timber-node.git" 10 | }, 11 | "engines": { 12 | "node": ">= 0.10.0" 13 | }, 14 | "maintainers": [ 15 | "Ben Johnson ", 16 | "David Antaramian ", 17 | "Garet McKinley ", 18 | "Zach Sherman " 19 | ], 20 | "author": { 21 | "name": "Zach Sherman ", 22 | "url": "https://github.com/zsherman" 23 | }, 24 | "scripts": { 25 | "watch": "babel src -d dist --experimental -w", 26 | "clean": "rimraf dist coverage", 27 | "lint": "eslint src", 28 | "format": "prettier --single-quote --trailing-comma es5 --no-semi --write src/**/*.js src/*.js", 29 | "compile": "babel -d dist src", 30 | "build": "npm run clean && babel -d dist src", 31 | "build:docs": "esdoc", 32 | "test": "jest --coverage", 33 | "test:watch": "jest --watch", 34 | "cover": "jest --coverage", 35 | "coveralls": "npm run compile && npm test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 36 | "postcoveralls": "rimraf coverage", 37 | "prepare": "npm run build", 38 | "postpublish": "git push origin master --follow-tags", 39 | "deploy": "git pull --rebase origin master && git push origin master", 40 | "deploy:docs": "gh-pages -d docs" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/timberio/timber-node/issues" 44 | }, 45 | "keywords": [ 46 | "es6", 47 | "node", 48 | "module", 49 | "timber", 50 | "logger", 51 | "logging", 52 | "structured logging", 53 | "node logging", 54 | "express logging", 55 | "morgan logging", 56 | "koa logging", 57 | "framework logging", 58 | "middleware" 59 | ], 60 | "homepage": "https://github.com/timberio/timber-node#readme", 61 | "devDependencies": { 62 | "babel-cli": "^6.22.2", 63 | "babel-eslint": "^7.1.1", 64 | "babel-jest": "^20.0.3", 65 | "babel-preset-es2015": "^6.22.0", 66 | "babel-preset-stage-0": "^6.22.0", 67 | "coveralls": "^2.11.4", 68 | "esdoc": "^0.5.2", 69 | "eslint": "^3.19.0", 70 | "eslint-config-prettier": "^2.1.1", 71 | "eslint-config-xo": "^0.18.2", 72 | "eslint-plugin-babel": "^4.1.1", 73 | "gh-pages": "^1.0.0", 74 | "jest-cli": "^20.0.4", 75 | "node-mocks-http": "^1.6.5", 76 | "prettier": "^1.4.2", 77 | "rimraf": "2.4.3" 78 | }, 79 | "dependencies": { 80 | "body-parser": "^1.17.2", 81 | "bunyan": "^1.8.12", 82 | "composable-middleware": "^0.3.0", 83 | "express-request-id": "^1.4.0", 84 | "find-package-json": "^1.0.0", 85 | "winston": "^2.3.1" 86 | }, 87 | "jest": { 88 | "testEnvironment": "node", 89 | "collectCoverageFrom": [ 90 | "src/**/*.js" 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import finder from 'find-package-json' 3 | 4 | const filename = (require.main && require.main.filename) || __dirname 5 | const projectPath = path.dirname(filename) 6 | const packageJson = finder(projectPath).next().value; 7 | const userConfig = packageJson && packageJson.timber; 8 | 9 | /** 10 | * The configuration options here are use throughout the timber library. 11 | * Any of the values can be changed in two different ways: 12 | * 13 | * ## Using your package.json 14 | * 15 | * To configure timber from your `package.json`, simply add a `timber` 16 | * object at the root level containing your desired overrides: 17 | * 18 | * ```json 19 | * "timber": { 20 | * "capture_request_body": true, 21 | * "capture_response_body": true 22 | * }, 23 | * ``` 24 | * 25 | * __Note:__ you cannot set the `debug_logger` option from the `package.json`. 26 | * This is because you must set it as a writeable stream. (see next section) 27 | * 28 | * ## Using inline overrides 29 | * 30 | * You can also configure timber by overriding the config options inline: 31 | * 32 | * ```js 33 | * const timber = require('timber'); 34 | * timber.config.debug_logger = process.stdout; 35 | * ``` 36 | * 37 | * __Note:__ inline overrides will override any options you have set 38 | * in your `package.json` file. 39 | * 40 | * @param {String} metadata_delimiter - delimiter between log message and log data (@metadata by default) 41 | * @param {boolean} append_metadata - append @metadata { ... } to all logs. If disabled, metadata will only be appended when `NODE_ENV === 'production'` (off by default) 42 | * @param {Writable} debug_logger - a writeable stream for internal debug messages to be sent to (disabled when undefined) 43 | * @param {boolean} timestamp_prefix - When `true`, log output should be prefixed with a timestamp in ISO 8601 format (off by default) 44 | */ 45 | const config = { 46 | _attached_stdout: false, 47 | _attached_stderr: false, 48 | logger: console, 49 | metadata_delimiter: '@metadata', // This should not be changed! The timber service only recognizes @metadata 50 | append_metadata: false, 51 | debug_logger: undefined, 52 | timestamp_prefix: false, 53 | ...userConfig, 54 | } 55 | 56 | export default config 57 | -------------------------------------------------------------------------------- /src/console.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module timber/console 3 | * 4 | * This module transparently augments console messages with structured data. Continue 5 | * to use `console` as you normally would and also pass an object as a second 6 | * parameter to log structured data. In the examples below you will notice logs are 7 | * appended with `@metadata ...`. This is what we mean by "augment". The timber.io 8 | * service will strip and parse this data. See timber.io/docs for more details. 9 | * 10 | * @example Logging a string 11 | * console.log('Hello world') 12 | * // Hello world @metadata {'dt': '2017-10-09T02:42:12.235421Z', 'level': 'info', ...} 13 | * 14 | * @example Logging a structured data 15 | * console.warn('Payent rejected', event: {payment_rejected: { customer_id: "abcd1234", amount: 100, reason: "Card expired" }}) 16 | * // Payent rejected @metadata {'dt': '2017-10-09T02:42:12.235421Z', 'level': 'warn', 'event': {'payment_rejected': {'customer_id': 'abcd1234', 'amount': 100, 'reason': 'Card expired'}}} 17 | */ 18 | 19 | import util from 'util' 20 | import config from './config' 21 | import LogEntry from './log_entry' 22 | 23 | /** 24 | * Transforms an ordinary console.log message into a structured Log object. 25 | * It also allows you to pass a Log object directly to a console.log function 26 | * It will automatically detect whether or not you are passing a structured 27 | * log into the console before attempting to transform it. 28 | * 29 | * This is also what is responsible for assigning the correct level to the log. 30 | * 31 | * @param {Array} args - argument list passed to console 32 | * @param {String} level - `info` `warn` `error` `debug` `fatal` 33 | */ 34 | const transformConsoleLog = ({ args, level }) => { 35 | // Allow custom metadata and event logging 36 | // https://github.com/timberio/timber-node/issues/41 37 | if ( 38 | args.length === 2 && 39 | typeof args[0] === 'string' && 40 | typeof args[1] === 'object' 41 | ) { 42 | if (args[1].meta && typeof args[1].meta === 'object') { 43 | return new LogEntry(args[0], { level, meta: { ...args[1].meta } }).format() 44 | } else if (args[1].event && typeof args[1].event === 'object') { 45 | return new LogEntry(args[0], { 46 | level, 47 | event: { custom: { ...args[1].event } }, 48 | }).format() 49 | } 50 | } 51 | const log = args[0] instanceof LogEntry 52 | ? args[0] 53 | : new LogEntry(`${util.format.apply(null, args)}\n`) 54 | log.setLevel(level) 55 | return log.format() 56 | } 57 | 58 | const originalConsole = { 59 | log: console.log, 60 | info: console.info, 61 | warn: console.warn, 62 | error: console.error 63 | } 64 | 65 | console.log = (...args) => 66 | config._attached_stdout || config.append_metadata 67 | ? process.stdout.write(transformConsoleLog({ args, level: 'info' })) 68 | : originalConsole.log(...args) 69 | 70 | console.info = (...args) => 71 | config._attached_stdout || config.append_metadata 72 | ? process.stdout.write(transformConsoleLog({ args, level: 'info' })) 73 | : originalConsole.info(...args) 74 | 75 | console.warn = (...args) => 76 | config._attached_stdout || config.append_metadata 77 | ? process.stdout.write(transformConsoleLog({ args, level: 'warn' })) 78 | : originalConsole.warn(...args) 79 | 80 | console.error = (...args) => 81 | config._attached_stderr || config.append_metadata 82 | ? process.stderr.write(transformConsoleLog({ args, level: 'error' })) 83 | : originalConsole.error(...args) 84 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the base class for all context types 3 | */ 4 | class Context { 5 | /** 6 | * required checks for the existence of the given attributes. 7 | * If any of the values provided are undefined, an error will be thrown 8 | * 9 | * @private 10 | * @param {object} attributes - key/value pair of required attributes 11 | */ 12 | required(attributes) { 13 | for (const attribute in attributes) { 14 | if (!attributes[attribute]) { 15 | throw new Error(`${attribute} is required`) 16 | } 17 | } 18 | } 19 | } 20 | 21 | export default Context 22 | -------------------------------------------------------------------------------- /src/contexts/http.js: -------------------------------------------------------------------------------- 1 | import Context from '../context' 2 | 3 | /** 4 | * The HTTP context adds data about the current HTTP request being processed 5 | * to your logs.This allows you to tail and filter by this data. 6 | */ 7 | class HTTP extends Context { 8 | static keyspace = 'http' 9 | 10 | constructor({ method, path, remote_addr, request_id } = {}) { 11 | super() 12 | 13 | // check for required attributes 14 | this.required({ method, path }) 15 | 16 | // bind context attributes to the class 17 | this.method = method 18 | this.path = path 19 | this.remote_addr = remote_addr 20 | this.request_id = request_id 21 | } 22 | } 23 | 24 | export default HTTP 25 | -------------------------------------------------------------------------------- /src/contexts/index.js: -------------------------------------------------------------------------------- 1 | import http from './http' 2 | 3 | export default { 4 | http, 5 | } 6 | -------------------------------------------------------------------------------- /src/data/errors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | transports: { 3 | winston: { 4 | stream: `You must provide a stream to the timber winston transport. 5 | Use: winston.add(timber.transports.Winston, { stream: new timber.transports.HTTPS('api-key') })`, 6 | }, 7 | }, 8 | log: { 9 | noMessage: 'You must supply a message when creating a log', 10 | }, 11 | install: { 12 | noTransport: 'No transport was provided.', 13 | }, 14 | attach: { 15 | notWritable: 'Stream must be of type Writable', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the base class for all event types 3 | */ 4 | class Event { 5 | /** 6 | * required checks for the existence of the given attributes. 7 | * If any of the values provided are undefined, an error will be thrown 8 | * 9 | * @private 10 | * @param {object} attributes - key/value pair of required attributes 11 | */ 12 | required(attributes) { 13 | for (const attribute in attributes) { 14 | if (!attributes[attribute] && attributes[attribute] !== 0) { 15 | throw new Error(`${attribute} is required`) 16 | } 17 | } 18 | } 19 | } 20 | 21 | export default Event 22 | -------------------------------------------------------------------------------- /src/events/custom.js: -------------------------------------------------------------------------------- 1 | import Event from '../event' 2 | 3 | /** 4 | * The Custom event allows you to past arbitrary events to timber. 5 | */ 6 | class Custom extends Event { 7 | /** 8 | * @param {String} [type] - This is the type of your event. It should be something unique and unchanging. It will be used to identify this event. 9 | * @param {Array} [data] - An object containing the event data 10 | */ 11 | constructor({ type, data } = {}) { 12 | super() 13 | 14 | // check for required attributes 15 | this.required({ type }) 16 | 17 | // bind context attributes to the class 18 | this.custom = { [type]: data } 19 | } 20 | } 21 | 22 | export default Custom 23 | -------------------------------------------------------------------------------- /src/events/http_request.js: -------------------------------------------------------------------------------- 1 | import Event from '../event' 2 | 3 | /** 4 | * The HTTP request event tracks incoming and outgoing 5 | * HTTP requests to your server. 6 | */ 7 | class HTTPRequest extends Event { 8 | /** 9 | * @param {String} [body] - the body of the request 10 | * @param {String} [direction] - incoming or outgoing 11 | * @param {Array} [headers] - the headers of the request 12 | * @param {String} host - the server's hostname 13 | * @param {String} method - `CONNECT` `DELETE` `GET` `HEAD` `OPTIONS` `PATCH` `POST` `PUT` `TRACE` 14 | * @param {String} [path] - the path of the request 15 | * @param {Number} [port] - the port of the request 16 | * @param {String} [query_string] - the query parameters present on the url 17 | * @param {String} [request_id] - the uuid attached to the request 18 | * @param {String} scheme - `HTTP` or `HTTPS` 19 | */ 20 | constructor( 21 | { 22 | body, 23 | direction, 24 | headers, 25 | host, 26 | method, 27 | path, 28 | port, 29 | query_string, 30 | request_id, 31 | scheme, 32 | } = {} 33 | ) { 34 | super() 35 | 36 | // check for required attributes 37 | this.required({ host, method, scheme }) 38 | 39 | // bind context attributes to the class 40 | this.body = body 41 | this.direction = direction 42 | this.headers = headers 43 | this.host = host 44 | this.method = method 45 | this.path = path 46 | this.port = port 47 | this.query_string = query_string 48 | this.request_id = request_id 49 | this.scheme = scheme 50 | } 51 | 52 | message() { 53 | return `Started ${this.method} "${this.path}"` 54 | } 55 | } 56 | 57 | export default HTTPRequest 58 | -------------------------------------------------------------------------------- /src/events/http_response.js: -------------------------------------------------------------------------------- 1 | import Event from '../event' 2 | 3 | /** 4 | * The HTTP server request event tracks incoming HTTP requests to your HTTP server. 5 | */ 6 | class HTTPResponse extends Event { 7 | /** 8 | * @param {String} [body] - the body of the request 9 | * @param {String} [direction] - incoming or outgoing 10 | * @param {Array} [headers] - the headers of the request 11 | * @param {Object} [request] - the request object (only set if combine_http_events is true) 12 | * @param {String} [request_id] - the uuid of the request 13 | * @param {String} status - the HTTP status code 14 | * @param {String} time_ms - the total duration of the request in milliseconds 15 | */ 16 | constructor( 17 | { body, direction, headers, request, request_id, status, time_ms } = {} 18 | ) { 19 | super() 20 | 21 | // check for required attributes 22 | this.required({ status, time_ms }) 23 | 24 | // bind context attributes to the class 25 | this.body = body 26 | this.direction = direction 27 | this.headers = headers 28 | this.request = request 29 | this.request_id = request_id 30 | this.status = status 31 | this.time_ms = time_ms 32 | } 33 | 34 | message() { 35 | const parts = ['Outgoing HTTP response'] 36 | 37 | if (this.service_name) { 38 | parts.push(`from ${this.service_name}`) 39 | } 40 | 41 | parts.push(`${this.status} in ${this.time_ms}ms`) 42 | 43 | return parts.join(' ') 44 | } 45 | } 46 | 47 | export default HTTPResponse 48 | -------------------------------------------------------------------------------- /src/events/index.js: -------------------------------------------------------------------------------- 1 | import Custom from './custom' 2 | import HTTPRequest from './http_request' 3 | import HTTPResponse from './http_response' 4 | 5 | export { Custom, HTTPRequest, HTTPResponse } 6 | -------------------------------------------------------------------------------- /src/formatters/index.js: -------------------------------------------------------------------------------- 1 | import Winston from './winston' 2 | 3 | export default { Winston } 4 | -------------------------------------------------------------------------------- /src/formatters/winston.js: -------------------------------------------------------------------------------- 1 | import { Custom } from '../events' 2 | import LogEntry from '../log_entry' 3 | 4 | const WinstonFormatter = ({ 5 | message: raw, 6 | level, 7 | meta: metadata, 8 | timestamp, 9 | }) => { 10 | const message = timestamp ? `${timestamp()} - ${raw}` : raw 11 | const structuredLog = new LogEntry(message, { level }) 12 | const { event, context, ...meta } = metadata 13 | 14 | // If custom metadata was provided with the log, append it 15 | if (Object.keys(meta).length) { 16 | structuredLog.append({ meta }) 17 | } 18 | 19 | // If the event key exists, append a custom event 20 | if (event) { 21 | for (const eventName in event) { 22 | if (!event[eventName]) continue 23 | structuredLog.append({ 24 | event: new Custom({ type: eventName, data: event[eventName] }), 25 | }) 26 | } 27 | } 28 | 29 | // If a context object was provided with the log, append it 30 | if (context) { 31 | structuredLog.append({ 32 | context, 33 | }) 34 | } 35 | 36 | return structuredLog.format() 37 | } 38 | 39 | export default WinstonFormatter 40 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // This is the main file that gets referenced by node 2 | import attach from './utils/attach' 3 | import config from './config' 4 | import install from './install' 5 | import middlewares from './middlewares' 6 | import transports from './transports' 7 | import formatters from './formatters' 8 | import events from './events' 9 | import contexts from './contexts' 10 | import LogEntry from './log_entry' 11 | import './console' 12 | 13 | module.exports = { 14 | attach, 15 | config, 16 | contexts, 17 | events, 18 | formatters, 19 | install, 20 | LogEntry, 21 | middlewares, 22 | transports, 23 | } 24 | -------------------------------------------------------------------------------- /src/install.js: -------------------------------------------------------------------------------- 1 | import attach from './utils/attach' 2 | import errors from './data/errors' 3 | 4 | /** 5 | * Installs the timber logger to route all stdout logs to the provided stream 6 | * 7 | * @param {Stream} transport - the stream that all logs will go through 8 | */ 9 | function install(transport) { 10 | if (!transport) throw Error(errors.install.noTransport) 11 | 12 | // attach our transport stream to stdout/stderr 13 | attach([transport], process.stdout) 14 | attach([transport], process.stderr) 15 | } 16 | 17 | export default install 18 | -------------------------------------------------------------------------------- /src/log_entry.js: -------------------------------------------------------------------------------- 1 | import config from './config' 2 | import errors from './data/errors' 3 | 4 | const JSON_SCHEMA_URL = 'https://raw.githubusercontent.com/timberio/log-event-json-schema/v3.1.3/schema.json'; 5 | 6 | /** 7 | * This class is instantiated before 8 | * Transforms a log message or object into a rich structured format 9 | * that timber expects, ex 'log message' @timber.io {"dt": "…", "level": "info", "context": {…}} 10 | * see https://github.com/timberio/log-event-json-schema for specs 11 | */ 12 | class LogEntry { 13 | /** 14 | * @param {String} message - the log message before transforming 15 | * @param {Object} [context] - context to be attached to message 16 | */ 17 | constructor(message, context = {}) { 18 | // Throw an error if no message is provided 19 | if (!message) throw new Error(errors.log.noMessage) 20 | 21 | /** 22 | * Reference to original log message 23 | * @type {String} 24 | */ 25 | this.raw = message 26 | 27 | /** 28 | * Structured log data 29 | * @type {Date} 30 | */ 31 | this.data = { 32 | $schema: JSON_SCHEMA_URL, 33 | dt: new Date(), 34 | message, 35 | ...context, 36 | } 37 | } 38 | 39 | /** 40 | * Adds the to the log entry. A log entry can only contain a single 41 | * event. 42 | * 43 | * @param {Event} event 44 | */ 45 | addEvent(event) { 46 | this.append({event: event}) 47 | } 48 | 49 | /** 50 | * Appends data to the end of the structured log object 51 | * 52 | * @param {Object} data 53 | */ 54 | append(data) { 55 | this.data = { 56 | ...this.data, 57 | ...data, 58 | } 59 | } 60 | 61 | /** 62 | * Convenience function for setting the log level 63 | * 64 | * @param {String} level - `info` `warn` `error` `debug` 65 | */ 66 | setLevel(level) { 67 | this.append({ level }) 68 | } 69 | 70 | /** 71 | * Transforms the structured log into a string 72 | * i.e. `Log message @metadata { ... }` 73 | */ 74 | format({ withMetadata = true } = {}) { 75 | const { dt, message, ...rest } = this.data 76 | 77 | let log = this.raw.endsWith('\n') 78 | ? this.raw.substring(0, this.raw.length - 1) 79 | : this.raw 80 | 81 | if (config.timestamp_prefix) { 82 | log = `${dt.toISOString()} ${log}` 83 | } 84 | 85 | if (withMetadata) { 86 | const data = config.timestamp_prefix ? rest : { dt, ...rest } 87 | log += ` ${config.metadata_delimiter} ${JSON.stringify(data)}` 88 | } 89 | 90 | return `${log}\n` 91 | } 92 | } 93 | 94 | export default LogEntry 95 | -------------------------------------------------------------------------------- /src/middlewares/express.js: -------------------------------------------------------------------------------- 1 | // import transform from '../transform' 2 | import compose from 'composable-middleware' 3 | import addRequestId from 'express-request-id' 4 | import bodyParser from 'body-parser' 5 | import HTTPContext from '../contexts/http' 6 | import { HTTPRequest, HTTPResponse } from '../events' 7 | import log from '../utils/log' 8 | import config from '../config' 9 | 10 | /** 11 | * The express middleware takes care of automatically logging 12 | * each http event with the appropriate context events attached. 13 | * 14 | * This middleware is composed of three separate middlewares: 15 | * - `addRequestId` automatically attaches a unique uuid to every request 16 | * - `bodyParser` allows parsing of JSON encoded request bodies 17 | * - `expressMiddleware` automatically logs http events to timber 18 | * 19 | * @param {object} [options] - An object with configuration options 20 | * @param {object} [options.logger] - A custom logger to log http events to (usually either: console, winston, or bunyan) 21 | * @param {boolean} [options.capture_request_body] - Whether the http request body data will be captured (off by default) 22 | * @param {boolean} [options.combine_http_events] - If true, HTTPRequest and HTTPResponse events will be combined in a single log message (off by defaut) 23 | */ 24 | const expressMiddleware = ({ ...options }) => { 25 | // If a custom logger was provided, use it to log http events 26 | if (options.logger) { 27 | config.logger = options.logger 28 | } 29 | return compose( 30 | addRequestId(), 31 | bodyParser.json(), 32 | (req, res, next) => { 33 | // save a reference of the start time so that we can determine 34 | // the amount of time each http request takes 35 | req.start_time = new Date().getTime() 36 | 37 | // destructure the request object for ease of use 38 | const { 39 | headers: { host, ...headers }, 40 | method, 41 | id: request_id, 42 | path, 43 | protocol: scheme, 44 | body: reqBody, 45 | connection, 46 | } = req 47 | 48 | // determine the ip address of the client 49 | // https://stackoverflow.com/a/10849772 50 | const remote_addr = headers['x-forwarded-for'] || connection.remoteAddress 51 | 52 | // send the request body if the capture_request_body flag is true (off by default) 53 | // and the request body is not empty 54 | let body = options.capture_request_body && Object.keys(reqBody).length > 0 55 | ? JSON.stringify(reqBody) 56 | : undefined 57 | 58 | // create the HTTP context item 59 | const http_context = new HTTPContext({ 60 | method, 61 | path, 62 | request_id, 63 | remote_addr, 64 | }) 65 | 66 | // add the http context information to the metadata object 67 | const metadata = { 68 | context: { 69 | http: http_context, 70 | }, 71 | } 72 | 73 | const http_request = new HTTPRequest({ 74 | direction: 'incoming', 75 | body, 76 | host, 77 | path, 78 | request_id, 79 | scheme, 80 | method, 81 | }) 82 | 83 | // add the http_request event to the metadata object 84 | metadata.event = { http_request } 85 | 86 | 87 | // Override the response end event 88 | // This event will send the http_client_response event to timber 89 | // If combine_http_events is true, this will be the only log generated 90 | const end = res.end 91 | res.end = (chunk, encoding) => { 92 | // Emit the original res.end event 93 | res.end = end 94 | res.end(chunk, encoding) 95 | 96 | // destructure the response object for ease of use 97 | const { body: resBody, statusCode: status } = res 98 | 99 | // calculate the duration of the http request 100 | const time_ms = new Date().getTime() - req.start_time 101 | 102 | // send the response body if the capture_response_body flag is true (off by default) 103 | body = options.capture_response_body ? JSON.stringify(resBody) : undefined 104 | 105 | const http_response = new HTTPResponse({ 106 | direction: 'outgoing', 107 | request_id, 108 | time_ms, 109 | status, 110 | body, 111 | }) 112 | 113 | // If we're combining http events, append the request event 114 | if (options.combine_http_events) { 115 | http_response.request = http_request 116 | } 117 | 118 | // add the http_response event to the metadata object 119 | metadata.event = { http_response } 120 | 121 | const message = options.combine_http_events 122 | ? `${method} ${host}${path} - ${status} in ${time_ms}ms` 123 | : http_response.message() 124 | 125 | // log the http response with metadata 126 | log('info', message, metadata) 127 | } 128 | 129 | // If we're not combining http events, log the http request 130 | if (!options.combine_http_events) { 131 | log('info', http_request.message(), metadata) 132 | } 133 | next() 134 | } 135 | ) 136 | } 137 | 138 | export default expressMiddleware 139 | -------------------------------------------------------------------------------- /src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | import express from './express' 2 | import koa from './koa' 3 | 4 | export default { express, koa } 5 | -------------------------------------------------------------------------------- /src/middlewares/koa.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vectordotdev/timber-node/dccaccad5e7a3e2c11c51bf3e956717719f42fb2/src/middlewares/koa.js -------------------------------------------------------------------------------- /src/transports/bunyan.js: -------------------------------------------------------------------------------- 1 | import bunyan from 'bunyan' 2 | import { Writable } from 'stream' 3 | import { Custom } from '../events' 4 | import errors from '../data/errors' 5 | import LogEntry from '../log_entry' 6 | 7 | /** 8 | * The Timber Bunyan transport allows you to seamlessly install 9 | * Timber in your apps that use bunyan as the logger. 10 | */ 11 | class BunyanTransport extends Writable { 12 | /** 13 | * @param {Object} [options] - Configuration options for the transport 14 | * @param {string} [options.stream] - Stream to write to 15 | */ 16 | constructor({ stream = process.stdout, ...options } = {}) { 17 | if (!stream) { 18 | throw new Error(errors.transports.bunyan.stream) 19 | } 20 | 21 | super(options) 22 | 23 | this.name = 'timberBunyan' 24 | this.level = options.level || 'info' 25 | 26 | // Attach the provided stream 27 | this.stream = stream 28 | } 29 | 30 | /** 31 | * @param {buffer|string} [chunk] - The chunk to be written. Will always be a buffer unless the decodeStrings option was set to false or the stream is operating in object mode. 32 | * @param {string} [encoding] - If the chunk is a string, then encoding is the character encoding of that string. If chunk is a Buffer, or if the stream is operating in object mode, encoding may be ignored. 33 | * @param {function} [next] - Call this function (optionally with an error argument) when processing is complete for the supplied chunk. 34 | */ 35 | _write(chunk, encoding, next) { 36 | // Parse the JSON object 37 | const data = JSON.parse(chunk.toString()) 38 | const { msg, event, context, meta } = data 39 | // Convert the level integer into a string representation 40 | const level = bunyan.nameFromLevel[data.level] 41 | 42 | // Create a structured log object out of the log message 43 | const structuredLog = new LogEntry(msg, { level }) 44 | 45 | // If custom metadata was provided with the log, append it 46 | if (meta && Object.keys(meta).length) { 47 | structuredLog.append({ meta }) 48 | } 49 | 50 | // If the event key exists, append a custom event 51 | if (event) { 52 | for (const eventName in event) { 53 | if (!event[eventName]) continue 54 | structuredLog.append({ 55 | event: new Custom({ type: eventName, data: event[eventName] }), 56 | }) 57 | } 58 | } 59 | 60 | // If a context object was provided with the log, append it 61 | if (context) { 62 | structuredLog.append({ 63 | context, 64 | }) 65 | } 66 | 67 | // Write our structured log to the timber https stream 68 | this.stream.write(structuredLog.format()) 69 | next() 70 | } 71 | } 72 | 73 | export default BunyanTransport 74 | -------------------------------------------------------------------------------- /src/transports/https.js: -------------------------------------------------------------------------------- 1 | import https from 'https' 2 | import { Writable } from 'stream' 3 | import debug from '../utils/debug' 4 | const HOSTNAME = 'logs.timber.io' 5 | const PATH = '/frames' 6 | const CONTENT_TYPE = 'application/json' 7 | const USER_AGENT = `Timber Node HTTPS Stream/${require('../../package.json').version}` 8 | const PORT = 443 9 | 10 | /** 11 | * A highly efficient stream for sending logs to Timber via HTTPS. It uses batches, 12 | * keep-alive connections (and in the future maybe msgpack) to deliver logs with high-throughput 13 | * and little overhead. It also implements the Stream.Writable interface so that it can be treated 14 | * like a stream. This is beneficial when using something like Morgan, where you can pass a custom stream. 15 | */ 16 | class HTTPS extends Writable { 17 | /** 18 | * @param {string} apiKey - Timber API Key 19 | * @param {Object} [options] - Various options to adjust the stream behavior. 20 | * @param {string} [options.flushInterval=1000] - How often, in milliseconds, the messages written to the stream should be delivered to Timber. 21 | * @param {string} [options.httpsAgent] - Your own custom https.Agent. We use agents to maintain connection pools and keep the connections alive. This avoids the initial connection overhead every time we want to communicate with Timber. See https.Agent for options. 22 | */ 23 | constructor( 24 | apiKey, 25 | { 26 | flushInterval = 1000, 27 | highWaterMark = 5000, 28 | httpsAgent, 29 | httpsClient, 30 | hostName = HOSTNAME, 31 | path = PATH, 32 | port = PORT, 33 | } = {} 34 | ) { 35 | // Ensure we use object mode and set a default highWaterMark 36 | super({ objectMode: true, highWaterMark }) 37 | debug('Initializing HTTPS transport stream') 38 | 39 | this.acceptsObject = true 40 | this.apiKey = apiKey 41 | this.hostName = hostName 42 | this.path = path 43 | this.port = port 44 | this.flushInterval = flushInterval 45 | this.httpsAgent = 46 | httpsAgent || 47 | new https.Agent({ 48 | maxSockets: 5, 49 | }) 50 | this.httpsClient = httpsClient || https 51 | 52 | // Cork the stream so we can utilize the internal Buffer. We do *not* want to 53 | // send a request for every message. The _flusher will take care of flushing the stream 54 | // on an interval. 55 | this.cork() 56 | 57 | // In the event the _flusher is not fast enough, we need to monitor the buffer size. 58 | // If it fills before the next flush event, we should immediately flush. 59 | 60 | if (flushInterval !== undefined && flushInterval > 0) { 61 | debug('Starting stream flusher') 62 | this._startFlusher() 63 | } 64 | } 65 | 66 | /** 67 | * _writev is a Stream.Writeable methods that, if present, will write multiple chunks of 68 | * data off of the buffer. Defining it means we do not need to define _write. 69 | */ 70 | _writev(chunks, next) { 71 | debug(`Sending ${chunks.length} log to stream`) 72 | const messages = chunks.map(chunk => chunk.chunk) 73 | const body = JSON.stringify(messages) 74 | const options = { 75 | headers: { 76 | 'Content-Type': CONTENT_TYPE, 77 | 'User-Agent': USER_AGENT, 78 | }, 79 | agent: this.httpsAgent, 80 | auth: this.apiKey, 81 | hostname: this.hostName, 82 | port: this.port, 83 | path: this.path, 84 | method: 'POST', 85 | } 86 | 87 | // Add debug outputs for every possible request event 88 | // This should help debugging network related issues 89 | debug(`Instantiating req object`) 90 | const req = this.httpsClient.request(options, res => { 91 | debug(`${this.hostName} responded with ${res.statusCode}`) 92 | res.on('aborted', () => debug('Response event: aborted')) 93 | res.on('close', () => debug('Response event: close')) 94 | }) 95 | 96 | req.on('error', (err) => debug('Error connecting to logs.timber.io:', err)) 97 | req.on('abort', () => debug('Request event: abort')) 98 | req.on('aborted', () => debug('Request event: aborted')) 99 | req.on('connect', () => debug('Request event: connect')) 100 | req.on('continue', () => debug('Request event: continue')) 101 | req.on('response', () => debug('Request event: response')) 102 | req.on('socket', sock => { 103 | debug('Request event: socket') 104 | sock.on('close', () => debug('Socket event: close')) 105 | sock.on('connect', () => debug('Socket event: connect')) 106 | sock.on('data', () => sock.end()) 107 | sock.on('drain', () => debug('Socket event: drain')) 108 | sock.on('end', () => debug('Socket event: end')) 109 | sock.on('error', () => debug('Socket event: error')) 110 | sock.on('lookup', () => debug('Socket event: lookup')) 111 | sock.on('drain', () => debug('Socket event: drain')) 112 | }) 113 | req.on('upgrade', () => debug('Request event: upgrade')) 114 | 115 | req.write(body) 116 | req.end() 117 | next() 118 | } 119 | 120 | _write(chunk, encoding, next) { 121 | this._writev([{ chunk, encoding }], next) 122 | } 123 | 124 | /** 125 | * Expressive function to flush the buffer contents. uncork flushes the buffer and write 126 | * the contents. Cork allows us to continue buffering the messages until the next flush. 127 | */ 128 | _flush() { 129 | // nextTick is recommended here to allow batching of write calls I think 130 | process.nextTick(() => { 131 | this.uncork() 132 | this.cork() 133 | }) 134 | } 135 | 136 | /** 137 | * Interval to call _flush continuously. This ensures log lines get sent on this.flushInterval 138 | * intervals. 139 | */ 140 | _startFlusher() { 141 | setInterval(() => this._flush(), this.flushInterval) 142 | } 143 | } 144 | 145 | export default HTTPS -------------------------------------------------------------------------------- /src/transports/index.js: -------------------------------------------------------------------------------- 1 | import HTTPS from './https' 2 | import Bunyan from './bunyan' 3 | 4 | module.exports = { HTTPS, Bunyan } 5 | -------------------------------------------------------------------------------- /src/utils/attach.js: -------------------------------------------------------------------------------- 1 | import { Writable } from 'stream' 2 | import { stripMetadata } from '../utils/metadata' 3 | import errors from '../data/errors' 4 | import config from '../config' 5 | import debug from './debug' 6 | import LogEntry from '../log_entry' 7 | 8 | /** 9 | * Attaches a transport stream to a writeable stream. 10 | * 11 | * @param {Array} transports - array of transports to attach to the stream 12 | * @param {Writable} toStream - the stream your transport will attach to 13 | * @param {Object} options - configuration options 14 | * @param {boolean} options.applyBackPressure 15 | */ 16 | const attach = (transports, toStream, { applyBackPressure = false } = {}) => { 17 | // Ensure all the streams are Writable 18 | for (let i = 0; i < transports.length; i++) { 19 | if (!(transports[i] instanceof Writable)) { 20 | throw new Error(errors.attach.notWritable) 21 | } 22 | } 23 | 24 | // Store refs to standard logging utilities 25 | const originalWrite = toStream.write 26 | 27 | debug(`attaching ${transports.length} transports to stream`) 28 | 29 | toStream.write = (message, encoding, fd) => { 30 | const log = message instanceof LogEntry ? message : new LogEntry(message) 31 | 32 | for (let i = 0; i < transports.length; i++) { 33 | const transport = transports[i] 34 | 35 | const written = transport.write( 36 | transport.acceptsObject ? log.data : log.data.message, 37 | encoding, 38 | fd 39 | ) 40 | 41 | if (!written && applyBackPressure) { 42 | transport.once('drain', () => transport.write(...arguments)) 43 | } 44 | } 45 | 46 | // When writing the log to the original stream, 47 | // strip the metadata if we're not in production 48 | originalWrite.apply(toStream, [ 49 | config.append_metadata || process.env.NODE_ENV === 'production' 50 | ? log.data.message 51 | : stripMetadata(log.data.message), 52 | ]) 53 | } 54 | 55 | if (toStream === process.stdout) { 56 | config._attached_stdout = true 57 | } else if (toStream === process.stderr) { 58 | config._attached_stderr = true 59 | } 60 | 61 | return { 62 | detach: () => { 63 | toStream.write = originalWrite 64 | }, 65 | } 66 | } 67 | 68 | export default attach 69 | -------------------------------------------------------------------------------- /src/utils/debug.js: -------------------------------------------------------------------------------- 1 | import util from 'util' 2 | import config from '../config' 3 | 4 | /** 5 | * Convenience function for retrieving a reference to 6 | * the debug_logger stream. 7 | * 8 | * @private 9 | */ 10 | export const debug_logger = () => config.debug_logger 11 | 12 | /** 13 | * Generate a timestamp string to use in debug lines 14 | * 15 | * @private 16 | */ 17 | const timestamp = () => new Date().toISOString() 18 | 19 | /** 20 | * Convenience function for logging debug messages 21 | * to the configured debug_logger 22 | * 23 | * This works much like the builtin console.log function, 24 | * accepting any amount of mixed arguments and concatenating 25 | * them into a single string to be sent to the debug_logger stream 26 | * 27 | * @private 28 | * @param {...*} args 29 | */ 30 | const debug = (...args) => { 31 | if (debug_logger()) { 32 | debug_logger().write(`[${timestamp()}]: ${util.format.apply(null, args)}\n`) 33 | } 34 | } 35 | 36 | export default debug 37 | -------------------------------------------------------------------------------- /src/utils/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @private 3 | * 4 | * This module is meant to be *private* and should not be used directly. 5 | * It's an internal function used by the Timber library to log within our 6 | * integrations. It an abstraction on top of the various loggers our clients 7 | * could use, ensuring we use the proper logger within each integration. 8 | * 9 | * For example, take Express. We provide a single middleware for capturing context 10 | * and logging HTTP request and response events. We need to log to winston if the 11 | * client is using winston, or the console if they are not. But a client should know 12 | * which logger they are using and use that directly. 13 | */ 14 | 15 | import config from '../config' 16 | import LogEntry from '../log_entry' 17 | 18 | const loggers = { 19 | console: { 20 | detect: () => config.logger.constructor.name === 'Console' || config.logger.constructor.name === 'CustomConsole', 21 | handler: (level, message, metadata) => { 22 | if (metadata) { 23 | return config.logger[level](new LogEntry(message, metadata)) 24 | } 25 | return config.logger[level](message) 26 | }, 27 | }, 28 | winston: { 29 | detect: () => 30 | config.logger.Container && 31 | config.logger.Logger && 32 | config.logger.Transport, 33 | handler: (level, message, metadata = {}) => 34 | config.logger.log(level, message, metadata), 35 | }, 36 | bunyan: { 37 | detect: () => config.logger.constructor.name === 'Logger', 38 | handler: (level, message, metadata) => { 39 | config.logger[level](metadata, message) 40 | }, 41 | }, 42 | } 43 | 44 | const log = (...args) => { 45 | // Iterate through the loggers object to detect 46 | // which logger is set in the timber config. 47 | for (const name in loggers) { 48 | // If we successfully detected the logger... 49 | if (loggers[name].detect()) { 50 | // Pass the provded arguments to the logger 51 | return loggers[name].handler(...args) 52 | } 53 | } 54 | } 55 | 56 | export default log 57 | -------------------------------------------------------------------------------- /src/utils/metadata.js: -------------------------------------------------------------------------------- 1 | export const stripMetadata = log => `${log.split(' @metadata')[0]}\n` 2 | -------------------------------------------------------------------------------- /test/attach.test.js: -------------------------------------------------------------------------------- 1 | import timber from '../src'; 2 | import attach from '../src/utils/attach.js'; 3 | import '../src/console.js'; 4 | import { Writable, Readable } from 'stream'; 5 | 6 | // timber.config.append_metadata = false 7 | 8 | class TestWriteStream extends Writable { 9 | constructor() { super({ objectMode: true }) } 10 | _write(){} 11 | } 12 | class TestReadStream extends Readable {} 13 | 14 | describe('Connect STDOUT', () => { 15 | 16 | it('intercepts stdout write', () => { 17 | const log = 'test log...'; 18 | 19 | // Create a new write stream and cork it 20 | // to keep the data in the buffer 21 | let testStream = new TestWriteStream(); 22 | const connectedStream = attach([testStream], process.stdout); 23 | testStream.cork(); 24 | 25 | // Write the sample message to stdout 26 | process.stdout.write(log); 27 | 28 | // Bring back the original stdout.write 29 | connectedStream.detach(); 30 | 31 | // Since we've detached, this shouldn't be added to the stream 32 | process.stdout.write("\n"); 33 | 34 | // Check that the buffered content is correct 35 | const written = testStream._writableState.getBuffer().pop().chunk; 36 | expect(written).toBe(log); 37 | }); 38 | 39 | it('sets the proper level for console.log', () => { 40 | const log = 'console log test'; 41 | let testStream = new TestWriteStream(); 42 | attach([testStream], process.stdout); 43 | testStream.cork(); 44 | 45 | console.log(log); 46 | 47 | const chunk = testStream._writableState.getBuffer().pop().chunk; 48 | expect(chunk).toMatch(log); 49 | expect(chunk).toMatch('info'); 50 | }); 51 | 52 | it('sets the proper level for console.warn', () => { 53 | const log = 'console log test'; 54 | let testStream = new TestWriteStream(); 55 | attach([testStream], process.stdout); 56 | testStream.cork(); 57 | 58 | console.warn(log); 59 | 60 | const chunk = testStream._writableState.getBuffer().pop().chunk; 61 | 62 | expect(chunk).toMatch(log); 63 | expect(chunk).toMatch('warn'); 64 | }); 65 | 66 | it('sets the proper level for console.error', () => { 67 | const log = 'console log test'; 68 | let testStream = new TestWriteStream(); 69 | attach([testStream], process.stderr); 70 | testStream.cork(); 71 | 72 | console.error(log); 73 | 74 | const chunk = testStream._writableState.getBuffer().pop().chunk; 75 | 76 | expect(chunk).toMatch(log); 77 | expect(chunk).toMatch('error'); 78 | }); 79 | 80 | it("throws an error when not passed a Writable stream", () => { 81 | let testStream = new TestReadStream(); 82 | expect(() => { 83 | attach([testStream], process.stdout); 84 | }).toThrow(); 85 | }); 86 | 87 | it("does not throw an error when instantiated properly", () => { 88 | let testStream = new TestWriteStream(); 89 | expect(() => { 90 | attach([testStream], process.stdout); 91 | }).not.toThrow(); 92 | }); 93 | 94 | }); 95 | -------------------------------------------------------------------------------- /test/console.test.js: -------------------------------------------------------------------------------- 1 | import { TestWriteStream, TestReadStream } from './support/test_streams'; 2 | import attach from '../src/utils/attach.js'; 3 | import '../src/console'; 4 | 5 | describe('Console', () => { 6 | it('should patch console.log', () => { 7 | const log = 'test console log'; 8 | const level = 'info'; 9 | 10 | // Create a new write stream and cork it 11 | // to keep the data in the buffer 12 | const testStream = new TestWriteStream(); 13 | attach([testStream], process.stdout); 14 | testStream.cork(); 15 | 16 | // Write the sample message to stdout 17 | console.log(log); 18 | 19 | // Check that the buffered content is correct 20 | const written = testStream._writableState.getBuffer().pop().chunk; 21 | 22 | // If the output is equal to the log message, its not patched 23 | expect(written).not.toBe(log); 24 | // The log should start with the original message 25 | expect(written.startsWith(`${log} @metadata`)).toBe(true); 26 | }); 27 | it('should patch console.info', () => { 28 | const log = 'test console log'; 29 | const level = 'info'; 30 | 31 | // Create a new write stream and cork it 32 | // to keep the data in the buffer 33 | const testStream = new TestWriteStream(); 34 | attach([testStream], process.stdout); 35 | testStream.cork(); 36 | 37 | // Write the sample message to stdout 38 | console.info(log); 39 | 40 | // Check that the buffered content is correct 41 | const written = testStream._writableState.getBuffer().pop().chunk; 42 | 43 | // If the output is equal to the log message, its not patched 44 | expect(written).not.toBe(log); 45 | // The log should start with the original message 46 | expect(written.startsWith(`${log} @metadata`)).toBe(true); 47 | }); 48 | it('should patch console.warn', () => { 49 | const log = 'test console log'; 50 | const level = 'info'; 51 | 52 | // Create a new write stream and cork it 53 | // to keep the data in the buffer 54 | const testStream = new TestWriteStream(); 55 | attach([testStream], process.stdout); 56 | testStream.cork(); 57 | 58 | // Write the sample message to stdout 59 | console.warn(log); 60 | 61 | // Check that the buffered content is correct 62 | const written = testStream._writableState.getBuffer().pop().chunk; 63 | 64 | // If the output is equal to the log message, its not patched 65 | expect(written).not.toBe(log); 66 | // The log should start with the original message 67 | expect(written.startsWith(`${log} @metadata`)).toBe(true); 68 | }); 69 | it('should patch console.error', () => { 70 | const log = 'test console log'; 71 | const level = 'info'; 72 | 73 | // Create a new write stream and cork it 74 | // to keep the data in the buffer 75 | const testStream = new TestWriteStream(); 76 | attach([testStream], process.stderr); 77 | testStream.cork(); 78 | 79 | // Write the sample message to stdout 80 | console.error(log); 81 | 82 | // Check that the buffered content is correct 83 | const written = testStream._writableState.getBuffer().pop().chunk; 84 | 85 | // If the output is equal to the log message, its not patched 86 | expect(written).not.toBe(log); 87 | // The log should start with the original message 88 | expect(written.startsWith(`${log} @metadata`)).toBe(true); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import Index from '../src/index'; 2 | 3 | describe('Entry File', () => { 4 | 5 | it('exports middlewares', () => { 6 | expect(typeof Index.middlewares).not.toBeUndefined(); 7 | }); 8 | 9 | it('exports a client', () => { 10 | expect(typeof Index.Client).not.toBeUndefined(); 11 | }); 12 | 13 | it('exports transport methods', () => { 14 | expect(typeof Index.transports).not.toBeUndefined(); 15 | }); 16 | 17 | it('exports stdout connect', () => { 18 | expect(typeof Index.connect).not.toBeUndefined(); 19 | }); 20 | 21 | }); -------------------------------------------------------------------------------- /test/install.test.js: -------------------------------------------------------------------------------- 1 | import install from '../src/install'; 2 | import { Readable, Writable } from 'stream'; 3 | 4 | describe('Install', () => { 5 | it('exports a function', () => { 6 | expect(typeof install).toBe('function'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /test/log.test.js: -------------------------------------------------------------------------------- 1 | import LogEntry from '../src/log_entry'; 2 | import config from '../src/config'; 3 | 4 | describe('Log Transformer', () => { 5 | it('exports a function', () => { 6 | expect(typeof LogEntry).toBe('function'); 7 | }); 8 | 9 | it('requires a message', () => { 10 | expect(() => new LogEntry()).toThrow(); 11 | }); 12 | 13 | it('stores a data object', () => { 14 | const message = 'Test log line'; 15 | const log = new LogEntry(message); 16 | 17 | expect(typeof log.data).toBe('object'); 18 | expect(log.data.message).toBe(message); 19 | }); 20 | 21 | it('append properly appends data values', () => { 22 | const message = 'Test log line'; 23 | const log = new LogEntry(message); 24 | 25 | log.append({ customAttribute: 'test' }); 26 | 27 | expect(log.data.customAttribute).toBe('test'); 28 | }); 29 | 30 | it('append replaces existing data values', () => { 31 | const message = 'Test log line'; 32 | const log = new LogEntry(message); 33 | 34 | log.append({ level: 'error' }); 35 | expect(log.data.level).toBe('error'); 36 | 37 | log.append({ level: 'info' }); 38 | expect(log.data.level).toBe('info'); 39 | }); 40 | 41 | it('setLevel properly sets the log level', () => { 42 | const message = 'Test log line'; 43 | const log = new LogEntry(message); 44 | 45 | expect(typeof log.setLevel).toBe('function'); 46 | 47 | log.setLevel('warn') 48 | 49 | expect(log.data.level).toBe('warn'); 50 | }); 51 | 52 | it('formats log properly', () => { 53 | const message = 'Test log line'; 54 | const log = new LogEntry(message); 55 | const regex = new RegExp(`${message} ${config.metadata_delimiter} {.*}`); 56 | 57 | expect(typeof log.format).toBe('function'); 58 | expect(log.format()).toMatch(regex); 59 | }); 60 | 61 | it('format respects withMetadata option', () => { 62 | const message = 'Test log line'; 63 | const log = new LogEntry(message); 64 | 65 | expect(log.format({ withMetadata: false })).toBe(`${message}\n`); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/loggers/winston.test.js: -------------------------------------------------------------------------------- 1 | import attach from '../../src/utils/attach.js'; 2 | import { Writable, Readable } from 'stream'; 3 | import winston from 'winston'; 4 | 5 | class TestWriteStream extends Writable { 6 | constructor() { super({ objectMode: true }) } 7 | _write(){} 8 | } 9 | class TestReadStream extends Readable {} 10 | 11 | describe('Winston', () => { 12 | it('captures Winston info logs', () => { 13 | const log = 'test winston log...'; 14 | const level = 'info'; 15 | 16 | // Create a new write stream and cork it 17 | // to keep the data in the buffer 18 | let testStream = new TestWriteStream(); 19 | const detach = attach([testStream], process.stdout); 20 | testStream.cork(); 21 | 22 | // Write the sample message to stdout 23 | winston.log(level, log); 24 | 25 | // Check that the buffered content is correct 26 | const written = testStream._writableState.getBuffer().pop().chunk; 27 | expect(written).toBe(`${level}: ${log}\n`); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/middlewares/express.test.js: -------------------------------------------------------------------------------- 1 | import attach from '../../src/utils/attach.js'; 2 | import httpMocks from 'node-mocks-http'; 3 | import expressMiddleware from '../../src/middlewares/express'; 4 | 5 | describe('Express Middleware', () => { 6 | it('log the events properly', () => { 7 | // let logs = []; 8 | // var storeLog = inputs => (logs.push(inputs)); 9 | // console["info"] = jest.fn(storeLog); 10 | 11 | // var req = httpMocks.createRequest({ 12 | // id: 1234, 13 | // url: '/user/42', 14 | // method: 'GET', 15 | // headers: { 16 | // 'host': 'google.com', 17 | // 'x-forwarded-for': '192.12.321.23' 18 | // }, 19 | // protocol: 'https' 20 | // }); 21 | 22 | // var res = httpMocks.createResponse({}); 23 | 24 | // var mw = expressMiddleware(); 25 | // var next = () => { }; 26 | // mw(req, res, next); 27 | 28 | // // Check that the buffered content is correct 29 | // //const written = testStream._writableState.getBuffer().pop().chunk; 30 | 31 | // // If the output is equal to the log message, its not patched 32 | // var log = logs[0]; 33 | // expect(log.constructor.name).toBe("Augment"); 34 | // expect(log.data.context.http.method).toBe("GET"); 35 | // expect(log.data.event.http_request.direction).toBe("incoming"); 36 | }) 37 | }) -------------------------------------------------------------------------------- /test/mocks/request.js: -------------------------------------------------------------------------------- 1 | const logs = [ 2 | "test log 1..." 3 | "test log 2..." 4 | ]; 5 | 6 | export default function request(url) { 7 | return new Promise((resolve, reject) => { 8 | process.nextTick( 9 | () => resolve(logs) 10 | ); 11 | }); 12 | } -------------------------------------------------------------------------------- /test/support/test_streams.js: -------------------------------------------------------------------------------- 1 | import { Writable, Readable } from 'stream'; 2 | 3 | class TestWriteStream extends Writable { 4 | constructor() { super({ objectMode: true }) } 5 | _write(){} 6 | } 7 | class TestReadStream extends Readable {} 8 | 9 | export { TestWriteStream, TestReadStream } 10 | -------------------------------------------------------------------------------- /test/transports/bunyan.test.js: -------------------------------------------------------------------------------- 1 | import { Writable, Readable } from 'stream'; 2 | import bunyan from 'bunyan'; 3 | import BunyanTransport from '../../src/transports/bunyan'; 4 | 5 | class TestWriteStream extends Writable { 6 | constructor() { super({ objectMode: true }) } 7 | _write(){} 8 | } 9 | class TestReadStream extends Readable {} 10 | 11 | describe('Bunyan Transport', () => { 12 | it('should use stdout as default stream', () => { 13 | const log = bunyan.createLogger({ 14 | name: 'Timber Logger' 15 | }); 16 | expect(log.streams[0].stream).toBe(process.stdout); 17 | }) 18 | 19 | // it('can be added to winston', () => { 20 | // const stream = new TestWriteStream(); 21 | 22 | // expect(() => { 23 | // const log = bunyan.createLogger({ 24 | // name: 'Timber Logger', 25 | // stream: new BunyanTransport({ stream }) 26 | // }); 27 | // }).not.toThrow(); 28 | // }) 29 | 30 | // it('logs messages to stream', () => { 31 | // const message = 'Test log message'; 32 | // const level = 'info'; 33 | // const stream = new TestWriteStream(); 34 | // stream.cork(); 35 | 36 | // const log = bunyan.createLogger({ 37 | // name: 'Timber Logger', 38 | // stream: new BunyanTransport({ stream }) 39 | // }); 40 | 41 | // log[level](message); 42 | 43 | // const written = stream._writableState.getBuffer().pop().chunk; 44 | 45 | // expect(written.message).toBe(message); 46 | // expect(written.level).toBe(level); 47 | // }) 48 | 49 | // it('augments logs with custom context', () => { 50 | // const message = 'Test log message'; 51 | // const level = 'info'; 52 | // const context = { foo: 'bar' }; 53 | // const stream = new TestWriteStream(); 54 | // stream.cork(); 55 | 56 | // const log = bunyan.createLogger({ 57 | // name: 'Timber Logger', 58 | // stream: new BunyanTransport({ stream }) 59 | // }); 60 | 61 | // log[level]({ context }, message); 62 | 63 | // const written = stream._writableState.getBuffer().pop().chunk; 64 | 65 | // expect(written.context).toMatchObject(context); 66 | // }) 67 | 68 | // it('augments logs with custom events', () => { 69 | // const message = 'Test log message'; 70 | // const level = 'info'; 71 | // const event = { test_event_name: { foo: 'bar' } }; 72 | // const stream = new TestWriteStream(); 73 | // stream.cork(); 74 | 75 | // const log = bunyan.createLogger({ 76 | // name: 'Timber Logger', 77 | // stream: new BunyanTransport({ stream }) 78 | // }); 79 | 80 | // log[level]({ event }, message); 81 | 82 | // const written = stream._writableState.getBuffer().pop().chunk; 83 | 84 | // expect(written.event).toMatchObject({ custom: event }); 85 | // }) 86 | 87 | // it('augments logs with metadata', () => { 88 | // const message = 'Test log message'; 89 | // const level = 'info'; 90 | // const meta = { foo: 'bar' }; 91 | // const stream = new TestWriteStream(); 92 | // stream.cork(); 93 | 94 | // const log = bunyan.createLogger({ 95 | // name: 'Timber Logger', 96 | // stream: new BunyanTransport({ stream }) 97 | // }); 98 | 99 | // log[level](meta, message); 100 | 101 | // const written = stream._writableState.getBuffer().pop().chunk; 102 | 103 | // expect(written.meta).toMatchObject(meta); 104 | // }) 105 | }) 106 | 107 | -------------------------------------------------------------------------------- /test/transports/https.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import EventEmitter from 'events'; 4 | import HTTPSStream from '../../src/transports/https'; 5 | 6 | // Allows time travel for ticks/timeouts/intervals 7 | jest.useFakeTimers(); 8 | 9 | // Mimics http.Request behavior so that we can assert usage below. 10 | class FakeRequest extends EventEmitter { 11 | constructor(options = {}) { 12 | super(); 13 | this.endCallCount = 0; 14 | this.options = options; 15 | this.writtenMessages = []; 16 | } 17 | 18 | end() { 19 | this.endCallCount++; 20 | } 21 | 22 | write(message) { 23 | this.writtenMessages.push(message); 24 | } 25 | } 26 | 27 | // Mimics https behavior so that we can assert usage below. 28 | class FakeHTTPSClient { 29 | constructor(fakeRequest) { 30 | this.fakeRequest = null; 31 | this.requestCallCount = 0; 32 | } 33 | 34 | request(options) { 35 | this.requestCallCount += 1; 36 | return this.fakeRequest = new FakeRequest(options); 37 | } 38 | } 39 | 40 | describe("HTTPS Stream", () => { 41 | 42 | describe("initialization", () => { 43 | 44 | it("sets the apiKey", () => { 45 | let httpsStream = new HTTPSStream('my_api_key', {}); 46 | expect(httpsStream.apiKey).toBe('my_api_key'); 47 | }); 48 | 49 | it("sets the flushInterval default", () => { 50 | let httpsStream = new HTTPSStream('my_api_key'); 51 | expect(httpsStream.flushInterval).toBe(1000); 52 | }); 53 | 54 | it("starts the flusher and flushes messages", () => { 55 | let fakeHTTPSClient = new FakeHTTPSClient(); 56 | let httpsStream = new HTTPSStream('my_api_key', { 57 | httpsClient: fakeHTTPSClient, 58 | flushInterval: 1000 59 | }); 60 | 61 | expect(fakeHTTPSClient.requestCallCount).toBe(0); 62 | 63 | // Send a few messages 64 | httpsStream.write('message 1'); 65 | httpsStream.write('message 2'); 66 | 67 | jest.runOnlyPendingTimers(); 68 | jest.runAllTicks(); 69 | 70 | expect(fakeHTTPSClient.requestCallCount).toBe(1); 71 | }); 72 | }); 73 | 74 | describe("transport", function() { 75 | it("sends a bulk HTTP request to Timber with 1 message", () => { 76 | let fakeHTTPSClient = new FakeHTTPSClient(); 77 | let httpsStream = new HTTPSStream('my_api_key', { httpsClient: fakeHTTPSClient, flushInterval: 2500 }); 78 | 79 | // Write the message and flush it 80 | httpsStream.write('message 1'); 81 | httpsStream._flush(); 82 | jest.runAllTicks(); 83 | 84 | expect(fakeHTTPSClient.requestCallCount).toBe(1); 85 | 86 | let fakeRequest = fakeHTTPSClient.fakeRequest; 87 | const messages = fakeRequest.writtenMessages; 88 | 89 | expect(messages.length).toBe(1); 90 | expect(JSON.parse(messages[0])[0]).toBe('message 1'); 91 | expect(fakeRequest.endCallCount).toBe(1); 92 | }); 93 | 94 | it("sends a bulk HTTP request to Timber with multiple messages", () => { 95 | let fakeHTTPSClient = new FakeHTTPSClient(); 96 | let httpsStream = new HTTPSStream('my_api_key', { httpsClient: fakeHTTPSClient, flushInterval: 2500 }); 97 | httpsStream.write('message 1'); 98 | httpsStream.write('message 2'); 99 | httpsStream._flush(); 100 | jest.runAllTicks(); 101 | 102 | expect(fakeHTTPSClient.requestCallCount).toBe(1); 103 | 104 | let fakeRequest = fakeHTTPSClient.fakeRequest; 105 | const messages = fakeRequest.writtenMessages; 106 | 107 | expect(messages.length).toBe(1); 108 | expect(JSON.parse(messages[0])[0]).toBe('message 1'); 109 | expect(JSON.parse(messages[0])[1]).toBe('message 2'); 110 | expect(fakeRequest.endCallCount).toBe(1); 111 | }); 112 | 113 | // it("handles logs sent as strings", () => {}); 114 | // it("handles logs sent as objects", () => {}); 115 | // it("throws a warning when the buffer is full", () => {}); 116 | // it("does not crash when the /frames endpoint times out", () => {}); 117 | // it("retries requests when backPressure is allowed", () => {}); 118 | 119 | }); 120 | }); 121 | 122 | // assert(requestOptions.agent); 123 | // assert.equal(requestOptions.auth, 'my_api_key'); 124 | // assert.equal(requestOptions.hostname, 'api.timber.io'); 125 | // assert.equal(requestOptions.path, '/frames'); 126 | // assert.equal(requestOptions.headers['Content-Type'], 'application/msgpack'); 127 | // assert(requestOptions.headers['User-Agent'].startsWith('Timber Node HTTPS Stream')); --------------------------------------------------------------------------------