The Problem

Logging has been growing concern of mine as I’ve been using the ServiceNow platform. The use of gs.log and its variants has great uses for debugging in sub-production but I’ve noticed that it frequently sneaks it way into production code which can diminish the overall value of the syslog table. There are a few common problems I’ve seen with logging:

  1. No source provided
  2. No log level controls
  3. Minimal consistency in formatting

Logging in production environments can be helpful to debug the system live as the issues are happening but there needs to be tight control on when a solution creates a record in syslog.

A Solution

GSLog is an out of the box script include that allows you to define log levels in a System Property for each call into GSLog. This will form the base to the following wrapper Script Include called SysLogger:


var SysLogger = Class.create();
SysLogger.prototype = {
    /**
     * @description Constructor for the AutoLogger class.
     * @param {String} property System property that contains a value indicating the level at or above which messages will be written to the log.
     * @param {String} caller The calling script name.
     * @param {Number} padding The number of digits to have on the log message's counter.
     * @throws {Error} Throws an error if the property parameter is not defined as a string.
     * @throws {Error} Throws an error if the caller parameter is not defined as a string.
     * @throws {Error} Throws an error if the padding parameter is not a number.
     * @example var logger = new AutoLogger('log.verbosity', 'script.name', 3); logger.log('This is a log message.', 'error');
     */
    initialize: function (property, caller, padding) {
        if (gs.nil(property) || typeof property != 'string') {
            throw new Error('The property parameter must be defined as a string.');
        }

        if (gs.nil(caller) || typeof caller != 'string') {
            throw new Error('The caller parameter must be defined as a string.');
        }

        if (!gs.nil(padding) && typeof padding != 'number') {
            throw new Error('The padding parameter must be a number.');
        }

        this.count = 0;
        this.property = property;
        this.caller = caller;
        this.padding = gs.nil(padding) ? 3 : padding;
    },

    /**
     * @description Class method for logging to the system log.
     * @param {String} message The log message.
     * @param {String} level The log level (default: debug). Consult GSLog documentation for available levels.
     * @example AutoLogger('This is a log message.', 'error');
     */
    log: function (message, level) {
        level = gs.nil(level) ? 'debug' : level;

        var logger = new global.GSLog(this.property, this.caller);
        SysLogger.formatAndLog(logger, message, level, this.count++, this.padding, this.caller);
    },

    type: 'AutoLogger'
};

SysLogger.logCount = 0;
/**
 * @description Static method for logging to the system log.
 * @param {String} property System property that contains a value indicating the level at or above which messages will be written to the log.
 * @param {String} caller The calling script name.
 * @param {String} message The log message.
 * @param {String} [level=debug] The log level (default: debug). Consult GSLog documentation for available levels.
 * @param {Number} [padding] The number of digits to have on the log message's counter.
 * @example AutoLogger.log('log.verbosity', 'script.name', 'message', 'error', 3);
 */
SysLogger.log = function (property, caller, message, level, padding) { // Static method
    if (gs.nil(property) || gs.nil(caller) || gs.nil(message)) {
        throw new Error('All parameters must be defined (property, caller, and message).');
    }

    level = gs.nil(level) ? 'debug' : level;
    padding = gs.nil(padding) ? 3 : padding;

    var logger = new global.GSLog(property, caller);
    SysLogger.formatAndLog(logger, message, level, SysLogger.logCount++, padding, caller);
};

/**
 * @description Private method to format and print the log message.
 * @param {Object} logger The logger object.
 * @param {String} message The log message.
 * @param {String} level The log level (default: debug). Consult GSLog documentation for available levels.
 * @param {Number} counter The log message counter.
 * @param {Number} padding The number of digits to have on the log message's counter.
 * @param {String} caller The calling script name.
 * @example AutoLogger.formatAndLog(logger, 'This is a log message.', 'error', 3, 3, 'script.name');
 */
SysLogger.formatAndLog = function (logger, message, level, counter, padding, caller) {
    var numMgr = new global.NumberManager();
    var paddedNumber = numMgr.padObjNumber(counter, padding);
    var logMessage = caller + '/' + paddedNumber + ':\n' + message;
    logger.log(level, logMessage);
};

The inclusion of NumberManager makes ordering much easier when multiple log statements trigger near the same time. This has helped my development team form a more consistent logging standard that can easily be changed in a single place as team need and preference changes. SysLogger uses GSLog and NumberManager to provide an easily trackable and consistent logging scheme.

There are two ways to call the utility - instantiation of the Syslogger class or using the static method. The former is useful when you are going to have multiple places within a script that needs debug logs while the latter is useful when in Script Includes (in its own wrapper function) or when you only have a single message to log.