|
|
@@ -0,0 +1,185 @@ |
|
|
import React, { Component } from 'react' |
|
|
import CloudWatchLogs from 'aws-sdk/clients/cloudwatchlogs' |
|
|
import Fingerprint2 from 'fingerprintjs2' |
|
|
import StackTrace from 'stacktrace-js' |
|
|
import { promisify } from 'es6-promisify' |
|
|
|
|
|
export default class Logger { |
|
|
|
|
|
events = [] |
|
|
originalConsole = null |
|
|
intervalId = null |
|
|
|
|
|
constructor(accessKeyId, secretAccessKey, region, group, levels = ['error'], interval = 10000, mute = false) { |
|
|
this.valid = accessKeyId && secretAccessKey && region && group |
|
|
this.client = new CloudWatchLogs({ accessKeyId, secretAccessKey, region }) |
|
|
this.client.createLogStreamAsync = promisify(this.client.createLogStream) |
|
|
this.client.putLogEventsAsync = promisify(this.client.putLogEvents) |
|
|
this.group = group |
|
|
this.levels = levels |
|
|
this.interval = interval |
|
|
this.mute = mute |
|
|
} |
|
|
|
|
|
setCache(key, value) { |
|
|
global.localStorage.setItem(`ConsoleCloudWatch:${key}`, value) |
|
|
} |
|
|
|
|
|
getCache(key) { |
|
|
return global.localStorage.getItem(`ConsoleCloudWatch:${key}`) |
|
|
} |
|
|
|
|
|
deleteCache(key) { |
|
|
return global.localStorage.removeItem(`ConsoleCloudWatch:${key}`) |
|
|
} |
|
|
|
|
|
init() { |
|
|
const original = {} |
|
|
for (const level of this.levels) { |
|
|
original[level] = global.console[level] |
|
|
global.console[level] = (message, ...args) => { |
|
|
this.onError(message) |
|
|
if (!this.mute) { |
|
|
original[level](message, ...args) |
|
|
} |
|
|
} |
|
|
} |
|
|
this.originalConsole = original |
|
|
this.intervalId = global.setInterval(this.onInterval.bind(this), this.interval) |
|
|
global.addEventListener('error', this.onError.bind(this)) |
|
|
} |
|
|
|
|
|
refresh() { |
|
|
this.deleteCache('key') |
|
|
this.deleteCache('sequenceToken') |
|
|
this.events.splice(0) |
|
|
} |
|
|
|
|
|
async onError(e, info = {}) { |
|
|
if (!this.valid) { |
|
|
return |
|
|
} |
|
|
this.events.push({ |
|
|
message: await this.createPushMessageFromError(e, info), |
|
|
timestamp: new Date().getTime(), |
|
|
}) |
|
|
} |
|
|
|
|
|
async onInterval() { |
|
|
if (!this.valid) { |
|
|
return |
|
|
} |
|
|
const pendingEvents = this.events.splice(0) |
|
|
if (!pendingEvents.length) { |
|
|
return |
|
|
} |
|
|
const key = await this.createOrRetrieveKey() |
|
|
if (!key) { |
|
|
return |
|
|
} |
|
|
const params = { |
|
|
logEvents: pendingEvents, |
|
|
logGroupName: this.group, |
|
|
logStreamName: key, |
|
|
} |
|
|
const sequenceToken = this.getCache('sequenceToken') |
|
|
if (sequenceToken) { |
|
|
params.sequenceToken = sequenceToken |
|
|
} |
|
|
let nextSequenceToken, match |
|
|
try { |
|
|
({ nextSequenceToken } = await this.client.putLogEventsAsync(params)) |
|
|
} catch (e) { |
|
|
if (!e || e.code !== 'InvalidSequenceTokenException' || !(match = e.message.match(/The next expected sequenceToken is: (\w+)/))) { |
|
|
this.originalConsole.error(e) |
|
|
this.refresh() |
|
|
return |
|
|
} |
|
|
} |
|
|
this.setCache('sequenceToken', nextSequenceToken || match[1]) |
|
|
} |
|
|
|
|
|
async createOrRetrieveKey() { |
|
|
let key |
|
|
if ((key = this.getCache('key'))) { |
|
|
return key |
|
|
} |
|
|
try { |
|
|
key = await new Promise((resolve) => new Fingerprint2().get(resolve)) |
|
|
await this.client.createLogStreamAsync({ |
|
|
logGroupName: this.group, |
|
|
logStreamName: key, |
|
|
}) |
|
|
} catch (e) { |
|
|
if (!e || e.code !== 'ResourceAlreadyExistsException') { |
|
|
this.originalConsole.error(e) |
|
|
this.refresh() |
|
|
return |
|
|
} |
|
|
} |
|
|
this.setCache('key', key) |
|
|
return key |
|
|
} |
|
|
|
|
|
async createPushMessageFromError(e, info = {}) { |
|
|
const message = e && e.message ? e.message : e |
|
|
const timestamp = new Date().getTime() |
|
|
const userAgent = global.navigator.userAgent |
|
|
|
|
|
let stack = null |
|
|
if (e && e.message && e.stack) { |
|
|
stack = e.stack |
|
|
try { |
|
|
stack = await StackTrace.fromError(e, { offline: true }) |
|
|
} catch (_) { |
|
|
} |
|
|
} |
|
|
|
|
|
return JSON.stringify({ |
|
|
message, |
|
|
timestamp, |
|
|
userAgent, |
|
|
stack, |
|
|
...info, |
|
|
}) |
|
|
} |
|
|
|
|
|
createLoggerMiddleware() { |
|
|
return (store) => (next) => (action) => { |
|
|
try { |
|
|
return next(action) |
|
|
} catch (e) { |
|
|
this.onError(e, { |
|
|
action, |
|
|
state: store.getState(), |
|
|
category: 'redux', |
|
|
}) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
createLoggerComponent() { |
|
|
const logger = this |
|
|
return class LoggerComponent extends Component { |
|
|
|
|
|
state = { |
|
|
e: null, |
|
|
} |
|
|
|
|
|
componentDidCatch(e, info) { |
|
|
this.setState({ e }) |
|
|
logger.onError(e, { |
|
|
info, |
|
|
category: 'react', |
|
|
}) |
|
|
} |
|
|
|
|
|
render() { |
|
|
if (this.state.e) { |
|
|
return <div>Fatal Error: {this.state.e.message}</div> |
|
|
} |
|
|
return this.props.children |
|
|
} |
|
|
} |
|
|
} |
|
|
} |