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 | */
|
15 | var 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 | */
|
63 | function 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 | */
|
115 | LogCollector.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 | */
|
138 | LogCollector.prototype.formatLogMessage = function (
|
139 | logLevel /* 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 | */
|
171 | LogCollector.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 | */
|
201 | LogCollector.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 | */
|
210 | LogCollector.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 | */
|
226 | LogCollector.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 | */
|
237 | LogCollector.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 | */
|
256 | LogCollector.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 | */
|
299 | LogCollector.prototype.stop = function() {
|
300 | // Flush and stop publishing logs
|
301 | this._flush(false /* do not force */, false /* do not reschedule */);
|
302 | };
|
303 |
|
304 | module.exports = LogCollector;
|