UNPKG

12 kBJavaScriptView Raw
1/* Copyright @ 2016-present 8x8, Inc.
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15var Logger = require('./Logger.js');
16
17/**
18 * Creates new <tt>LogCollector</tt>. Class implements <tt>LoggerTransport</tt>
19 * and thus can be added as global transport in order to capture all the logs.
20 *
21 * It captures subsequent log lines created whenever <tt>Logger</tt> logs
22 * a message and stores them in a queue in order to batch log entries. There are
23 * time and size limit constraints which determine how often batch entries are
24 * stored. Whenever one of these limits is exceeded the <tt>LogCollector</tt>
25 * will use the <tt>logStorage</tt> object given as an argument to save
26 * the batch log entry.
27 *
28 * @param {Object} logStorage an object which allows to store the logs collected
29 * @param {function(string|object[])} logStorage.storeLogs a method called when
30 * this <tt>LogCollector</tt> requests log entry storage. The method's argument
31 * is an array which can contain <tt>string</tt>s and <tt>object</tt>s. If given
32 * item is an object it means that it's an aggregated message. That is a message
33 * which is the same as the previous one and it's representation has
34 * the following format:
35 * {
36 * {string} text: 'the text of some duplicated message'
37 * {number} count: 3 // how many times the message appeared in a row
38 * }
39 * If a message "B" after an aggregated message "A" is different, then it breaks
40 * the sequence of "A". Which means that even if the next message "C" is
41 * the same as "A" it will start a new aggregated message "C".
42 * @param {function()} logStorage.isReady a method which should return
43 * a <tt>boolean</tt> to tell the collector that it's ready to store. During the
44 * time storage is not ready log batches will be cached and stored on the next
45 * occasion (flush or interval timeout).
46 *
47 * @param {Object} options the <tt>LogCollector</tt> configuration options.
48 * @param {number} options.maxEntryLength the size limit for a single log entry
49 * to be stored. The <tt>LogCollector</tt> will push the entry as soon as it
50 * reaches or exceeds this limit given that <tt>logStorage.isReady</tt>
51 * returns <tt>true</tt>. Otherwise the log entry will be cached until the log
52 * storage becomes ready. Note that the "is ready" condition is checked every
53 * <tt>options.storeInterval</tt> milliseconds.
54 * @param {number} options.storeInterval how often the logs should be stored in
55 * case <tt>maxEntryLength</tt> was not exceeded.
56 * @param {boolean} options.stringifyObjects indicates whether or not object
57 * arguments should be "stringified" with <tt>JSON.stringify</tt> when a log
58 * message is composed. Note that objects logged on the error log level are
59 * always stringified.
60 *
61 * @constructor
62 */
63function LogCollector(logStorage, options) {
64 this.logStorage = logStorage;
65 this.stringifyObjects = options && options.stringifyObjects ? options.stringifyObjects : false;
66 this.storeInterval = options && options.storeInterval ? options.storeInterval: 30000;
67 this.maxEntryLength = options && options.maxEntryLength ? options.maxEntryLength : 10000;
68 // Bind the log method for each level to the corresponding method name
69 // in order to implement "global log transport" object.
70 Object.values(Logger.levels).forEach(
71 function (logLevel) {
72 this[logLevel] = function () {
73 this._log.apply(this, arguments);
74 }.bind(this, logLevel);
75 }.bind(this));
76 /**
77 * The ID of store logs interval if one is currently scheduled or
78 * <tt>null</tt> otherwise.
79 * @type {number|null}
80 */
81 this.storeLogsIntervalID = null;
82 /**
83 * The log messages that are to be batched into log entry when
84 * {@link LogCollector._flush} method is called.
85 * @type {string[]}
86 */
87 this.queue = [];
88 /**
89 * The total length of all messages currently stored in the {@link queue}.
90 * @type {number}
91 */
92 this.totalLen = 0;
93 /**
94 * An array used to temporarily store log batches, before the storage gets
95 * ready.
96 * @type {string[]}
97 */
98 this.outputCache = [];
99}
100
101/**
102 * Method called inside of {@link formatLogMessage} in order to covert an
103 * <tt>Object</tt> argument to string. The conversion will happen when either
104 * 'stringifyObjects' option is enabled or on the {@link Logger.levels.ERROR}
105 * log level. The default implementation uses <tt>JSON.stringify</tt> and
106 * returns "[object with circular refs?]" instead of an object if it fails.
107 *
108 * @param {object} someObject the <tt>object</tt> to be stringified.
109 *
110 * @return {string} the result of <tt>JSON.stringify</tt> or
111 * "[object with circular refs?]" if any error occurs during "stringification".
112 *
113 * @protected
114 */
115LogCollector.prototype.stringify = function (someObject) {
116 try {
117 return JSON.stringify(someObject);
118 } catch (error) {
119 return '[object with circular refs?]';
120 }
121};
122
123/**
124 * Formats log entry for the given logging level and arguments passed to the
125 * <tt>Logger</tt>'s log method. The first argument is log level and the next
126 * arguments have to be captured using JS built-in 'arguments' variable.
127 *
128 * @param {Logger.levels} logLevel provides the logging level of the message to
129 * be logged.
130 * @param {Date} timestamp - The {@code Date} when a message has been logged.
131 *
132 * @return {string|null} a non-empty string representation of the log entry
133 * crafted from the log arguments. If the return value is <tt>null</tt> then
134 * the message wil be discarded by this <tt>LogCollector</tt>.
135 *
136 * @protected
137 */
138LogCollector.prototype.formatLogMessage = function (
139logLevel /* timestamp, arg2, arg3, arg4... */) { // jshint ignore:line
140 var msg = '';
141 for (var i = 1, len = arguments.length; i < len; i++) {
142 var arg = arguments[i];
143
144 if (arg instanceof Error) {
145 msg += arg.toString() + ': ' + arg.stack;
146 } else if (this.stringifyObjects && typeof arg === 'object') {
147 // NOTE: We were trying to stringify all error logs before but because of a bug that we were getting the keys
148 // of the log levels which are all with upper case and comparing it with the keys which are all lower case we
149 // were never actually strinfying the error logs. That's why I've removed the check for error logs here.
150 // NOTE: The non-enumerable properties of the objects wouldn't be included in the string after JSON.strigify.
151 // For example Map instance will be translated to '{}'. So I think we have to eventually do something better
152 // for parsing the arguments. But since this option for strigify is part of the public interface and I think
153 // it could be useful in some cases I will it for now.
154 msg += this.stringify(arg);
155 } else {
156 msg += arg;
157 }
158 if (i !== len - 1) {
159 msg += ' ';
160 }
161 }
162 return msg.length ? msg : null;
163};
164
165/**
166 * The log method bound to each of the logging levels in order to implement
167 * "global log transport" object.
168 *
169 * @private
170 */
171LogCollector.prototype._log = function() {
172
173 // var logLevel = arguments[0]; first argument is the log level
174 var timestamp = arguments[1];
175 var msg = this.formatLogMessage.apply(this, arguments);
176 if (msg) {
177 // The same as the previous message aggregation logic
178 var prevMessage = this.queue[this.queue.length - 1];
179 var prevMessageText = prevMessage && prevMessage.text;
180 if (prevMessageText === msg) {
181 prevMessage.count += 1;
182 } else {
183 this.queue.push({
184 text: msg,
185 timestamp: timestamp,
186 count: 1
187 });
188 this.totalLen += msg.length;
189 }
190 }
191
192 if (this.totalLen >= this.maxEntryLength) {
193 this._flush(true /* force */, true /* reschedule */);
194 }
195};
196
197/**
198 * Starts periodical "store logs" task which will be triggered at the interval
199 * specified in the constructor options.
200 */
201LogCollector.prototype.start = function () {
202 this._reschedulePublishInterval();
203};
204
205/**
206 * Reschedules the periodical "store logs" task which will store the next batch
207 * log entry in the storage.
208 * @private
209 */
210LogCollector.prototype._reschedulePublishInterval = function () {
211 if (this.storeLogsIntervalID) {
212 window.clearTimeout(this.storeLogsIntervalID);
213 this.storeLogsIntervalID = null;
214 }
215 // It's actually a timeout, because it is rescheduled on every flush
216 this.storeLogsIntervalID = window.setTimeout(
217 this._flush.bind(
218 this, false /* do not force */, true /* reschedule */),
219 this.storeInterval);
220};
221
222/**
223 * Call this method to flush the log entry buffer and store it in the log
224 * storage immediately (given that the storage is ready).
225 */
226LogCollector.prototype.flush = function() {
227 this._flush(
228 false /* do not force, as it will not be stored anyway */,
229 true /* reschedule next update */ );
230};
231
232/**
233 * Passes the logs to logStorage.storeLogs in order to store them. If logStorage.storeLogs throws an error, it prints it.
234 * Note: We are not retrying to pass the logs to the logStorage if there is an error.
235 * @param {string[]} logs - The logs to be stored.
236 */
237LogCollector.prototype._storeLogs = function (logs) {
238 try {
239 this.logStorage.storeLogs(logs);
240 } catch (error) {
241 console.error('LogCollector error when calling logStorage.storeLogs(): ', error);
242 }
243};
244
245/**
246 * Stores the next batch log entry in the log storage.
247 * @param {boolean} force enforce current logs batch to be stored or cached if
248 * there is anything to be logged, but the storage is not ready yet. One of
249 * legitimate reasons to force is when the logs length exceeds size limit which
250 * could result in truncation.
251 * @param {boolean} reschedule <tt>true</tt> if the next periodic task should be
252 * scheduled after the log entry is stored. <tt>false</tt> will end the periodic
253 * task cycle.
254 * @private
255 */
256LogCollector.prototype._flush = function(force, reschedule) {
257 var logStorageReady = false;
258
259 try {
260 logStorageReady = this.logStorage.isReady();
261 } catch (error) {
262 console.error('LogCollector error when calling logStorage.isReady(): ', error);
263 }
264
265 // Publish only if there's anything to be logged
266 if (this.totalLen > 0 && (logStorageReady || force)) {
267 //FIXME avoid truncating
268 // right now we don't care if the message size is "slightly" exceeded
269 if (logStorageReady) {
270 // Sends all cached logs
271 if (this.outputCache.length) {
272 this.outputCache.forEach(
273 function (cachedQueue) {
274 this._storeLogs(cachedQueue);
275 }.bind(this)
276 );
277 // Clear the cache
278 this.outputCache = [];
279 }
280 // Send current batch
281 this._storeLogs(this.queue);
282 } else {
283 this.outputCache.push(this.queue);
284 }
285
286 this.queue = [];
287 this.totalLen = 0;
288 }
289
290 if (reschedule) {
291 this._reschedulePublishInterval();
292 }
293};
294
295/**
296 * Stops the periodical "store logs" task and immediately stores any pending
297 * log entries as a batch.
298 */
299LogCollector.prototype.stop = function() {
300 // Flush and stop publishing logs
301 this._flush(false /* do not force */, false /* do not reschedule */);
302};
303
304module.exports = LogCollector;