1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2025 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 const backup = {};
32
33 // The current log state
34 const 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 const 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 const 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 const 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 const 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 const 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 const 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 const 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 const 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 let 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 let lastId = 0;
348 const events = state.history.filter((evt) => {
349 lastId = evt.id;
350 return evt.id > state.publish.last && config.filter(evt);
351 });
352 if (events.length <= 0) {
353 state.publish.last = lastId;
354 state.publish.timer = null;
355 } else {
356 try {
357 config.publisher(events).then(
358 () => state.publish.last = lastId,
359 (e) => info("error publishing log events", e)
360 ).finally(
361 () => state.publish.timer = setTimeout(_publishLoop, config.interval)
362 );
363 } catch (e) {
364 info("error publishing log events", e);
365 }
366 }
367 }
368
369 /**
370 * Publishes events to the server.
371 */
372 function _publish(events) {
373 return $.ajax(config.url, {
374 method: "POST",
375 contentType: "application/json",
376 data: JSON.stringify(events)
377 });
378 }
379
380 /**
381 * Checks if an event is an error or a warning.
382 */
383 function _isErrorOrWarning(evt) {
384 return !!evt && /error|warn/.test(evt.level);
385 }
386
387 /**
388 * Creates a string representation (suitable for logging) for any value or
389 * object. The returned string is similar to a JSON representation of the
390 * value, but may be simplified for increased readability.
391 *
392 * @param {Object} val the value or object to convert
393 *
394 * @return {string} the string representation of the value
395 *
396 * @memberof RapidContext.Log
397 */
398 function stringify(val) {
399 const isObject = Object.prototype.toString.call(val) === "[object Object]";
400 const isArray = val instanceof Array;
401 const isSerializable = val && typeof(val.toJSON) === "function";
402 if (val && (isObject || isArray || isSerializable)) {
403 try {
404 return JSON.stringify(val, null, 2);
405 } catch (e) {
406 return String(val);
407 }
408 } else if (val instanceof Error) {
409 const parts = [val.toString()];
410 if (val.stack) {
411 const stack = String(val.stack).trim()
412 .replace(`${val.toString()}\n`, "")
413 .replace(/https?:.*\//g, "")
414 .replace(/\?[^:\s]+:/g, ":")
415 .replace(/@(.*)/mg, " ($1)")
416 .replace(/^(\s+at\s+)?/mg, " at ");
417 parts.push("\n", stack);
418 }
419 return parts.join("");
420 } else if (val && (val.nodeType === 1 || val.nodeType === 9)) {
421 const el = val.documentElement || val.document || val;
422 const xml = el.outerHTML || el.outerXML || el.xml || String(el);
423 const children = el.childNodes && el.childNodes.length;
424 return xml.replace(/>[^<]*/, children ? ">..." : "/>");
425 } else {
426 return String(val);
427 }
428 }
429
430 // Create namespaces
431 const RapidContext = window.RapidContext || (window.RapidContext = {});
432 const module = RapidContext.Log || (RapidContext.Log = {});
433
434 // Export namespace symbols
435 module.init = init;
436 module.history = history;
437 module.clear = clear;
438 module.level = level;
439 module.context = context;
440 module.error = error;
441 module.warn = warn;
442 module.info = info;
443 module.log = module.debug = module.trace = debug;
444 module.stringify = stringify;
445
446 // Install console loggers and global error handler
447 init();
448
449})(globalThis);
450
RapidContext
Access · Discovery · Insight
www.rapidcontext.com