-
Notifications
You must be signed in to change notification settings - Fork 25
Services
A service is a module that can be made available to handlers and other services throughout an app. A service might seem like an ordinary JavaScript module, but it's different in that it contains lifecycle methods for things like initialization.
Additionally, services make use of dependency injection, making code easier to test.
Services are placed in the services directory of your application. The name of the JS file containing the service determines the service's name.
At a minimum, a service must export an init method. Any other functions exported by a service are available to be invoked after startup.
exports.init = function() {
}
exports.doSomething = function() {
}
The init method is called when the server starts up.
To access other services, rather than having to require them, they're injected as parameters to the init function. Dependency injection relies on the variable name to load the correct service. For example, if you want to use the logger service to log that the service is starting up, add a logger parameter to the init function.
exports.init = function(logger) {
logger.info('Starting my service');
}
JavaScript modules often contain dashes, which cannot be used as a variable name. To get around that, service names are converted to camelCase. For example, a service defined in my-server.js can be loaded through a myService parameter.
For convenience, parameter names can be surrounded by underscores. This is helpful in the case that a parameter dependency-injected during initialization needs to be made available to other functions within a service.
var logger; //will be set after initialization
exports.init = function(_logger_) {
logger = _logger_;
}
exports.doSomething = function() {
logger.info('doSomething was called');
}
Suppose your service needs to perform an asynchronous task during startup. If the last parameter of the init function is named callback, startup will be asynchronous. Any error that occurs during initialization can be passed to the callback to be logged appropriately.
Keep in mind that if the callback is never called, the server won't finish starting.
Example of an asynchronous service.
var fs = require('fs');
var _content
exports.init = function(logger, callback) {
fs.readFile('/some/file', function(err, data) {
if (err) {
return callback(err);
}
_content = data.toString();
callback();
});
}
Server startup uses dependencies to determine the order to initialize services. As long as no cycles exist within the dependencies, the server will only initialize a service only after all its dependencies have also been initialized.
Suppose you have an ordinary module that's loaded via require
, and you need to be able to access a service like the logger.
You won't be able to use the dependency-injection mechanism because there's no init
method.
Instead you can use the global services
object to get the logger.
var logger = services.get('logger');
However, this can be tricky because a required module will likely be loaded before all the services have initialized. To better handle that case, a callback can be specified to make the call asynchronous. The callback won't be called until after the specified module has been initialized.
services.get('logger', function(logger) {
logger.info('It worked!');
});
Configuration is stored in JSON files within the config directory of the application. The config service is based off of the node-config project. Comments are allowed in the JSON files.
Projects should typically include a defaults.json config file, but additional environment-specific files can be added and merged in automatically. For example, a config file named production.json will be merged in as long as the $NODE_ENV is set to production. See the node-config docs for more detailed information.
Most services have a top-level field in the config, e.g. logger for the logger service and express for the express service.
Each field can contain either an array or object.
{
"service1": {
"test": true
},
"service2": ["foo", "bar"]
}
To access the config data, place the config service as an argument in the init method method.
{
"myConfig": {
"foo": "bar"
}
}
exports.init = function(config) {
var fooValue = config.get('myConfig').foo; //fooValue === "bar"
}
The config can contain encrypted strings. If an encrypted string is found in the value of an array or object, the server will prompt for a password during startup.
The format for encrypted values is {cipher}value=
, e.g. {aes-256-cbc}cf3d490f602b718d5694e2ca1a231d08=
{
"data": {
"field": "{aes-256-cbc}cf3d490f602b718d5694e2ca1a231d08="
}
}
A tool is provided, blueoak-server/bin/encrypt.js, for encrypting values.
Usage is encrypt.js -c <cipher> <key> <data>
. The default cipher is aes-256-cbc.
The key can either be specified as an environment variable, decryptionKey
, or included in the security section of the config file.
Storing the key in the config isn't secure and is only suggested for avoiding sensitive data in plain text.
{
"security": {
"key": "myKey"
}
}
Blocks of config can be placed into separate files.
For example, suppose there's a block of config called service1.
"service1": {
"foo": "bar"
}
Additional config data can instead be placed into a file named service1.json.
{
"key": "value"
}
Any config from the other config files will be merged with the content from service1.json. In the above case, the final config for service1 will be
{
"foo": "bar",
"key": "value"
}
The logging service uses winston. Out of the box log levels debug, verbose, info, warn, and error are enabled.
var logger = server.get('logger');
logger.debug('debug message');
logger.info('info message');
logger.warn('warn message');
logger.error('error message');
Additional transports can be used in place of the default console transport. Here you can see a mongodb transport being used.
"logger": {
"transports": [
{
"package": "winston-mongodb",
"field": "MongoDB",
"options": {
"db": "test"
}
}
]
}
A transport requires a package, field, and options.
The package will be loaded through require
, the specified field will than be added to the logger with the given options.
For example, to use the file transport, the config might look like
"logger": {
"transports": [
{
"package": "winston",
"field": "transports.File",
"options": {
"filename": "foo.log"
}
}
]
}
Or to use the console transport with a different log level
"transports": [
{
"package": "winston",
"field": "transports.Console",
"options": {
"level": "info",
"colorize": true
}
}
]
If transport settings can't be adequately represented in config, e.g. if you need to add a custom function to a transport, there's an alternative way to add a transport.
Create a logger.js in your application directory with an init
method. The winston logger will be passed to the init method during server startup and let you add transports. Keep in mind though that any logger configuration in the config files will be ignored.
The example below creates a Console transport with a custom timestamp function.
var winston = require('winston');
module.exports.init = function(logger) {
logger.add(winston.transports.Console, {
timestamp: function() {
return Date.now();
}
});
}
The monitor service is a client for communicating with the StatsD daemon. StatsD can then integrate with many back-end monitoring services, such as Librato, Datadog, or Graphite.
StatsD communicates via UDP and at a minimum needs a host and a port. The port defaults to 8125. The debug parameter can be enabled to log all monitoring calls to stdout.
"monitor": {
"host": "localhost"
}
The monitoring service uses the Node StatsD client. Check out their documentation for more details.
To use the monitoring service, include the monitor parameter in your init method.
exports.init = function(monitor) {
monitor.increment('some.stat.value');
}
The service provides several ways to record stats.
Increment a counter by 1, or by the optional value.
monitor.increment('some.counter'); //increment by 1
monitor.increment('some.counter', 10); //increment by 10
Decrement a counter by 1, or by the optional value.
monitor.decrement('some.counter'); //decrement by 1
monitor.decrement('some.counter', -10); //decrement by 10
Set a counter to a specific value.
monitor.gauge('some.value', 99); //set counter to 99
Counts unique occurrences of a stat
monitor.unique('some.value', 'foobar');
Record the duration of an event.
monitor.timing('some.time', 55); //record 55 ms
Use to get an express function that can be added to a route.
app.get('/hello', monitor.express('myPrefix'), function(req, res) {
...
}
In addition to the express function, there's also a middleware service for enabling monitoring on all routes.
The cache service provides a key-value store backed by either node-cache or Redis.
The interface is the same regardless of which backing store is used. It's therefore simple to switch between them simply by modifying config.
Put a value in the cache.
cache.set('my.key', {foo: 'bar'});
The value can be any JS object, even null or JSON. The optional ttl (time to live) is a value in seconds.
Retrieve a value from the cache. If the key does not exist in the cache, undefined
is returned.
cache.get('my.key', function(err, value) {
...
});