Skip to content
Patrick Wolf edited this page Jan 15, 2016 · 18 revisions

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.

Creating Services

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');
}

Naming Conventions

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');
}

Asynchronous Startup

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();
  });
}

Dependencies

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.

Accessing services inside of modules

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!');
    });

Config

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.

Service Config

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"]
}

API

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"
}

Securing Config Files

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="
  }
}

Generating Encrypted Config

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.

Bypassing the password prompt

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"
  }
}

Separating Config Into Individual Files

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"
}

Logger

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');

Configuration

Configuring transports

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
        }
      }
    ]

Programmatically adding transports

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();
        }
    });
}

Monitor

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.

Configuration

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"
}

API

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

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

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

gauge

Set a counter to a specific value.

monitor.gauge('some.value', 99); //set counter to 99

unique

Counts unique occurrences of a stat

monitor.unique('some.value', 'foobar');

timing

Record the duration of an event.

monitor.timing('some.time', 55); //record 55 ms

express

Use to get an express function that can be added to a route.

app.get('/hello', monitor.express('myPrefix'), function(req, res) {
  ...
}

Express JS

In addition to the express function, there's also a middleware service for enabling monitoring on all routes.

Cache

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.

API

cache.set(key, val, [ttl], [callback)

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.

cache.get(key, callback)

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) {
  ...
});
Clone this wiki locally