This tutorial shows you how to control Seneca’s logging output. Clone the main Seneca repository from github, and open the _doc/examples_folder.
You’ll use the Sales Tax example code. This code shows you how to handle sales tax rules using Seneca. Take a look at the main README for details. For this tutorial, you’ll focus on the logging output.
Here’s some code to calculate sales tax. It won’t work, because you haven’t actually told Seneca how to do that yet.
var seneca = require('seneca')()
seneca.act({cmd: 'salestax', net: 100}, function (err, result) {
if (err) return console.error(err)
console.log(result.total)
})
This invokes a Seneca action that, hopefully, calculates sales tax. Arbitrarily you’re using the property cmd
to
indicate what you want done (calculate sales tax), and net
is net price before tax. The callback
function returns
the total price, and uses the standard Node.js signature (error object as first parameter).
Let’s try to run this code, even though it will fail. In the examples folder, this code is saved in the file sales-tax-error.js
. Run this file using Node.js, and you’ll see the following output:
$ node sales-tax-error.js
[-isodate-] INFO init start
[-isodate-] INFO init end
[-isodate-] ERROR fail seneca/act_not_found
Seneca: act(args,cb):
action not found for args = {"cmd":"salestax","net":100}
{ [Error: ...] }
Seneca outputs some logging information so you can track what’s going
on. The INIT
entries log the start and end of the initialization phase, when Seneca loads plugins.
The ERROR
entry tells you what went wrong: no action pattern matched the input args in the JSON document:
{"cmd":"salestax","net":100}
. The code also prints the JavaScript Error object to the console. That’s the line:
if( err ) return console.error(err);
You can fix this by defining an action:
seneca.add( {cmd:'salestax'}, function(args,callback){
var rate= 0.23
var total = args.net * (1+rate)
callback(null,{total:total})
})
The file sales-tax.js
in the examples folder contains the new code. Run it:
$ node sales-tax.js
[-isodate-] INFO init start
[-isodate-] INFO init end
123
Well that worked! 23% sales tax on a price of $100 gives a total of $123. Fabulous!
You might find that logging output annoying. Turn it off with:
$ node sales-tax.js --seneca.log.quiet
123
Or you might be a logging freak, in which case, here’s the all-you-can-eat version:.
node sales-tax.js --seneca.log.print
[-isodate-] INFO init start
... lots of init stuff ...
[-isodate-] INFO init end
[-isodate-] INFO add {cmd=salestax}
[-isodate-] DEBUG act in 90xkee {cmd=salestax,net=100}
[-isodate-] DEBUG act out 90xkee {total=123}
123
So you might be wondering how to get finer-grained logging output. Logging can be filtered on:
level
: DEBUG
, INFO
, WARN
, ERROR
, FATAL
type
: short string code, examples: init
, plugin
, error
, …plugin
: the names of the plugintag
: an identifier tag, used when you have multiple instances of the same pluginLet do that now:
node sales-tax.js --seneca.log=type:act
[-isodate-] DEBUG act in actid0 {cmd=salestax,net=100}
[-isodate-] DEBUG act out actid0 {total=123}
123
The command line argument --seneca.log
accepts a
comma-separated list of filters. The filter type:act
means only
output the log entries of type act
. The type
is the third
field. The act
entries are very low level debugging logs
showing the operation of individual actions. The logs show the input
arguments, in
, and the eventual output of the
action out
. Because these can be separated in time, a random
action identifier (above: actid0
) is generated for each action, so that you
can match up the input and output.
Let’s turn the sales tax calculator into a plugin. This plugin accepts two configuration options. You can specify the tax rate, and the country which applies that rate (using two letter country codes).
Here’s the client code, in the file sales-tax-log.js
:
var seneca = require('seneca')()
seneca.use('sales-tax-plugin', {country: 'IE', rate: 0.23})
seneca.use('sales-tax-plugin', {country: 'UK', rate: 0.20})
seneca.ready(function (err) {
if (err) return process.exit(!console.error(err))
seneca.act({role: 'shop', cmd: 'salestax', country: 'IE', net: 100})
seneca.act({role: 'shop', cmd: 'salestax', country: 'UK', net: 200})
seneca.act({role: 'shop', cmd: 'salestax', country: 'UK', net: 300})
})
Since you’re using log files to trace the commands, you can drop the
callback function from the act
method call.
Now you need a plugin - that’s in the sales-tax-plugin.js
file:
module.exports = function (options) {
var seneca = this
var plugin = 'shop'
var country = options.country || 'IE'
var rate = options.rate || 0.23
var calc = function (net) {
return net * (1 + rate)
}
seneca.add({ role: plugin, cmd: 'salestax', country: country }, function (args, callback) {
var total = calc(parseFloat(args.net, 10))
seneca.log.debug('apply-tax', args.net, total, rate, country)
callback(null, { total: total })
})
seneca.add({ role: plugin, cmd: 'salestax' }, function (args, callback) {
var total = calc(parseFloat(args.net, 10))
seneca.log.debug('apply-tax', args.net, total, rate, country)
callback(null, { total: total })
})
seneca.act({ role: 'web', use: {
prefix: 'shop/',
pin: { role: 'shop', cmd: '*' },
map: {
salestax: { GET: true }
}
}})
return {
name: plugin
}
}
The plugin creates a separate instance of the salestax
object
for each country and one instance that matches a call with no country. This object stores the country rate, country code,
and the number of times that sales tax for that country is calculated
(hit count).
This plugin follows the standard format for Seneca plugins. You provide a function that takes
a set of options and it’s called in the Seneca context (eg the this
object). Using the Seneca instance, you add some actions, and
return a plugin object:
module.exports = function (options) {
var seneca = this
seneca.add( { ... }, function (args, callback) {
...
})
return {
name: 'plugin-name'
}
}
Run this code, and filter the log to show only debug output from the sales-tax plugin:
$ node sales-tax-log.js --seneca.log=plugin:shop
[-isodate-] DEBUG plugin sales-tax IE annv4h
net: 100 total: 123 tax: {hits=1,rate=0.23,country=IE}
[-isodate-] DEBUG plugin sales-tax UK 3rkaa2
net: 200 total: 240 tax: {hits=1,rate=0.2,country=UK}
[-isodate-] DEBUG plugin sales-tax UK cwxcts
net: 300 total: 360 tax: {hits=2,rate=0.2,country=UK}
These logs appear because the plugin calls seneca.log.debug
and provides the information about the sales tax calculation:
seneca.add({ role: plugin, cmd: 'salestax', country: country }, function (args, callback) {
var total = calc(parseFloat(args.net, 10))
seneca.log.debug('apply-tax', args.net, total, rate, country)
callback(null, { total: total })
})
The object seneca.log
has convenience functions for the built-in log levels:
These functions concatenate all their arguments into an array, which is the content of the log message. This array is then formatted for display or storage by a handler function (which you can provide). To minimize impact on performance, log data is only evaluated if a matching log filter is active. The last argument to the logging function can be a function (which should return an array of values), which again is only executed if a logging filter matches.
Here’s another example. This time, you filter on the act
log
entry type. This allows you to see the data passing into and out of
actions:
$ node sales-tax-log.js --seneca.log=type:act
[-isodate-] DEBUG act in uk74hd {cmd=salestax,country=IE,net=100}
[-isodate-] DEBUG act out uk74hd {total=123}
[-isodate-] DEBUG act in qv5sts {cmd=salestax,country=UK,net=200}
[-isodate-] DEBUG act out qv5sts {total=240}
[-isodate-] DEBUG act in 7j9q4a {cmd=salestax,country=UK,net=300}
[-isodate-] DEBUG act out 7j9q4a {total=360}
You can see two entries for each action, in
and out
. Each entry shows the JSON document data being passed
into Seneca, and out of, Seneca. You can also see that each pair has the same action identifier, such as uk74hd
.
Let’s put this all together. You want to see the input and output data of the actions, and anything the sale tax plugin decides to log:
$ node sales-tax-log.js --seneca.log=plugin:sales-tax --seneca.log=type:act
[-isodate-] DEBUG act in cpvycd {cmd=salestax,country=IE,net=100}
[-isodate-] DEBUG plugin sales-tax IE cpvycd
net: 100 total: 123 tax: {hits=1,rate=0.23,country=IE}
[-isodate-] DEBUG act out cpvycd {total=123}
[-isodate-] DEBUG act in tx5zj3 {cmd=salestax,country=UK,net=200}
[-isodate-] DEBUG plugin sales-tax UK tx5zj3
net: 200 total: 240 tax: {hits=1,rate=0.2,country=UK}
[-isodate-] DEBUG act out tx5zj3 {total=240}
[-isodate-] DEBUG act in 8ikumj {cmd=salestax,country=UK,net=300}
[-isodate-] DEBUG plugin sales-tax UK 8ikumj
net: 300 total: 360 tax: {hits=2,rate=0.2,country=UK}
[-isodate-] DEBUG act out 8ikumj {total=360}
This shows the detailed processing of the sales tax calculation. The
action identifiers, which you can get using args.actid$
inside
an action function.You need to specify two --seneca.log
filters, as the type is plugin
for one, and act
for the
other.
The tag
filter can be used to focus on a specific, tagged, plugin instance. Here’s how you look at UK sales tax
operations only:
$ node sales-tax-log.js --seneca.log=plugin:sales-tax,tag:UK
[-isodate-] DEBUG plugin sales-tax UK i2r7wn
net: 200 total: 240 tax: {hits=1,rate=0.2,country=UK}
[-isodate-] DEBUG plugin sales-tax UK 8ir490
net: 300 total: 360 tax: {hits=2,rate=0.2,country=UK}
Console logs are fun, but live logs in your web browser are awesome! Seneca can do this too:
You’ll need to create an app that provides a sales-tax calculation HTTP JSON API. Using the
web
plugin this is easy. This plugin accepts JSON documents from remote clients
over HTTP and submits them to the local Seneca instance.
Here the code, in sales-tax-app.js
, that sets up the app:
var connect = require('connect')
var connect_query = require('connect-query')
var body_parser = require('body-parser')
var seneca = require('seneca')()
seneca.use('sales-tax-plugin', {country: 'IE', rate: 0.23})
seneca.use('sales-tax-plugin', {country: 'UK', rate: 0.20})
seneca.use('sales-tax-plugin', {country: '*', rate: 0.25})
var app = connect()
app.use(connect_query())
app.use(body_parser.json())
app.use(seneca.export('web'))
app.listen(3000)
seneca.use('data-editor')
seneca.use('admin', {server: app, local: true})
The script sets up a simple HTTP server, using the Node.js connect
module. The web
plugin is preloaded by Seneca and
works locally without any configuration, so all you have to do is hook it as a connect or express middleware, or directly with the standard HTTP API:
var app = connect()
app.use(seneca.export('web'))
app.listen(3000)
The admin
and data-editor
plugins together provide a web administration interface for
Seneca. It uses web sockets, so you need to provide a reference to the
http server object in the plugin options. To expose the
administration web interface locally without requiring a password,
use the local:true
option:
seneca.use('data-editor')
seneca.use('admin', {server: app, local: true})
Run this app, and open localhost:3000/admin. You can still use command line logging - you can have multiple separate logging channels.
$ node sales-tax-app.js --seneca.log=plugin:shop
The administration interface let’s you set filtering options. They work the same way as the command line options. There’s nothing to log yet, so let’s generate some sales tax calculations!
The file sales-tax-app-client.js
contains the client
code. We’re using the standard Node HTTP client here:
var http = require('http')
http.get({
hostname: 'localhost',
port: 3000,
path: '/shop/salestax?net=100&country=UK'
}, function (res) {
res.on('data', function (chunk) {
console.log(JSON.parse(chunk.toString()))
})
})
But you can also test it with cUrl:
$ curl -S 'http://localhost:3000/shop/salestax?net=100&country=UK'
$ {"total":120}
The sales tax operations code is as before. This is the key idea behind Seneca - your business logic code stays the same, but you can move around and reconfigure where it happens without worrying about refactoring your code.
With the app up and running, run the client: you’ll see log entries in the web interface.
You can define your own log handlers programmatically when you setup Seneca. The file
sales-tax-log-handler.js
shows you how to do this:
var seneca = require('seneca')
// need this to get a reference to seneca.loghandler
seneca = seneca({
log: {
map: [
{plugin: 'shop', handler: 'print'},
{level: 'all', handler: seneca.loghandler.file('shop.log')}
]
}
})
seneca.use('sales-tax-plugin', {rate: 0.23})
seneca.ready(function (err) {
if (err) return process.exit(!console.error(err))
seneca.act({role: 'shop', cmd: 'salestax', net: 100})
seneca.act({role: 'shop', cmd: 'salestax', net: 200})
seneca.act({role: 'shop', cmd: 'salestax', net: 300})
})
Running this script will output log entries both to the console (only where plugin is “shop” ), and
to a log file shop.log
, which gets everything. In production you mostly just want to output to the console and use
the operating system tools for file redirection. The file handler is mostly for creating special log files.
The logging map allows you to send log entries to multiple locations based on the filters you specify. You can still use the
command line argument --seneca.log=...
to add further filters.
The built-in handlers are:
seneca.loghandler.print
: logs to the consoleseneca.loghandler.file(filepath)
: logs to a fileseneca.loghandler.stream(WriteStream)
: logs to a streamseneca.loghandler.emitter(EventEmitter)
: logs using eventsYou can write your own handler. It’s just a function that takes the log entry as first argument. The log entry is an array of values.
Here’s an example using the LogEntries.com service. This is cloud logging service that
stores your logs and makes them searchable. I wrote their Node.js API module :) - $ npm install node-logentries
.
This example is in the file sales-tax-logentries.js
:
var logentries = require('node-logentries')
var log = logentries.logger({
token: 'YOUR_TOKEN',
// redefine log levels to match the ones seneca uses
levels: {debug: 0, info: 1, warn: 2, error: 3, fatal: 4}
})
var seneca = require('seneca')({
log: {
map: [
{level: 'all', handler: function () {
log.log(arguments[1], Array.prototype.join.call(arguments, '\t'))
}}
]
}
})
seneca.use('sales-tax-plugin', {rate: 0.23})
seneca.ready(function (err) {
if (err) return process.exit(!console.error(err))
seneca.act({role: 'shop', cmd: 'salestax', net: 100})
seneca.act({role: 'shop', cmd: 'salestax', net: 200})
seneca.act({role: 'shop', cmd: 'salestax', net: 300})
})
You’ll need to register a LogEntries.com account and get a token for this to work.
You can use custom handler functions to send logs anywhere you want, and process them anyway you need.
Log filters are dynamic. You can add new ones at runtime using the seneca.logroute
method:
seneca.logroute( {level:'all', handler:seneca.handler.print} )
If you omit the handler, any previous filter is removed. If you add multiple handlers for the same filter, the logs will be sent to all the handlers.This feature enables the administration web site to dynamically modify the filters at runtime.
That’s all folks!
Issues? From spelling errors to broken tutorials and everything in between, report them here.