/* * client.js: Core client functions for accessing Loggly * * (C) 2010 Charlie Robbins * MIT LICENSE * */ // // Setting constant value of EVENT_SIZE variable // var EVENT_SIZE = 1000 * 1000; var events = require('events'), util = require('util'), qs = require('querystring'), common = require('./common'), loggly = require('../loggly'), Search = require('./search').Search, stringifySafe = require('json-stringify-safe'); function stringify(msg) { var payload; try { payload = JSON.stringify(msg) } catch (ex) { payload = stringifySafe(msg, null, null, noop) } return payload; } // // function to truncate message over 1 MB // function truncateLargeMessage(message) { var maximumBytesAllowedToLoggly = EVENT_SIZE; var bytesLengthOfLogMessage = Buffer.byteLength(message); var isMessageTruncated = false; if(bytesLengthOfLogMessage > maximumBytesAllowedToLoggly) { message = message.slice(0, maximumBytesAllowedToLoggly); isMessageTruncated = true; } return { message: message, isMessageTruncated: isMessageTruncated }; } // // function createClient (options) // Creates a new instance of a Loggly client. // exports.createClient = function (options) { return new Loggly(options); }; // // ### function Loggly (options) // #### @options {Object} Options for this Loggly client // #### @subdomain // #### @token // #### @json // #### @auth // #### @tags // Constructor for the Loggly object // var Loggly = exports.Loggly = function (options) { if (!options || !options.subdomain || !options.token) { throw new Error('options.subdomain and options.token are required.'); } events.EventEmitter.call(this); this.subdomain = options.subdomain; this.token = options.token; this.host = options.host || 'logs-01.loggly.com'; this.json = options.json || null; this.auth = options.auth || null; this.proxy = options.proxy || null; this.userAgent = 'node-loggly ' + loggly.version; this.useTagHeader = 'useTagHeader' in options ? options.useTagHeader : true; this.isBulk = options.isBulk || false; this.bufferOptions = options.bufferOptions || {size: 500, retriesInMilliSeconds: 30 * 1000}; this.networkErrorsOnConsole = options.networkErrorsOnConsole || false; // // Set the tags on this instance. // this.tags = options.tags ? this.tagFilter(options.tags) : null; var url = 'https://' + this.host, api = options.api || 'apiv2'; this.urls = { default: url, log: [url, 'inputs', this.token].join('/'), bulk: [url, 'bulk', this.token].join('/'), api: 'https://' + [this.subdomain, 'loggly', 'com'].join('.') + '/' + api }; }; // // Inherit from events.EventEmitter // util.inherits(Loggly, events.EventEmitter); // // ### function log (msg, tags, callback) // #### @msg {string|Object} Data to log // #### @tags {Array} **Optional** Tags to send with this msg // #### @callback {function} Continuation to respond to when complete. // Logs the message to the token associated with this instance. If // the message is an Object we will attempt to serialize it. If any // `tags` are supplied they will be passed via the `X-LOGGLY-TAG` header. // - http://www.loggly.com/docs/api-sending-data/ // Loggly.prototype.log = function (msg, tags, callback) { // typeof msg is string when we are using node-loggly-bulk to send logs. // If we are sending logs using winston-loggly-bulk, msg is object. // Check if 'msg' is an object, if yes then stringify it to truncate it over 1MB. var truncatedMessageObject = null; if(typeof(msg) === 'object'){ var stringifiedMessage = JSON.stringify(msg) truncatedMessageObject = truncateLargeMessage(stringifiedMessage); msg = truncatedMessageObject.isMessageTruncated ? truncatedMessageObject.message : msg; }else if (typeof(msg) === 'string') { truncatedMessageObject = truncateLargeMessage(msg); msg = truncatedMessageObject.isMessageTruncated ? truncatedMessageObject.message : msg; } if (!callback && typeof tags === 'function') { callback = tags; tags = null; } var self = this, logOptions; // // Remark: Have some extra logic for detecting if we want to make a bulk // request to loggly // function serialize(msg) { if (msg instanceof Object) { return self.json ? stringify(msg) : common.serialize(msg); } else { return self.json ? stringify({ message: msg }) : msg; } } msg = this.isBulk && Array.isArray(msg) ? msg.map(serialize) : serialize(msg); logOptions = { uri: this.isBulk ? this.urls.bulk : this.urls.log, method: 'POST', body: msg, proxy: this.proxy, isBulk: this.isBulk, bufferOptions: this.bufferOptions, networkErrorsOnConsole: this.networkErrorsOnConsole, headers: { host: this.host, accept: '*/*', 'user-agent': this.userAgent, 'content-type': this.json ? 'application/json' : 'text/plain' } }; // // Remark: if tags are passed in run the filter on them and concat // with any tags that were passed or just use default tags if they exist // tags = tags ? (this.tags ? this.tags.concat(this.tagFilter(tags)) : this.tagFilter(tags)) : this.tags; // // Optionally send `X-LOGGLY-TAG` if we have them. // Set the 'X-LOGGLY-TAG' only when we have actually some tag value. // The library receives "400 Bad Request" in response when the // value of 'X-LOGGLY-TAG' is empty string in request header. // if (tags && tags.length) { // Decide whether to add tags as http headers or add them to the URI. if (this.useTagHeader) { logOptions.headers['X-LOGGLY-TAG'] = tags.join(','); } else { logOptions.uri += '/tag/' + tags.join(',') + '/'; } } common.loggly(logOptions, callback, function (res, body) { try { if(body && res.statusCode.toString() === '200'){ var result = JSON.parse(body); self.emit('log', result); if (callback) { callback(null, result); } } else console.log('Error Code- ' + res.statusCode + ' "' + res.statusMessage + '"'); } catch (ex) { if (callback) { callback(new Error('Unspecified error from Loggly: ' + ex)); } } }); return this; }; // // ### function tag (tags) // #### @tags {Array} Tags to use for `X-LOGGLY-TAG` // Sets the tags on this instance // Loggly.prototype.tagFilter = function (tags) { var isSolid = /^[\w\d][\w\d-_.]+/; tags = !Array.isArray(tags) ? [tags] : tags; // // TODO: Filter against valid tag names with some Regex // http://www.loggly.com/docs/tags/ // Remark: Docs make me think we dont need this but whatevs // return tags.filter(function (tag) { // // Remark: length may need to use Buffer.byteLength? // return tag && isSolid.test(tag) && tag.length <= 64; }); }; // // ### function customer (callback) // ### @callback {function} Continuation to respond to. // Retrieves the customer information from the Loggly API: // - http://www.loggly.com/docs/api-account-info/ // Loggly.prototype.customer = function (callback) { common.loggly({ uri: this.logglyUrl('customer'), auth: this.auth }, callback, function (res, body) { var customer; try { customer = JSON.parse(body) } catch (ex) { return callback(ex) } callback(null, customer); }); }; // // function search (query, callback) // Returns a new search object which can be chained // with options or called directly if @callback is passed // initially. // // Sample Usage: // // client.search('404', function () { /* ... */ }) // .on('rsid', function (rsid) { /* ... */ }) // // client.search({ query: '404', rows: 100 }) // .on('rsid', function (rsid) { /* ... */ }) // .run(function () { /* ... */ }); // Loggly.prototype.search = function (query, callback) { var options = typeof query === 'string' ? { query: query } : query; options.callback = callback; return new Search(options, this); }; // // function logglyUrl ([path, to, resource]) // Helper method that concats the string params into a url // to request against a loggly serverUrl. // Loggly.prototype.logglyUrl = function (/* path, to, resource */) { var args = Array.prototype.slice.call(arguments); return [this.urls.api].concat(args).join('/'); }; // // Simple noop function for reusability // function noop() {}