├── .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'));
--------------------------------------------------------------------------------