1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2024 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15/**
16 * Provides a logging service for debugging apps and server calls.
17 *
18 * All log messages are filtered by log level and either discarded or
19 * stored to an internal array. Log messages on the error or warning levels
20 * are also sent to the server for remote logging.
21 *
22 * This module replaces the built-in `console.error()`, `console.warn()`,
23 * `console.info()`, `console.log()` and `console.debug()` functions with its
24 * own versions, passing through the log messages if not filtered.
25 *
26 * @namespace RapidContext.Log
27 */
28(function (window) {
29
30 // The original console logger functions
31 var backup = {};
32
33 // The current log state
34 var state = {
35 count: 0,
36 level: 3,
37 context: null,
38 history: [],
39 publish: {
40 last: 0,
41 timer: null
42 }
43 };
44
45 // The configuration settings
46 var config = {
47 interval: 10000,
48 url: "rapidcontext/log",
49 filter: null,
50 publisher: null
51 };
52
53 /**
54 * Initializes and configures the logging module. Will modify the `console`
55 * object for logging. This replaces the default `console.error`,
56 * `console.warn`, `console.info`, `console.log` and `console.debug`
57 * functions. Safe to call multiple times to change or update config.
58 *
59 * @param {Object} [opts] the log configuration options, or null
60 * @param {number} [opts.interval] the publish delay in millis, default is 10s
61 * @param {string} [opts.url] the publish URL endpoint
62 * @param {function} [opts.filter] the event filter, returns a boolean
63 * @param {function} [opts.publisher] the event publisher, returns a Promise
64 *
65 * @memberof RapidContext.Log
66 */
67 function init(opts) {
68 function overwrite(obj, key, fn) {
69 if (obj[key] !== fn) {
70 backup[key] = obj[key] || function () {};
71 obj[key] = fn;
72 }
73 }
74 if (typeof(window.console) !== "object") {
75 window.console = {};
76 }
77 overwrite(window.console, "error", error);
78 overwrite(window.console, "warn", warn);
79 overwrite(window.console, "info", info);
80 overwrite(window.console, "log", debug);
81 overwrite(window.console, "debug", debug);
82 window.onerror = window.onerror || _onerror;
83 opts = opts || {};
84 config.interval = opts.interval || config.interval;
85 config.url = opts.url || config.url;
86 config.filter = opts.filter || _isErrorOrWarning;
87 config.publisher = opts.publisher || _publish;
88 }
89
90 /**
91 * Clears the log console and the array of stored messages.
92 *
93 * @see RapidContext.Log.history
94 *
95 * @memberof RapidContext.Log
96 */
97 function clear() {
98 state.history = [];
99 if (window.console && typeof(window.console.clear) === "function") {
100 window.console.clear();
101 }
102 }
103
104 /**
105 * Returns the history of filtered log entries. Each log entry is a plain
106 * object with properties -- `id`, `time`, `level`, `context`, `message`
107 * and `data`.
108 *
109 * @return {Array} the array of log entries
110 *
111 * @memberof RapidContext.Log
112 */
113 function history() {
114 return state.history.slice(0);
115 }
116
117 /**
118 * Returns and optionally sets the current log level. The supported log
119 * level values are -- "none", "error", "warn", "info", "log" and "all".
120 *
121 * @param {string} [value] the new log level
122 * @return {string} the current log level
123 *
124 * @memberof RapidContext.Log
125 */
126 function level(value) {
127 if (typeof(value) !== "undefined") {
128 if (value === 0 || /^none/i.test(value)) {
129 state.level = 0;
130 } else if (value === 1 || /^err/i.test(value)) {
131 state.level = 1;
132 } else if (value === 2 || /^warn/i.test(value)) {
133 state.level = 2;
134 } else if (value === 3 || /^info/i.test(value)) {
135 state.level = 3;
136 } else if (value === 4 || /^(log|debug|trace)/i.test(value)) {
137 state.level = 4;
138 } else {
139 state.level = 5;
140 }
141 }
142 if (state.level <= 0) {
143 return "none";
144 } else if (state.level <= 1) {
145 return "error";
146 } else if (state.level <= 2) {
147 return "warn";
148 } else if (state.level <= 3) {
149 return "info";
150 } else if (state.level <= 4) {
151 return "log";
152 } else {
153 return "all";
154 }
155 }
156
157 /**
158 * Returns and optionally sets the current log context. The log context is
159 * used to tag all subsequent log messages until the context is removed or
160 * modified.
161 *
162 * @param {string} [value] the new log context, or null to clear
163 *
164 * @return {string} the current log context, or null for none
165 *
166 * @example
167 * RapidContext.Log.context('mybutton.onclick');
168 * ...
169 * console.warn('unsupported xyz value:', value);
170 * ...
171 * RapidContext.Log.context(null);
172 *
173 * @memberof RapidContext.Log
174 */
175 function context(value) {
176 if (typeof(value) !== "undefined") {
177 state.context = value;
178 if (!value) {
179 // Clear group immediately, but create new group on first log
180 _group();
181 }
182 }
183 return state.context;
184 }
185
186 /**
187 * Logs an error message with optional data. Also available as the global
188 * `console.error()` function.
189 *
190 * @param {string} msg the log message
191 * @param {...Object} [data] the additional log data or messages
192 *
193 * @example
194 * console.error('failed to initialize module');
195 *
196 * @memberof RapidContext.Log
197 */
198 function error(msg/**, ...*/) {
199 if (state.level >= 1) {
200 var args = Array.prototype.slice.call(arguments);
201 _log("error", args);
202 _store("error", args.map(stringify));
203 }
204 }
205
206 /**
207 * Logs a warning message with optional data. Also available as the global
208 * `console.warn()` function.
209 *
210 * @param {string} msg the log message
211 * @param {...Object} [data] the additional log data or messages
212 *
213 * @example
214 * console.warn('missing "data" attribute on document root:', document.body);
215 *
216 * @memberof RapidContext.Log
217 */
218 function warn(msg/**, ...*/) {
219 if (state.level >= 2) {
220 var args = Array.prototype.slice.call(arguments);
221 _log("warn", args);
222 _store("warn", args.map(stringify));
223 }
224 }
225
226 /**
227 * Logs an information message with optional data. Also available as the
228 * global `console.info()` function.
229 *
230 * @param {string} msg the log message
231 * @param {...Object} [data] the additional log data or messages
232 *
233 * @example
234 * console.info('authorization failed, user not logged in');
235 *
236 * @memberof RapidContext.Log
237 */
238 function info(msg/**, ...*/) {
239 if (state.level >= 3) {
240 var args = Array.prototype.slice.call(arguments);
241 _log("info", args);
242 _store("info", args.map(stringify));
243 }
244 }
245
246 /**
247 * Logs a debug message with optional data. Also available as the global
248 * `console.log()` and `console.debug()` functions.
249 *
250 * @param {string} msg the log message
251 * @param {...Object} [data] the additional log data or messages
252 *
253 * @example
254 * console.log('init AJAX call to URL:', url);
255 * ...
256 * console.log('done AJAX call to URL:', url, responseCode);
257 *
258 * @memberof RapidContext.Log
259 */
260 function debug(msg/**, ...*/) {
261 if (state.level >= 4) {
262 var args = Array.prototype.slice.call(arguments);
263 _log("log", args);
264 _store("log", args.map(stringify));
265 }
266 }
267
268 /**
269 * Handles window.onerror events (global uncaught errors).
270 */
271 function _onerror(msg, url, line, col, err) {
272 url = url.replace(document.baseURI || "", "");
273 var location = [url, line, col].filter(Boolean).join(":");
274 error(msg || "Uncaught error", location, err);
275 return true;
276 }
277
278 /**
279 * Logs a message to one of the console loggers.
280 *
281 * @param {string} level the log level (i.e. function 'error', 'warn'...)
282 * @param {Array} args the log message & data (as raw objects)
283 */
284 function _log(level, args) {
285 var logger = backup[level];
286 if (typeof(logger) === "function") {
287 _group(state.context);
288 logger.apply(window.console, args);
289 }
290 }
291
292 /**
293 * Calls the `console.group` and `console.groupEnd` functions (if
294 * they exist) to change the group label (if needed).
295 *
296 * @param {string} label the log context label
297 */
298 function _group(label) {
299 var console = window.console;
300 if (console._group !== label) {
301 if (console._group) {
302 delete console._group;
303 if (typeof(console.groupEnd) === "function") {
304 console.groupEnd();
305 }
306 }
307 if (label) {
308 console._group = label;
309 if (typeof(console.group) === "function") {
310 console.group(label + ":");
311 }
312 }
313 }
314 }
315
316 /**
317 * Stores a log message to the history.
318 *
319 * @param {string} level the message log level
320 * @param {Array} args the log message arguments (as strings)
321 */
322 function _store(level, args) {
323 var m = 1;
324 for (; m < args.length; m++) {
325 if (args[m] && args[m].indexOf("\n") >= 0) {
326 break;
327 }
328 }
329 state.history.push({
330 id: ++state.count,
331 time: new Date(),
332 level: level,
333 context: state.context,
334 message: args.slice(0, m).join(" "),
335 data: (m < args.length) ? args.slice(m).join("\n") : null
336 });
337 state.history.splice(0, Math.max(0, state.history.length - 100));
338 if (!state.publish.timer) {
339 state.publish.timer = setTimeout(_publishLoop, 100);
340 }
341 }
342
343 /**
344 * Handles stored events publishing in a timer loop.
345 */
346 function _publishLoop() {
347 function isValid(evt) {
348 lastId = evt.id;
349 return evt.id > state.publish.last && config.filter(evt);
350 }
351 function onSuccess() {
352 state.publish.last = lastId;
353 state.publish.timer = setTimeout(_publishLoop, config.interval);
354 }
355 function onError(err) {
356 info("error publishing log events", err);
357 state.publish.timer = setTimeout(_publishLoop, config.interval);
358 }
359 var lastId = 0;
360 var events = state.history.filter(isValid);
361 if (events.length <= 0) {
362 state.publish.last = lastId;
363 state.publish.timer = null;
364 } else {
365 try {
366 config.publisher(events).then(onSuccess, onError);
367 } catch (e) {
368 onError(e);
369 }
370 }
371 }
372
373 /**
374 * Publishes events to the server.
375 */
376 function _publish(events) {
377 return $.ajax(config.url, {
378 method: "POST",
379 contentType: "application/json",
380 data: JSON.stringify(events)
381 });
382 }
383
384 /**
385 * Checks if an event is an error or a warning.
386 */
387 function _isErrorOrWarning(evt) {
388 return !!evt && /error|warn/.test(evt.level);
389 }
390
391 /**
392 * Creates a string representation (suitable for logging) for any value or
393 * object. The returned string is similar to a JSON representation of the
394 * value, but may be simplified for increased readability.
395 *
396 * @param {Object} val the value or object to convert
397 *
398 * @return {string} the string representation of the value
399 *
400 * @memberof RapidContext.Log
401 */
402 function stringify(val) {
403 var isObject = Object.prototype.toString.call(val) === "[object Object]";
404 var isArray = val instanceof Array;
405 var isSerializable = val && typeof(val.toJSON) === "function";
406 if (val && (isObject || isArray || isSerializable)) {
407 try {
408 return JSON.stringify(val, null, 2);
409 } catch (e) {
410 return String(val);
411 }
412 } else if (val instanceof Error) {
413 var parts = [val.toString()];
414 if (val.stack) {
415 var stack = String(val.stack).trim()
416 .replace(val.toString() + "\n", "")
417 .replace(/https?:.*\//g, "")
418 .replace(/\?[^:\s]+:/g, ":")
419 .replace(/@(.*)/mg, " ($1)")
420 .replace(/^(\s+at\s+)?/mg, " at ");
421 parts.push("\n", stack);
422 }
423 return parts.join("");
424 } else if (val && (val.nodeType === 1 || val.nodeType === 9)) {
425 var el = val.documentElement || val.document || val;
426 var xml = el.outerHTML || el.outerXML || el.xml || String(el);
427 var children = el.childNodes && el.childNodes.length;
428 return xml.replace(/>[^<]*/, children ? ">..." : "/>");
429 } else {
430 return String(val);
431 }
432 }
433
434 // Create namespaces
435 var RapidContext = window.RapidContext || (window.RapidContext = {});
436 var module = RapidContext.Log || (RapidContext.Log = {});
437
438 // Export namespace symbols
439 module.init = init;
440 module.history = history;
441 module.clear = clear;
442 module.level = level;
443 module.context = context;
444 module.error = error;
445 module.warn = warn;
446 module.info = info;
447 module.log = module.debug = module.trace = debug;
448 module.stringify = stringify;
449
450 // Install console loggers and global error handler
451 init();
452
453})(this);
454
RapidContext
Access · Discovery · Insight
www.rapidcontext.com