Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
/ opserver Public archive

A developer-friendly MongoDB oplog event aggregator

Notifications You must be signed in to change notification settings

samhagman/opserver

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

npm version bitHound Overall Score

Opserver

A developer-friendly MongoDB oplog event aggregator

About

The MongoDB oplog stores a record of all transactions (i.e., changes) that occur on your MongoDB server. These records are designed to have a minimal footprint including shorthand key names and strange and changing values depending on transaction type. Inevitably any developer attempting to tail the oplog for their own purposes will spend a lot of time googling around for what the keys mean and why the values change. Opserver was created to hopefully ease this pain.

Put simply, Opserver is an event emitter that tails the MongoDB oplog and emits developer-friendly events. Besides attempting to normalize and expand the key, value naming and values for oplog events, Opserver has a few tricks up its sleeve. It can also do up-front filtering based on database, collection, and/or properties of collection records and automatic record retrieval for all transaction types.

To use the MongoDB oplog you must be running your MongoDB instance as a replica set, even if you are not actually replicating your instance.

Table of Contents

Examples

// Get an opserver factory back
var opserverFactory = require(‘opserver’);

var opserverOptions = {
    mongoAdminURI: ‘mongodb://127.0.0.1:27017/admin?replicaSet=test’,
    mongoAdminUser: 'admin',
    mongoAdminPass: 'adminPass',
    mongoReadOnlyUser: 'readUser',
    mongoReadOnlyPass: 'readPass'
};

// Get the opserver via callback or Promise (omit the callback argument)
opserverBuilder(opserverOptions, function(opserver) {

    // Listen for new inserts into a collection
    opserver.on(‘insert:mydb.users’, function(eventData) {

        //  Access the id of the inserted document
        console.log(eventData.id);
    
        //  Access the full inserted document
        console.log(eventData.doc.firstName);
    
        //  Access the doc that was sent in the insert
        console.log(eventData.operation);
    
    });

    // Update events give you even more useful information
    opserver.on(‘update:mydb.users.email’, function(eventData) {

        //  `eventData.operation` for update events is the MongoDB update command that was executed
        console.log(eventData.operation);
    
        //  Also have access to the raw MongoDB oplog document
        console.log(oplogDoc);
    
        //  Quick access to the property that was updated
        //  (the last value after the dot — e.g. email, firstName)
        console.log(eventData.setVal);
    
        //  You can also look at the exact query used to update
        console.log(eventData.query);
    
    });

});

Initialization

Initializing an Opserver requires three key arguments:

  • A mongo connection string for the admin database of your MongoDB installation

  • The options needed to connect to that database (either in object or connection string format)

  • The options needed to authenticate as a read only user to the databases you want to listen for events on. This would just be the same user/pass setup on all your databases that you want Opserver to be able to access

Opserver will initialize in the following way:

  1. Connect to your admin database using the given admin credentials
  2. List all the databases that are in your MongoDB installation
  3. Attempt to connect to all the databases with the read only user credentials
  4. Begin to tail the Mongo oplog
  5. Start emitting events

Note: The admin user must have the 'clusterAdmin' and 'readAnyDatabase' roles in the admin database

The full options object used for initialization are listed below with a description and their default values:

let opserverOptions = {
    mongoURI:                       undefined // URI to database
    mongoAdminUser:                 undefined // Admin user
    mongoAdminPass:                 undefined // Admin user password
    mongoAdminConnectionOptions:    {},       // Options object passed to `mongo.connect()`
    mongoReadOnlyUser:              undefined // readOnly user
    mongoReadOnlyPass:              undefined // readOnly pasword
    mongoReadOnlyConnectionOptions: {},       // Options object passed to `mongo.connect()`
    debugMode:                      false,    // Whether or not to print debugging messages
    loggerFunctions: {                        // Optionally pass in your own logger functions
        info() {                                // for info, warn, debug, and error messages when in debug mode
            console.log(...arguments);
        },
        warn() {
            console.log(...arguments);
        },
        debug() {
            console.log(...arguments);
        },
        error() {
            console.log(...arguments);
        }
    },
    excludes:                       {           // These are paths to exclude from their respective events. 
        updatePaths: [],                        // Pass in a string to any of these arrays and if that string is found
        insertPaths: [],                        // anywhere inside one of the event strings, that event will be ignored
        deletePaths: []                         // ex. 'update:mydb.users.firstName' will ignore any updates of users
    }                                           // firstName fields in the mydb database.  'firstName' would ignore any
};                                              // update event that had 'firstName' anywhere in its event string.

Event Types

There are two types of events in Opserver: Insert/Delete and Update.

  • Insert/Delete event strings are formatted like so: 'insert:DATABASE_NAME.COLLECTION' / 'delete:DATABASE_NAME.COLLECTION'

These are straightforward, you have the name of the operation, a colon, the name of the database, and then a collection within that database.

  • Update event strings are formatted like so: 'update:DATABASE_NAME.COLLECTION.PATH.TO.FIELD'

These are more interesting and let you listen for updates to specific fields or subdocuments within a collection. Using update events combined with Opserver#onMatched you are able to construct powerful triggers on changes to your data fairly easily.

Here are the event types taken straight from the code's JSDoc comments:

/**
 * Insert event.  Listen on 'insert:DATABASE' to get insert events for that database.
 * @example 'insert:mydb.users'
 * @event Opserver#insert:DATABASE
 * @type {OpserverEventData}
 */

/**
 * Delete event.  Listen on 'delete:DATABASE' to get delete events for that database.
 * @example 'delete:mydb.users'
 * @event Opserver#delete:DATABASE
 * @type {OpserverEventData}
 */

/**
 * Update event.  Listen on 'update:DATABASE.PROPERTY_PATH' to get update events for that database.
 * @example 'update:mydb.users.preferences.timezone'
 * @event Opserver#update:DATABASE.PROPERTY_PATH
 * @type {OpserverEventData}
 */

Event Data

Your promise or callback returned by any of Opserver's APIs will come with Event Data. This data will include the following fields:

/**
 * This is the structure of the data that is sent along with Opserver events
 * @typedef {object} OpserverEventData
 * @prop {object} oplogDoc - passed in oplogDoc
 * @prop {string} id - The _id of the mongo doc involved in the operation.
 * @prop {string} type - The type of MongoDB operation - e.g. insert, update, delete
 * @prop {object} doc - The actual mongo doc that was involved in the operation.
 * @prop {object} operation - For inserts/deletes this is the actual doc; for updates it is the command
 *     to update old document.
 * @prop {*} setVal - The value of the path that is being observed, only for updates.
 * @prop {string} event - The event that this data was attached to
 * @prop {object} query - Only for updates.  The selection query for the operation.
 * @prop {string} changedPropPath - Only for updates.  The path that is being observed.
 */

Methods

EventEmitter3 (which Opserver extends) gives most of the usual EventEmitter functions such as .on, .removeListener, etc. Please refer to the EventEmitter3 and Node EventEmitter docs for these methods standard Node Event methods. The methods below are in addition to those standard methods.

Opserver#onAny(<String[]> eventStrings, <Function> listener) => {Function}

onAny() allows you to register multiple different event listeners that share the same callback. This returns a function that when called will remove all the event listeners.

Opserver#onceMatched(<String> eventString, <Object> valuesToMatch, <Function> [cb]) => {Promise<EventData>}

Register a listener which will only trigger when the EventData of an event matches the valuesToMatch object. The valuesToMatch object must be constructed in the form { 'OBJECT.PATH': VALUE } e.g., { 'doc.firstName': 'Sam' }

Opserver#onMatched(<String> eventString, <Object> valuesToMatch, <Function> listener) => {Function}

Same as Opserver#onceMatched but will trigger multiple times until the listener is removed. Returns a function that when called will remove the listener from the event.

TODO

  • Completely document code in JSDoc format
  • Build and serve JSDoc output on github.io
  • Write tests
  • Optimize further
  • ???

About

A developer-friendly MongoDB oplog event aggregator

Resources

Stars

Watchers

Forks

Packages

No packages published