A library, plus a Chrome DevTools extension.
Blog post: http://guigrpa.github.io/2016/07/18/untangling-spaghetti-logs/
These docs are for Storyboard v3. Docs for v2 are also available, but you're encouraged to upgrade!
- Hierarchical stories: put logs in context (stories), and group them in higher-order stories; they can be a life-saver with concurrent user actions and async events.
- End-to-end stories: see all client and server tasks triggered by a user action (a click on the Login button, maybe) in a single place.
- Storyboard DevTools Chrome extension: view client and server logs with a clean and detail-rich interface, including advanced features such as remote monitoring (for mobile devices and non-Chrome browsers) and relative timestamps.
- Storyboard CLI: wrap any application with it (no changes required) to monitor it remotely with the Storyboard DevTools.
- Real-time push of server logs to the Storyboard DevTools extension via WebSockets, with opt-in client-server clock synchronization. Even more: control the level of detail you get from various parts of your server remotely, without relaunching.
- Secure server logs: remote access is opt-in, and can be authenticated.
- Attach anything to your logs for further investigation.
- Plug-in architecture. Available plugins include Console (& Parallel Console), WebSocket Server & Client, File, (PostgreSQL) Database, and Browser Extension, but you can write your own too!
- Lightweight. Plugins are now (v3) available separately, so you only need to bring in the dependencies you actually use.
- Rich filter options: give logs source and severity attributes and apply fine-grained filtering, with white and black lists.
- Colorful: use color to convey meaning and importance. Storyboard extends the popular chalk library so that it can also be used on the browser.
- Enjoy the simple-yet-powerful API.
- Flow-compatible (with zero config).
For the simplest possible Storyboard installation (for more options, check out the Listeners section below):
$ npm install --save storyboard storyboard-preset-console
If you only want the (less powerful) CLI tool, see this section.
To install the Storyboard DevTools Chrome extension, get it from the Chrome Web Store. Optional, but highly recommended! After installing it, open the Storyboard pane in the Chrome DevTools and point your browser to a Storyboard-equipped page (see below for how to use the library).
Hopefully the next sections will convince you of the benefits of adding Storyboard to your project. If you don't want to modify your existing application but still want to use the Storyboard DevTools or other Storyboard features, you can use the sb
CLI tool:
$ npm install -g storyboard-cli
$ sb --server ls
2016-07-15T17:26:33.974Z storyboard INFO ┌── ROOT STORY [CREATED]
2016-07-15T17:26:33.975Z storyboard INFO Log filter: *:DEBUG
2016-07-15T17:26:34.151Z storyboard INFO Logs available via web on port 8090
2016-07-15T17:26:34.154Z main INFO CHANGELOG.md
2016-07-15T17:26:34.155Z main INFO LICENSE
2016-07-15T17:26:34.155Z main INFO README.md
2016-07-15T17:26:34.155Z main INFO ROADMAP.md
2016-07-15T17:26:34.155Z main INFO chromeExtension
2016-07-15T17:26:34.155Z main INFO coverage
...
You can pipe stdin
and stdout
in the standard way:
$ sb ls | head -n 3
2016-07-15T14:41:47.573Z storyboard INFO ┌── ROOT STORY [CREATED]
2016-07-15T14:41:47.574Z storyboard INFO Log filter: *:DEBUG
2016-07-15T14:41:47.601Z main INFO CHANGELOG.md
$ ls | sb -- head -n 3
2016-07-15T14:41:52.174Z storyboard INFO ┌── ROOT STORY [CREATED]
2016-07-15T14:41:52.176Z storyboard INFO Log filter: *:DEBUG
2016-07-15T14:41:52.201Z main INFO CHANGELOG.md
2016-07-15T14:41:52.201Z main INFO LICENSE
2016-07-15T14:41:52.201Z main INFO README.md
2016-07-15T14:41:52.202Z main INFO
2016-07-15T14:41:52.203Z storyboard INFO └── ROOT STORY [CLOSED]
Note the use of the --
separator: options before the separator are passed to the sb
tool; after the separator, they are passed to the called application.
Here are the CLI tool configuration options:
$ sb --help
Usage: sb [options] <command> [args...]
Options:
-h, --help output usage information
-V, --version output the version number
--no-console Disable console output
--stderr Enable stderr for errors
--no-colors Disable color output
-f, --file <path> Save logs to file
-s, --server Launch web server for logs
-p, --port <port> Port for web server
import { mainStory } from 'storyboard';
import 'storyboard-preset-console';
mainStory.info('Hello world!');
We're using the storyboard-preset-console
preset for convenience, which is equivalent to:
import { mainStory, addListener } from 'storyboard';
import consoleListener from 'storyboard-listener-console';
addListener(consoleListener);
mainStory.info('Hello world!');
See more details on plugins in Listeners below.
mainStory.trace('Teeny-weeny detail: x = 3, y = 4');
mainStory.debug('Called login()');
mainStory.info('User "admin" authenticated successfully');
mainStory.warn('Sad we can\'t show colors in GFM');
mainStory.error('User "admin" could not be authenticated', { attach: err });
mainStory.fatal('Ooops! Crashed! Mayday!', { attach: fatalError });
// ...
// 2016-03-09T16:18:19.659Z main WARN Sad we can't show colors in GFM
// 2016-03-09T16:18:19.672Z main ERROR User "admin" could not be authenticated
// 2016-03-09T16:18:19.672Z main ERROR name: 'Error'
// 2016-03-09T16:18:19.672Z main ERROR message: 'AUTHENTICATION_ERROR'
// 2016-03-09T16:18:19.672Z main ERROR stack: Error: AUTHENTICATION_ERROR
// 2016-03-09T16:18:19.672Z main ERROR stack: at repl:3:11
// ...
Maybe you noticed that the trace
call produces no output by default. See Log filtering to fine-tune your filters.
Namespace your logs for readability, as well as to allow finer-grained filtering later on.
mainStory.info('http', 'GET /api/item/25');
mainStory.info('db', 'Fetching item 25...');
// 2016-03-09T16:29:51.943Z http INFO GET /api/item/25
// 2016-03-09T16:31:52.231Z db INFO Fetching item 25...
Use colors to emphasize/de-emphasize parts of your logs:
import { mainStory, chalk } from 'storyboard';
mainStory.info('http', `GET ${chalk.green.bold('/api/item/26')}`);
mainStory.info('db', `Fetching item ${chalk.green.bold('26')}...`);
// 2016-03-09T16:29:51.943Z http INFO GET /api/item/26
// 2016-03-09T16:31:52.231Z db INFO Fetching item 26...
As seen above, we recommend using the popular chalk library by Sindre Sorhus. Chalk is automatically extended by Storyboard for use in the browser. If you prefer another ANSI-color library, make sure it's universal and doesn't disable itself automatically in the browser.
Attach anything to your logs that might provide additional context: an object, an array, an exception, a simple value... Don't worry about circular references, long buffers, or undefined
! Use the attach
option to display it as a tree, or attachInline
for a more compact, JSON.stringify
-ed version.
You can also use the attachLevel
option to control the (severity) level of the detailed object logs (by default: the same level of the main logged line). Pro tip: use the trace
level for long attachments (hidden by default), so that they don't pollute your console but are still accessible via the Storyboard DevTools extension.
mainStory.info('test', 'A simple object', { attachInline: obj1 });
// 2016-03-09T16:51:16.436Z test INFO A simple object -- {"foo":2,"bar":3}
mainStory.info('test', 'An object with a circular reference', {
attach: obj2,
attachLevel: 'debug',
});
// 2016-03-09T16:52:48.882Z test INFO An object with a circular reference
// 2016-03-09T16:52:48.882Z test DEBUG foo: 2
// 2016-03-09T16:52:48.882Z test DEBUG bar: 3
// 2016-03-09T16:52:48.882Z test DEBUG circularRef: [CIRCULAR]
mainStory.info('test', 'This message is logged', {
attach: butThisHugeObjectIsNot,
attachLevel: 'trace',
});
// 2017-02-17T16:03:23.124Z test INFO This message is logged
// [attachment is hidden; inspect it in the Storyboard DevTools]
Note: attach
and attachInline
have no effect on the way attachments are shown in the Storyboard DevTools.
Inspired by the popular debug library, Storyboard allows you to filter logs according to source, specifying white and black lists and using wildcards. Beyond that, you can specify the minimum severity level you are interested in, depending on the source:
*:DEBUG
(default) or*
will include logs from all sources, as long as they have severitydebug
or higher.*:*
will include absolutely all logs.foo
orfoo:DEBUG
will include logs fromfoo
but exclude all other sources.-test, *:*
will include all logs, except those from sourcetest
.foo, bar:INFO, -test, *:WARN
will include logs fromfoo
(DEBUG
or higher),bar
(INFO
or higher), and all other sources (WARN
or higher), but exclude sourcetest
.ba*:*, -basket
will include all logs frombar
,baz
, etc. but exclude sourcebasket
.
In Node, you can configure log filtering via the STORYBOARD
environment variable (have a look at cross-env for a cross-platform setup):
# OS X / Linux
$ STORYBOARD=*:* node myScript
# Windows
$ set "STORYBOARD=*:*" && node myScript
In the browser, use localStorage
:
localStorage.STORYBOARD = '*:*'
Alternatively, you can configure the log filters programatically:
import { config } from 'storyboard';
config({ filter: '*:*' });
And even more convenient: configure filters remotely and without reloading by using the Storyboard DevTools.
Create child stories by calling child()
on the parent story and passing an options argument. Don't forget to close()
the child story when you're done with it! More on child stories here.
const story = mainStory.child({
src: 'lib',
title: 'Little Red Riding Hood',
level: 'DEBUG',
});
story.info('Once upon a time...');
story.warn('...a wolf appeared!...');
story.info('...and they lived happily ever after.');
story.close();
// 2016-03-19T14:10:14.080Z lib DEBUG ┌── Little Red Riding Hood [CREATED]
// 2016-03-19T14:10:14.083Z main INFO Once upon a time...
// 2016-03-19T14:10:14.085Z main WARN ...a wolf appeared!...
// 2016-03-19T14:10:14.087Z main INFO ...and they lived happily ever after.
// 2016-03-19T14:10:14.088Z lib DEBUG └── Little Red Riding Hood [CLOSED]
Pro tip: Child stories have INFO
level by default, and can be completely hidden by log filtering. However, when a log with level WARN
or higher is added to a hidden story, the story and all of its ancestors will become visible. You will not miss any errors, nor the actions that led to them!
Logs emitted by stories are relayed by the Storyboard Hub to all attached listeners. A Hub exists at the core of every Storyboard instance. Here is an example of a typical configuration, with server and client-side Hubs (other use cases have proved possible in production):
Several listeners are readily available as separate packages:
-
Console (
storyboard-listener-console
): formats logs and sends them to the console (also in the browser). -
Parallel Console (
storyboard-listener-console-parallel
): shows parallel, top-level stories in the console, with support for resizing. -
WebSocket Server (
storyboard-listener-ws-server
): encapsulates logs and pushes them to WebSocket clients. Used jointly with the WebSocket Client and Browser Extension, it allows remote access to server stories. -
WebSocket Client (
storyboard-listener-ws-client
): downloads logs from the WebSocket Server, and optionally uploads client logs to the server for remote monitoring. -
Browser Extension (
storyboard-listener-browser-extension
): relays logs to the Storyboard DevTools. -
File (
storyboard-listener-file
): saves logs to file. -
PostgreSQL Database (
storyboard-listener-db-postgres
): saves logs to a PostgreSQL database, including attachments, story hierarchy, etc.
Check out the full listener configuration options.
More listeners can be added by the user (see the API), e.g. to support different databases, integrate with other services, etc. Get inspired by winston's and bunyan's plugins.
The simplest way to add remote access to a Node application's logs is to enable the WebSocket Server listener:
// Server
import { addListener } from 'storyboard';
import wsServerListener from 'storyboard-listener-ws-server';
addListener(wsServerListener);
You now have a standalone HTTP server at port 8090 (by default) and can use the Storyboard DevTools to see your logs.
You can also integrate the log server functionality with your own application server. This may be desirable if you want to use a single port, or if you want to see end-to-end stories. In this case, your client application should enable the WebSocket Client and Browser Extension listeners:
// Client
import { addListener } from 'storyboard';
import wsClientListener from 'storyboard-listener-ws-client';
import browserExtListener from 'storyboard-listener-browser-extension';
addListener(wsClientListener);
addListener(browserExtListener);
At the server side, initialize the WebSocket Server listener with either your http
Server
instance, or your socket.io Server
instance, depending on your case:
// If your application doesn't use WebSockets:
import express from 'express';
import http from 'http';
const httpServer = http.createServer(express());
httpServer.listen(3000);
addListener(wsServerListener, { httpServer });
// If your application uses socket.io WebSockets without socket auth:
import socketio from 'socket.io';
const socketServer = socketio(httpServer);
addListener(wsServerListener, { socketServer });
// If your application uses sockets with auth, namespace them
// so that they don't clash with the log server's:
// At the server...
const io = socketServer.of('/myApp');
io.use(myAuthMiddleware);
io.on('connection', myConnectFunction);
// ...and at the client:
const socket = socketio.connect('/myApp')
Now when you open your client-side application, you can see both server and client logs in the Storyboard DevTools.
You can add prevent unauthorized access to your logs via a listener option:
addListener(wsServerListener, {
authenticate: ({ login, password }) => isAuthorized(login, password),
});
In some cases, you may want to remotely monitor client logs, e.g. if you are building a mobile web app, or you want to see the logs generated in non-Chrome browsers for which there is (currently) no browser extension.
For these cases, you can configure your WebSocket Client listener so that it uploads its logs to the server, which can then provide remote access to them:
import { addListener } from 'storyboard';
import wsClientListener from 'storyboard-listener-ws-client';
addListener(wsClientListener, { uploadClientStories: true });
Client logs will not pollute the server's own log, and will appear as a separate remote client story in the Storyboard DevTools, along with a short description of the remote platform:
The icing on the cake is linking server- and client-side stories to get a complete picture of what is triggered by a user action (see video at the top of this page).
Storyboard provides a simple yet flexible way to achieve this: stories can have multiple parents, which are specified upon creation. This feature is leveraged by the Storyboard DevTools: when it receives a new story from the server with multiple parents, it checks whether any of the parents is a client-side story. If so, it prioritizes this parent for display purposes, since it is expected to provide more context.
For this to work, the client's storyId
must be transmitted to the server somehow. This example uses the URL query string for simplicity, but feel free to use whatever technique you want (the body of a POST
request, your own WebSocket messaging scheme, etc.):
// Client:
const onClick = async () => {
const story = mainStory.child({
src: 'itemList',
title: 'User click on Refresh',
});
try {
story.info('itemList', 'Fetching items...');
const response = await fetch(`/items?storyId=${story.storyId}`);
const items = await response.json();
story.info('itemList', `Fetched ${items.length} items`);
} finally {
story.close();
}
};
// Server (using Express):
import express from 'express';
const app = express();
app.get('/items', (req, res) => {
const { storyId } = req.query;
const story = mainStory.child({
src: 'http',
title: `HTTP request ${req.url}`,
extraParents: storyId != null ? [storyId] : undefined,
});
story.info('http', 'Processing request...');
// ...
res.json(items);
story.close();
});
Want to see the end-to-end story? Use the Storyboard DevTools extension.
Note: end-to-end stories work better when server and client system clocks are not too different. Servers are typically NTP-synchronized, as are most modern PCs with Internet access. If this is not the case, enable Storyboard's time synchronization function (available since v2.0.0):
import { addListener } from 'storyboard';
import wsClientListener from 'storyboard-listener-ws-client';
addListener(wsClientListener, { clockSync: true });
Enable the link to the browser extension in your application:
import { addListener } from 'storyboard';
import browserExtListener from 'storyboard-listener-browser-extension';
addListener(browserExtListener);
After installing the Chrome extension, open the Chrome DevTools, select the Storyboard pane and point your browser at either:
- Your standard application URL, to see both server and client logs
- Port 8090 (configurable) of your server, to see server logs only (+ uploaded client logs)
Some highlighted features:
- Modify the server's filter configuration without restarting it.
- Show stories chronologically (flat) or hierarchically (tree): hover on the story title for the button to appear.
- Collapse/expand stories: click on the caret. Even when stories are collapsed, detect that they contain an error or warning thanks to a special icon.
- Open attachments and exceptions: click on the folder icon.
- Choose among 3 timestamp formats: UTC, local or relative to now: click on any timestamp.
- Set reference timestamps: right-click or control-click on any timestamp.
- Use quick find (case-insensitive) to highlight what you're looking for.
- Squash identical, consecutive messages into a convenient summary line.
- Configure when and how Storyboard forgets old logs and stories.
- Customize colors to your heart's content!
You can check out your new extension navigating to: https://storyboard-examples-ifkpkpoyhz.now.sh/ (might be a bit slow at first; free hosting!)
Storyboard DevTools is built with React, Redux and Redux-Saga.
Copyright (c) Guillermo Grau Panea 2016-now
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.