UNPKG

modules-pack

Version:

JavaScript Modules for Modern Frontend & Backend Projects

348 lines (316 loc) 10.2 kB
import { Active, DISCONNECTED, ERROR, first, GET, last, ONE_SECOND, performStorage, REPORT, SET, TIMEOUT, toList, toListValuesTotal, warn } from 'utils-pack' import { stateActionType } from '../redux' import { fork } from '../saga/utils' import { MAX_ERROR_RECORDS, MAX_IDLE_DURATION, MAX_LATENCY_RECORDS, MIN_IDLE_ALERT_DURATION, STATS_TYPE, STORAGE_KEY_REPORTS } from './constants' /** * HELPER FUNCTIONS ============================================================ * ============================================================================= */ export function actionTypeColor (type) { const lastWord = (type.match(/(?:[^a-zA-Z0-9])([a-zA-Z0-9]+)$/) || [])[1] switch (lastWord) { case ERROR: return 'red' case TIMEOUT: case DISCONNECTED: return 'yellow' default: return 'cyan' } } export function latencyColor (milliseconds) { if (milliseconds < 500) return 'yellow' if (milliseconds < 1000) return 'blue' if (milliseconds < 1500) return 'magenta' return 'red' } export class Log { static _statsBy = {} // id static _statsByService = {} // VPS instance names static get errors () { return (performStorage(GET, STORAGE_KEY_REPORTS) || {}).errors || [] } static set errors (errors) { performStorage(SET, STORAGE_KEY_REPORTS, {errors}) } /** * Get Updated Stats for all logged types * * @return {Array} - list of objects containing stats ready for report */ static get stats () { const results = [] for (const id in Log._statsBy) { const data = Log._statsBy[id] if (data.type === STATS_TYPE.API || data.type === STATS_TYPE.SOCKET) results.push(Log.requestStats(id)) if (data.type === STATS_TYPE.TASK) results.push(Log.taskStats(id)) } return results.concat(...Object.values(Log._statsByService)) } /** * Get List of Connected Services */ static get services () { return Object.keys(Log._statsByService) } /** * Log Stats Report from other Services to query later with Log.stats getter * * @param {Object} action - API result action from report */ static saveStats (action) { const {payload: {items = []} = {}, meta: {service} = {}} = action if (!service && !items) return warn(`Log.${Log.saveStats.name} expects payload 'items' and meta 'service'`) Log._statsByService[service] = items } /** * Remove Log Stats for given list of Service names * * @param {Array|String} services - list of services to remove */ static servicePrune (services) { toList(services).forEach(service => { const id = `${Active.SERVICE}_${service}_${STATS_TYPE.SOCKET}` delete Log._statsByService[service] delete Log._statsBy[id] }) } /** * Record Error Action in Storage and Broadcast to Subscribers * * @param {Object} action - Error stateAction */ static handleError (action) { const errors = Log.errors errors.unshift(action) if (errors.length > MAX_ERROR_RECORDS) errors.length = MAX_ERROR_RECORDS try { // Update Error Log Log.errors = errors // Broadcast New Error Report Active.pubsub.publish(stateActionType(ERROR, REPORT), {errorReports: [action]}) } catch (err) { try { // Action payload from API can be an Error object action.payload = action.payload.toString() // Update Error Log Log.errors = errors // Broadcast New Error Report Active.pubsub.publish(stateActionType(ERROR, REPORT), {errorReports: [action]}) } catch (err) { warn('Cannot Convert to JSON!!!', action) } } } /** * Log API action to query stats later * * @param {Object} action - API action result * @param {String} metaKey - meta data key property to use as Stats name */ static api (action, metaKey) { setTimeout(() => { // avoid affecting performance by postponing logging const {meta: {[metaKey]: name, request: {latency, end} = {}} = {}} = action || {} if (!name || !latency) return const record = {latency, timestamp: end || Date.now()} Log.request(name, record, STATS_TYPE.API) }, ONE_SECOND) } /** * Remove Log Stats for given list of API names * * @param {Array|String} metaKeys - list of meta data key property to use as Stats name */ static apiPrune (metaKeys) { toList(metaKeys).forEach(name => { const id = `${Active.SERVICE}_${name}_${STATS_TYPE.API}` delete Log._statsBy[id] }) } /** * Log API/Socket action to query stats later * * @param {String} name - Stats name * @param {Object} record - {latency, timestamp} * @param {String} type - Stats type */ static request (name, record, type) { const id = `${Active.SERVICE}_${name}_${type}` let result = Log._statsBy[id] // Create New Log if (!result) { Log._statsBy[id] = { id, name, type, service: Active.SERVICE, latencies: [record] } } // eslint-disable-line // Update Existing Log else { if (result.latencies.unshift(record) > MAX_LATENCY_RECORDS) result.latencies.length = MAX_LATENCY_RECORDS } } /** * Generate API/Socket Stats by ID * * @param {String} id - as generated by Log.api/socket() * @return {Object} stats - ready for report */ static requestStats (id) { const {latencies, ...props} = Log._statsBy[id] const result = props const now = Date.now() result.latency = Math.round(toListValuesTotal(latencies, 'latency') / latencies.length) result.start = last(latencies).timestamp result.requested = first(latencies).timestamp // last requested result.updated = now result.idle = now - result.requested result.active = result.idle < MAX_IDLE_DURATION if (result.idle > MIN_IDLE_ALERT_DURATION) result.stopped = result.requested return result } /** * Log Socket action to query stats later * * @param {Object} payload - decoded message received from Socket action * @param {Object} [meta] - Socket action meta data */ static socket (payload, meta) { const now = Date.now() setTimeout(() => { // avoid affecting performance by postponing logging let {id: name, start} = payload || {} if (!name || !start) return // do not warn here because initial messages are not signed const {request: {end, latency} = {}} = meta || {} const record = (latency && end) ? {latency, timestamp: end} : {latency: now - start, timestamp: now} Log.request(name, record, STATS_TYPE.SOCKET) }, ONE_SECOND) } /** * Wrapper Saga to log Generator or Normal Function execution time * Note: this wrapper costs about 0.1 milliseconds * * @param {Function} func - generator or normal function to call * @param {String} [args] - other arguments to pass to given function * @return {*} result - from calling given function */ static * saga (func, ...args) { const start = Date.now() const task = yield fork(func, ...args) return yield task.toPromise() .then(result => { Log.task(task, {start, done: Date.now()}) return result }) .catch(error => error) } /** * Log Task to query stats later * * @param {Object} task - redux-saga Task object * @param {Number} [start] - task start time * @param {Number} [done] - task done time * @param {String} [parentId] - task ID of the saga that called this task */ static task (task, {start = Date.now(), done = null, parentId = null} = {}) { setTimeout(() => { // avoid affecting performance by postponing logging const name = task.name const record = done ? {latency: done - start, timestamp: done} : null const id = `${parentId ? '_' + parentId : Active.SERVICE}_${name}` let result = Log._statsBy[id] // Create New Log if (!result) { Log._statsBy[id] = { id, name, type: STATS_TYPE.TASK, service: Active.SERVICE, start, ...done && { done, result: task.result(), latencies: [record] }, task } } // eslint-disable-line // Update Existing Log else { result.task = task if (done) { result.done = done result.result = task.result() const updateLatencies = result.latencies && record && result.latencies.unshift(record) > MAX_LATENCY_RECORDS if (updateLatencies) result.latencies.length = MAX_LATENCY_RECORDS } } }, ONE_SECOND) } /** * Log Task End to query latency stats later * * @param {Object} task - redux-saga Task object * @param {Number} [done] - task done time * @param {String} [parentId] - task ID of the saga that called this task */ static taskEnd (task, {parentId = null, done = Date.now()}) { setTimeout(() => { // avoid affecting performance by postponing logging const name = task.name const id = `${parentId ? '_' + parentId : Active.SERVICE}_${name}` const result = Log._statsBy[id] if (!result) return warn(`Log.${Log.taskEnd.name} found no task named '${task.name}'`) result.done = done result.result = task.result() result.latency = done - result.start }, ONE_SECOND) } /** * Generate Task Stats by ID * * @param {String} id - as generated by Log.task() * @return {Object} stats - ready for report */ static taskStats (id) { const stats = Log._statsBy[id] const {task} = stats const now = Date.now() stats.updated = now stats.active = task.isRunning() stats.error = task.error() if (task.isCancelled() || stats.error) { if (!stats.stopped) stats.stopped = now } else { delete stats.stopped } if (!stats.done && !stats.active && !stats.stopped && !stats.error) { stats.done = now stats.result = task.result() } const {task: extract, latencies, ...result} = stats if (latencies) result.latency = Math.round(toListValuesTotal(latencies, 'latency') / latencies.length) return result } }