UNPKG

ionic-logging-service

Version:

Logging functionalities for apps built with Ionic framework

966 lines (954 loc) 32.5 kB
import * as i0 from '@angular/core'; import { signal, Injectable } from '@angular/core'; import * as log4javascript from 'log4javascript'; /** * Formats a logging event into JavaScript Object Notation (JSON). * The implemenatation is mainly the same as with log4javascript.JsonLayout, * with an improvement of serializing messages containing '\"'." */ class JsonLayout extends log4javascript.JsonLayout { /** * Formats the log message. */ format(loggingEvent) { const eventObj = { logger: loggingEvent.logger.name, timestamp: loggingEvent.timeStampInMilliseconds, level: loggingEvent.level.toString(), url: window.location.href, message: this.isCombinedMessages() ? loggingEvent.getCombinedMessages() : loggingEvent.messages }; return JSON.stringify(eventObj); } /** * Gets the layout's name. * Mainly for unit testing purposes. * * @return layout's name */ toString() { return "Ionic.Logging.JsonLayout"; } } /** * Logging levels. */ var LogLevel; (function (LogLevel) { /** * All events should be logged. */ LogLevel[LogLevel["ALL"] = 0] = "ALL"; /** * A fine-grained debug message, typically capturing the flow through the application. */ LogLevel[LogLevel["TRACE"] = 1] = "TRACE"; /** * A general debugging event. */ LogLevel[LogLevel["DEBUG"] = 2] = "DEBUG"; /** * An event for informational purposes. */ LogLevel[LogLevel["INFO"] = 3] = "INFO"; /** * An event that might possible lead to an error. */ LogLevel[LogLevel["WARN"] = 4] = "WARN"; /** * An error in the application, possibly recoverable. */ LogLevel[LogLevel["ERROR"] = 5] = "ERROR"; /** * A severe error that will prevent the application from continuing. */ LogLevel[LogLevel["FATAL"] = 6] = "FATAL"; /** * No events will be logged. */ LogLevel[LogLevel["OFF"] = 7] = "OFF"; })(LogLevel || (LogLevel = {})); /** * Helper class for converting log levels from and to different data type. */ class LogLevelConverter { /** * Converts log4javascript.Level to internal LogLevel. * * @param level log4javascript's data type * @return internal data type. */ static levelFromLog4Javascript(level) { switch (level) { case log4javascript.Level.ALL: return LogLevel.ALL; case log4javascript.Level.DEBUG: return LogLevel.DEBUG; case log4javascript.Level.ERROR: return LogLevel.ERROR; case log4javascript.Level.FATAL: return LogLevel.FATAL; case log4javascript.Level.INFO: return LogLevel.INFO; case log4javascript.Level.OFF: return LogLevel.OFF; case log4javascript.Level.TRACE: return LogLevel.TRACE; case log4javascript.Level.WARN: return LogLevel.WARN; default: throw new Error(`invalid level ${level}`); } } /** * Converts string representation to internal LogLevel. * * @param level string representation * @return internal data type. */ static levelFromString(level) { switch (level) { case "ALL": return LogLevel.ALL; case "DEBUG": return LogLevel.DEBUG; case "ERROR": return LogLevel.ERROR; case "FATAL": return LogLevel.FATAL; case "INFO": return LogLevel.INFO; case "OFF": return LogLevel.OFF; case "TRACE": return LogLevel.TRACE; case "WARN": return LogLevel.WARN; default: throw new Error(`invalid level ${level}`); } } /** * Converts internal LogLevel to log4javascript.Level. * * @param internal data type. * @return level log4javascript's data type */ static levelToLog4Javascript(level) { switch (level) { case LogLevel.ALL: return log4javascript.Level.ALL; case LogLevel.DEBUG: return log4javascript.Level.DEBUG; case LogLevel.ERROR: return log4javascript.Level.ERROR; case LogLevel.FATAL: return log4javascript.Level.FATAL; case LogLevel.INFO: return log4javascript.Level.INFO; case LogLevel.OFF: return log4javascript.Level.OFF; case LogLevel.TRACE: return log4javascript.Level.TRACE; case LogLevel.WARN: return log4javascript.Level.WARN; default: throw new Error(`invalid level ${level}`); } } } /** * An appender which sends the log messages to a server via HTTP. * * A typical configuration could be: * * ```json * { * "url": "https://my.backend.xy/LoggingBackend", * "batchSize": 10, * "timerInterval": 60000, * "threshold": "INFO" * } * ``` */ class AjaxAppender extends log4javascript.Appender { static { this.batchSizeDefault = 1; } static { this.timerIntervalDefault = 0; } static { this.thresholdDefault = "WARN"; } /** * Creates a new instance of the appender. * * @param configuration configuration for the appender. */ constructor(configuration) { super(); this.lastFailure = signal(undefined, /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "lastFailure" }] : /* istanbul ignore next */ [])); if (!configuration) { throw new Error("configuration must be not empty"); } if (!configuration.url) { throw new Error("url must be not empty"); } this.ajaxAppender = new log4javascript.AjaxAppender(configuration.url, configuration.withCredentials); this.url = configuration.url; this.withCredentials = configuration.withCredentials ?? false; this.ajaxAppender.setLayout(new JsonLayout(false, false)); this.ajaxAppender.addHeader("Content-Type", "application/json; charset=utf-8"); this.ajaxAppender.setSendAllOnUnload(true); this.ajaxAppender.setFailCallback((message) => { this.lastFailure.set(message); }); // process remaining configuration this.configure({ batchSize: configuration.batchSize || AjaxAppender.batchSizeDefault, threshold: configuration.threshold || AjaxAppender.thresholdDefault, timerInterval: configuration.timerInterval || AjaxAppender.timerIntervalDefault, url: configuration.url, withCredentials: configuration.withCredentials }); } /** * Configures the logging depending on the given configuration. * * Only the defined properties get overwritten. * Neither url nor withCredentials can be modified. * * @param configuration configuration data. */ configure(configuration) { if (configuration) { if (configuration.url && configuration.url !== this.url) { throw new Error("url must not be changed"); } if (configuration.withCredentials && configuration.withCredentials !== this.withCredentials) { throw new Error("withCredentials must not be changed"); } if (configuration.batchSize) { this.setBatchSize(configuration.batchSize); } if (typeof configuration.timerInterval === "number") { this.setTimerInterval(configuration.timerInterval); } if (configuration.threshold) { const convertedThreshold = LogLevelConverter.levelToLog4Javascript(LogLevelConverter.levelFromString(configuration.threshold)); this.setThreshold(convertedThreshold); } } } /** * Appender-specific method to append a log message. * * @param loggingEvent event to be appended. */ append(loggingEvent) { this.ajaxAppender.append(loggingEvent); } /** * Gets the appender's name. * Mainly for unit testing purposes. * * @return appender's name */ toString() { return "Ionic.Logging.AjaxAppender"; } /** * Get the internally used appender. * Mainly for unit testing purposes. */ getInternalAppender() { return this.ajaxAppender; } /** * Returns the number of log messages sent in each request. */ getBatchSize() { return this.ajaxAppender.getBatchSize(); } /** * Sets the number of log messages to send in each request. * * @param batchSize new batch size */ setBatchSize(batchSize) { this.ajaxAppender.setBatchSize(batchSize); } /** * Returns the appender's layout. */ getLayout() { return this.ajaxAppender.getLayout(); } /** * Sets the appender's layout. */ setLayout(layout) { this.ajaxAppender.setLayout(layout); } /** * Returns the length of time in milliseconds between each sending of queued log messages. */ getTimerInterval() { return this.ajaxAppender.getTimerInterval(); } /** * Sets the length of time in milliseconds between each sending of queued log messages. * * @param timerInterval new timer interval */ setTimerInterval(timerInterval) { this.ajaxAppender.setTimed(timerInterval > 0); this.ajaxAppender.setTimerInterval(timerInterval); } /** * Last error message when the appender could not send log messages to the server. * @returns error message */ getLastFailure() { return this.lastFailure.asReadonly(); } } /** * An appender which stores the log messages in the browser's local storage. * * The messages are saved JSON-serialized. * You have to configure which key is used for storing the messages. * * A typical configuration could be: * * ```json * { * "localStorageKey": "myLogs", * "maxMessages": 500, * "threshold": "INFO" * } * ``` */ class LocalStorageAppender extends log4javascript.Appender { static { this.maxMessagesDefault = 250; } static { this.thresholdDefault = "WARN"; } /** * Creates a new instance of the appender. * * @param configuration configuration for the appender. */ constructor(configuration) { super(); this.maxMessages = LocalStorageAppender.maxMessagesDefault; if (!configuration) { throw new Error("configuration must be not empty"); } if (!configuration.localStorageKey || configuration.localStorageKey === "") { throw new Error("localStorageKey must be not empty"); } this.localStorageKey = configuration.localStorageKey; // read existing logMessages this.logMessages = LocalStorageAppender.loadLogMessages(this.localStorageKey); // process remaining configuration this.configure({ localStorageKey: configuration.localStorageKey, maxMessages: configuration.maxMessages || LocalStorageAppender.maxMessagesDefault, threshold: configuration.threshold || LocalStorageAppender.thresholdDefault, }); } /** * Load log messages from local storage which are stored there under the given key. * * @param localStorageKey local storage key * @return stored messages */ static loadLogMessages(localStorageKey) { let logMessages; if (!localStorageKey || localStorage.getItem(localStorageKey) === null) { logMessages = []; } else { logMessages = JSON.parse(localStorage.getItem(localStorageKey) ?? ""); for (const logMessage of logMessages) { // timestamps are serialized as strings logMessage.timeStamp = new Date(logMessage.timeStamp); } } return logMessages; } /** * Remove log messages from local storage which are stored there under the given key. * * @param localStorageKey local storage key */ static removeLogMessages(localStorageKey) { localStorage.removeItem(localStorageKey); } /** * Configures the logging depending on the given configuration. * * Only the defined properties get overwritten. * The localStorageKey cannot be modified. * * @param configuration configuration data. */ configure(configuration) { if (configuration) { if (configuration.localStorageKey && configuration.localStorageKey !== this.localStorageKey) { throw new Error("localStorageKey must not be changed"); } if (configuration.maxMessages) { this.setMaxMessages(configuration.maxMessages); } if (configuration.threshold) { const convertedThreshold = LogLevelConverter.levelToLog4Javascript(LogLevelConverter.levelFromString(configuration.threshold)); this.setThreshold(convertedThreshold); } } } /** * Appender-specific method to append a log message. * * @param loggingEvent event to be appended. */ append(loggingEvent) { // if logMessages is already full, remove oldest element while (this.logMessages.length >= this.maxMessages) { this.logMessages.shift(); } // add event to logMessages const message = { level: LogLevel[LogLevelConverter.levelFromLog4Javascript(loggingEvent.level)], logger: typeof loggingEvent.logger !== "undefined" ? loggingEvent.logger.name : undefined, message: loggingEvent.messages.slice(1), methodName: loggingEvent.messages[0], timeStamp: loggingEvent.timeStamp, }; this.logMessages.push(message); // write values to localStorage localStorage.setItem(this.localStorageKey, JSON.stringify(this.logMessages)); } /** * Gets the appender's name. * Mainly for unit testing purposes. * * @return appender's name */ toString() { return "Ionic.Logging.LocalStorageAppender"; } /** * Get the key which is used to store the messages in the local storage. */ getLocalStorageKey() { return this.localStorageKey; } /** * Get the maximum number of messages which will be stored in local storage. */ getMaxMessages() { return this.maxMessages; } /** * Set the maximum number of messages which will be stored in local storage. * * If the appender stores currently more messages than the new value allows, the oldest messages get removed. * * @param value new maximum number */ setMaxMessages(value) { if (this.maxMessages !== value) { this.maxMessages = value; if (this.logMessages.length > this.maxMessages) { // there are too much logMessages for the new value, therefore remove oldest messages while (this.logMessages.length > this.maxMessages) { this.logMessages.shift(); } // write values to localStorage localStorage.setItem(this.localStorageKey, JSON.stringify(this.logMessages)); } } } /** * Gets all messages stored in local storage. * Mainly for unit testing purposes. * * @return stored messages */ getLogMessages() { return this.logMessages; } /** * Removes all messages from local storage. * Mainly for unit testing purposes. */ clearLog() { this.logMessages = []; localStorage.removeItem(this.localStorageKey); } } /** * Logger for writing log messages. */ class Logger { /** * Creates a new instance of a logger. */ constructor(logger) { if (typeof logger === "undefined") { this.logger = log4javascript.getRootLogger(); } else if (typeof logger === "string") { this.logger = log4javascript.getLogger(logger); } else { this.logger = logger; } } /** * Get the log level. */ getLogLevel() { return LogLevelConverter.levelFromLog4Javascript(this.logger.getLevel()); } /** * Set the log level. * * @param level the new log level */ setLogLevel(level) { this.logger.setLevel(LogLevelConverter.levelToLog4Javascript(level)); } /** * Logs a message at level TRACE. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ trace(methodName, ...params) { if (this.logger.isTraceEnabled()) { const args = [methodName]; for (const param of params) { args.push(this.formatArgument(param)); } this.logger.trace(...args); } } /** * Logs a message at level DEBUG. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ debug(methodName, ...params) { if (this.logger.isDebugEnabled()) { const args = [methodName]; for (const param of params) { args.push(this.formatArgument(param)); } this.logger.debug(...args); } } /** * Logs a message at level INFO. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ info(methodName, ...params) { if (this.logger.isInfoEnabled()) { const args = [methodName]; for (const param of params) { args.push(this.formatArgument(param)); } this.logger.info(...args); } } /** * Logs a message at level WARN. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ warn(methodName, ...params) { if (this.logger.isWarnEnabled()) { const args = [methodName]; for (const param of params) { args.push(this.formatArgument(param)); } this.logger.warn(...args); } } /** * Logs a message at level ERROR. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ error(methodName, ...params) { if (this.logger.isErrorEnabled()) { const args = [methodName]; for (const param of params) { args.push(this.formatArgument(param)); } this.logger.error(...args); } } /** * Logs a message at level FATAL. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ fatal(methodName, ...params) { if (this.logger.isFatalEnabled()) { const args = [methodName]; for (const param of params) { args.push(this.formatArgument(param)); } this.logger.fatal(...args); } } /** * Logs the entry into a method. * The method name will be logged at level INFO, the parameters at level DEBUG. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ entry(methodName, ...params) { if (this.logger.isInfoEnabled()) { const args = [methodName, "entry"]; if (this.logger.isDebugEnabled()) { for (const param of params) { args.push(this.formatArgument(param)); } } this.logger.info(...args); } } /** * Logs the exit of a method. * The method name will be logged at level INFO, the parameters at level DEBUG. * * @param methodName name of the method * @param params optional parameters to be logged; objects will be formatted as JSON */ exit(methodName, ...params) { if (this.logger.isInfoEnabled()) { const args = [methodName, "exit"]; if (this.logger.isDebugEnabled()) { for (const param of params) { args.push(this.formatArgument(param)); } } this.logger.info(...args); } } /** * Formats the given argument. */ formatArgument(arg) { if (typeof arg === "string") { return arg; } else if (typeof arg === "number") { return arg.toString(); } else if (arg instanceof Error) { // JSON.stringify() returns here "{ }" return arg.toString(); } else { try { return JSON.stringify(arg); } catch (e) { return e.message; } } } /** * Returns the internal Logger (for unit tests only). */ getInternalLogger() { return this.logger; } } /** * An appender which stores the log messages in the browser's memory. * * The MemoryAppender is enabled by default. * If you do not specify anything else, it is using this configuration: * * ```JSON * { * "memoryAppender": [ * { * "maxMessages": 250, * "threshold": "ALL" * } * } * ``` */ class MemoryAppender extends log4javascript.Appender { static { this.maxMessagesDefault = 250; } static { this.thresholdDefault = "ALL"; } /** * Creates a new instance of the appender. * * @param configuration configuration for the appender. */ constructor(configuration) { super(); this.logMessages = signal([], /* @ts-ignore */ ...(ngDevMode ? [{ debugName: "logMessages" }] : /* istanbul ignore next */ [])); // process configuration configuration = configuration || {}; this.configure({ maxMessages: configuration.maxMessages || MemoryAppender.maxMessagesDefault, threshold: configuration.threshold || MemoryAppender.thresholdDefault, }); this.maxMessages = MemoryAppender.maxMessagesDefault; } /** * Configures the logging depending on the given configuration. * Only the defined properties get overwritten. * * @param configuration configuration data. */ configure(configuration) { if (configuration) { if (configuration.maxMessages) { this.setMaxMessages(configuration.maxMessages); } if (configuration.threshold) { const convertedThreshold = LogLevelConverter.levelToLog4Javascript(LogLevelConverter.levelFromString(configuration.threshold)); this.setThreshold(convertedThreshold); } } } /** * Appender-specific method to append a log message. * * @param loggingEvent event to be appended. */ append(loggingEvent) { // if logMessages is already full, remove oldest element while (this.logMessages().length >= this.maxMessages) { this.logMessages.update(messages => messages.slice(1)); } // add event to logMessages const message = { level: LogLevel[LogLevelConverter.levelFromLog4Javascript(loggingEvent.level)], logger: typeof loggingEvent.logger === "object" ? loggingEvent.logger.name : undefined, message: loggingEvent.messages.slice(1), methodName: loggingEvent.messages[0], timeStamp: loggingEvent.timeStamp, }; this.logMessages.update(messages => [...messages, message]); } /** * Gets the appender's name. * Mainly for unit testing purposes. * * @return appender's name */ toString() { return "Ionic.Logging.MemoryAppender"; } /** * Get the maximum number of messages which will be stored in memory. */ getMaxMessages() { return this.maxMessages; } /** * Set the maximum number of messages which will be stored in memory. * * If the appender stores currently more messages than the new value allows, the oldest messages get removed. * * @param value new maximum number */ setMaxMessages(value) { this.maxMessages = value; // if there are too much logMessages for the new value, remove oldest messages if (this.logMessages().length > this.maxMessages) { this.logMessages.update(messages => messages.slice(this.logMessages().length - this.maxMessages)); } } /** * Gets all messages stored in memory. * * @return stored messages */ getLogMessages() { return this.logMessages.asReadonly(); } /** * Remove all messages stored in memory. */ removeLogMessages() { this.logMessages.set([]); } } /** * Service for logging functionality. * * By default, the following settings are used: * - logger: root with level WARN * - appender: BrowserConsoleAppender with threshold DEBUG and MemoryAppender with threshold ALL * * Via [configure](#configure), it is possible to amend these settings. */ class LoggingService { /** * Creates a new instance of the service. */ constructor() { // prevent log4javascript to show alerts on case of errors log4javascript.logLog.setQuietMode(true); // configure appender const logger = log4javascript.getRootLogger(); logger.setLevel(log4javascript.Level.WARN); // browser console appender for debugger this.browserConsoleAppender = new log4javascript.BrowserConsoleAppender(); this.browserConsoleAppender.setLayout(new log4javascript.PatternLayout("%d{HH:mm:ss,SSS} %c %m")); this.browserConsoleAppender.setThreshold(log4javascript.Level.ALL); logger.addAppender(this.browserConsoleAppender); // in-memory appender for display on log messages page this.memoryAppender = new MemoryAppender(); this.memoryAppender.setLayout(new log4javascript.PatternLayout("%d{HH:mm:ss,SSS} %c %m")); logger.addAppender(this.memoryAppender); this.configure(); } /** * Configures the logging depending on the given configuration. * * @param configuration configuration data. */ configure(configuration) { if (typeof configuration === "undefined") { configuration = {}; } // set log levels if (typeof configuration.logLevels !== "undefined") { for (const level of configuration.logLevels) { let logger; if (level.loggerName === "root") { logger = log4javascript.getRootLogger(); } else { logger = log4javascript.getLogger(level.loggerName); } try { logger.setLevel(LogLevelConverter.levelToLog4Javascript(LogLevelConverter.levelFromString(level.logLevel))); } catch { throw new Error(`invalid log level ${level.logLevel}`); } } } // configure AjaxAppender if (typeof configuration.ajaxAppender !== "undefined") { this.ajaxAppender = new AjaxAppender(configuration.ajaxAppender); log4javascript.getRootLogger().addAppender(this.ajaxAppender); } // configure LocalStorageAppender if (typeof configuration.localStorageAppender !== "undefined") { const localStorageAppender = new LocalStorageAppender(configuration.localStorageAppender); log4javascript.getRootLogger().addAppender(localStorageAppender); // ensure that an eventual memoryAppender is behind the localStorageAppender const appenders = new Logger().getInternalLogger().getEffectiveAppenders(); const memoryAppender = appenders.find((a) => a.toString() === "Ionic.Logging.MemoryAppender"); if (memoryAppender) { log4javascript.getRootLogger().removeAppender(memoryAppender); log4javascript.getRootLogger().addAppender(memoryAppender); } } // configure MemoryAppender if (configuration.memoryAppender) { this.memoryAppender.configure(configuration.memoryAppender); } // configure BrowserConsoleAppender if (configuration.browserConsoleAppender) { if (configuration.browserConsoleAppender.threshold) { const convertedThreshold = LogLevelConverter.levelToLog4Javascript(LogLevelConverter.levelFromString(configuration.browserConsoleAppender.threshold)); this.browserConsoleAppender.setThreshold(convertedThreshold); } } } /** * Gets the root logger from which all other loggers derive. * * @return root logger */ getRootLogger() { return new Logger(); } /** * Gets a logger with the specified name, creating it if a logger with that name does not already exist. * * @param loggerName name of the logger * @return logger */ getLogger(loggerName) { return new Logger(loggerName); } /** * Gets the last log messages. * * The log messages are retrieved from the internal [MemoryAppender](../memoryappender.html). * That means you will get only the most current messages. The number of the messages is limited * by its maxMessages value. * * @return log messages */ getLogMessages() { return this.memoryAppender.getLogMessages(); } /** * Loads the log messages written by the LocalStorageAppender with the given key. * * @param localStorageKey key for the local storage * @returns log messages */ getLogMessagesFromLocalStorage(localStorageKey) { return LocalStorageAppender.loadLogMessages(localStorageKey); } /** * Remove all log messages. */ removeLogMessages() { this.memoryAppender.removeLogMessages(); } /** * Removes the log messages written by the LocalStorageAppender with the given key. * * @param localStorageKey key for the local storage */ removeLogMessagesFromLocalStorage(localStorageKey) { LocalStorageAppender.removeLogMessages(localStorageKey); } /** * Error messages when the ajax appender could not send log messages to the server. * @returns error messages */ getLastAjaxAppenderFailure() { return this.ajaxAppender ? this.ajaxAppender.getLastFailure() : signal(undefined).asReadonly(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: LoggingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: LoggingService, providedIn: "root" }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.0", ngImport: i0, type: LoggingService, decorators: [{ type: Injectable, args: [{ providedIn: "root" }] }], ctorParameters: () => [] }); /* * Public API Surface of ionic-logging-service. */ /** * Generated bundle index. Do not edit. */ export { AjaxAppender, LocalStorageAppender, LogLevel, LogLevelConverter, Logger, LoggingService, MemoryAppender }; //# sourceMappingURL=ionic-logging-service.mjs.map