Использование vue+node для создания внешней системы мониторинга исключений (1) — принципиальный анализ
Навыки, которые вы получите
- Собирать ошибки интерфейса (native, React, Vue)
- Напишите логику сообщения об ошибках
- Напишите службу сбора журнала ошибок, используя Egg.js
- Напишите плагин webpack для автоматической загрузки исходной карты
- Сокращение с помощью сжатого кода исходных позиций sourcemap
- Модульное тестирование с помощью Jest
процесс работы
- ошибка сбора
- сообщить об ошибке
- Код находится в сети и упакован, а файл исходной карты загружен на сервер мониторинга ошибок.
- При возникновении ошибки сервер мониторинга получает ее и записывает в журнал.
- Анализ ошибок на основе исходной карты и содержимого журнала ошибок
(1) Принцип сбора исключений
Сначала посмотрите, как перехватывать исключения.
JS-исключение
Особенность исключения js заключается в том, что оно не приведет к сбою JS-движка, а только завершит выполнение текущей задачи. Например, на странице есть две кнопки.Если при нажатии кнопки возникает ненормальная страница, страница не будет падать в это время, но функция этой кнопки недействительна, а другие кнопки будут по-прежнему действительны.
setTimeout(() => {
console.log('1->begin')
error
console.log('1->end')
})
setTimeout(() => {
console.log('2->begin')
console.log('2->end')
})
На самом деле, если вы не откроете консоль, вы не увидите возникшую ошибку. Как будто ошибка происходила в тишине.
Давайте посмотрим, как собирать такие ошибки.
try-catch
JS, как язык высокого уровня, мы сначала думаем об использовании try-catch для сбора.
setTimeout(() => {
try {
console.log('1->begin')
error
console.log('1->end')
} catch (e) {
console.log('catch',e)
}
})
function fun1() {
console.log('1->begin')
error
console.log('1->end')
}
setTimeout(() => {
try {
fun1()
} catch (e) {
console.log('catch',e)
}
})
Прочитав это, вы можете подумать, что было бы лучше сделать ошибку try-catch внизу. Ведь я как программист, свернувшийся с java, тоже так думаю. Но идеал полный, а реальность очень худая. Давайте посмотрим на следующий пример.
function fun1() {
console.log('1->begin')
error
console.log('1->end')
}
try {
setTimeout(() => {
fun1()
})
} catch (e) {
console.log('catch', e)
}
Все обращают внимание на бегущий результат, исключение не поймано.
Это связано с тем, что функция try-catch в JS очень ограничена, и ее непросто использовать, когда она сталкивается с асинхронностью. Тогда нельзя во всю асинхронность добавлять try-catch, чтобы собирать ошибки, слишком тупо. На самом деле, если вы думаете об асинхронной задаче, она фактически не вызывается верхним уровнем в виде кода, такого как settimeout в этом примере. Если вы подумаете об eventloop, то поймёте, по сути, эти одношаговые функции подобны группе детей без мам, которые ошибаются и не могут найти своих родителей. Конечно, я также думал о какой-то черной магии для решения этой проблемы, такой как выполнение прокси или использование асинхронных методов. В любом случае, давайте посмотрим.
window.onerror
Самым большим преимуществом window.onerror является возможность захвата как синхронных, так и асинхронных задач.
function fun1() {
console.log('1->begin')
error
console.log('1->end')
}
window.onerror = (...args) => {
console.log('onerror:',args)
}
setTimeout(() => {
fun1()
})
-
возвращаемое значение при ошибке
Есть еще одна проблема с onerror, на нее всем стоит обратить внимание, если она возвращает true, то не будет выброшена. В противном случае вы увидите журнал ошибок в консоли.
Прослушивание событий ошибок
window.addEventListener('ошибка',() => {})
На самом деле, onerror — это хорошо, но есть еще тип исключения, которое нельзя поймать. Это ошибка сетевого исключения. Возьмем пример ниже.
<img src="./xxxxx.png">
Представьте, если изображение, которое мы хотим отобразить на странице, вдруг не отображается, и мы не знаем, что это может быть проблемой.
addEventListener это
window.addEventListener('error', args => {
console.log(
'error event:', args
);
return true;
},
true // 利用捕获方式
);
Результаты приведены ниже:
Перехват исключения обещания
Появление промисов в основном позволяет нам решить проблему областей обратного вызова. Это в принципе стандартно для нашей разработки программ. Хотя мы выступаем за использование синтаксиса async/await es7 для записи, это не исключает того, что многие наследственные коды все еще существуют в написании промисов.
new Promise((resolve, reject) => {
abcxxx()
});
В этом случае ни onerror, ни прослушивание ошибок не могут быть перехвачены.
new Promise((resolve, reject) => {
error()
})
// 增加异常捕获
.catch((err) => {
console.log('promise catch:',err)
});
Если только каждое обещание не добавляет метод catch. Но очевидно, что это невозможно.
window.addEventListener("unhandledrejection", e => {
console.log('unhandledrejection',e)
});
Мы можем рассмотреть событие unhandledrejection, чтобы захватить генерацию ошибки и передать ее событию ошибки для унифицированной обработки.
window.addEventListener("unhandledrejection", e => {
throw e.reason
});
асинхронный/ожидающий захват исключений
const asyncFunc = () => new Promise(resolve => {
error
})
setTimeout(async() => {
try {
await asyncFun()
} catch (e) {
console.log('catch:',e)
}
})
На самом деле суть синтаксиса async/await по-прежнему заключается в синтаксисе Promise. Разница в том, что асинхронные методы могут быть перехвачены верхним try/catch.
резюме
| тип исключения | Синхронный метод | асинхронный метод | загрузка ресурсов | Promise | async/await |
|---|---|---|---|---|---|
| try/catch | ✔️ | ✔️ | |||
| onerror | ✔️ | ✔️ | |||
| прослушиватель событий ошибки | ✔️ | ✔️ | ✔️ | ||
| прослушиватель событий необработанного отклонения | ✔️ | ✔️ |
На самом деле, мы можем повторно генерировать исключение, вызванное событием unhandledrejection, чтобы обрабатывать его единообразно через событие ошибки.
Окончательный код выглядит следующим образом:
window.addEventListener("unhandledrejection", e => {
throw e.reason
});
window.addEventListener('error', args => {
console.log(
'error event:', args
);
return true;
}, true);
Используйте vue+node для создания фронтенд системы мониторинга исключений (2) - как собрать фреймворк
Вебпак Инжиниринг
Сейчас эра фронтенд-инжиниринга, и код, экспортируемый инжинирингом, обычно сжат и запутан.
Например:
setTimeout(() => {
xxx(1223)
}, 1000)
Код ошибки указывает на сжатый файл JS, и файл JS выглядит так, как показано на рисунке ниже.
что такое исходная карта
просто скажи,sourceMapЭто файл, в котором хранится информация о местоположении.
Чтобы быть осторожным, то, что сохраняется в этом файле, является местоположением преобразованного кода и соответствующим местоположением до преобразования.
Итак, как использовать sourceMap для восстановления местоположения кода исключения, поговорим об этом в главе анализа исключений.
Vue
Создать проект
Создайте проект напрямую с помощью инструмента vue-cli.
# 安装vue-cli
npm install -g @vue/cli
# 创建一个项目
vue create vue-sample
cd vue-sample
npm i
// 启动应用
npm run serve
В целях тестирования мы временно закрываем eslint, рекомендуется открывать eslint на протяжении всего процесса.
Настроить в vue.config.js
module.exports = {
// 关闭eslint规则
devServer: {
overlay: {
warnings: true,
errors: true
}
},
lintOnSave:false
}
Мы намеренно поместили его в src/components/HelloWorld.vue.
<script>
export default {
name: "HelloWorld",
props: {
msg: String
},
mounted() {
// 制造一个错误
abc()
}
};
</script>
```html
然后在src/main.js中添加错误事件监听
```js
window.addEventListener('error', args => {
console.log('error', error)
})
В это время ошибка будет распечатана в консоли, но событие ошибки не прослушивается.
handleError
Чтобы единообразно сообщать об исключениях, возникающих в Vue, вам необходимо использовать дескриптор handleError, предоставляемый Vue. Этот метод вызывается всякий раз, когда в Vue возникает исключение.
Мы в src/main.js
Vue.config.errorHandler = function (err, vm, info) {
console.log('errorHandle:', err)
}
Результат выполнения:
React
npx create-react-app react-sample
cd react-sample
yarn start
Сделаем баг с хуками useEffect
import React ,{useEffect} from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
useEffect(() => {
// 发生异常
error()
});
return (
<div className="App">
// ...略...
</div>
);
}
export default App;
И добавьте логику мониторинга событий ошибок в src/index.js.
window.addEventListener('error', args => {
console.log('error', error)
})
Однако из результатов работы, хотя журнал ошибок выводится, он все еще захвачен службой.
Тег ErrorBoundary
Граница ошибок может перехватывать только ошибки своих дочерних компонентов.. Граница ошибки не может зафиксировать свои собственные ошибки. Если граница ошибки не может отобразить сообщение об ошибке, ошибка всплывает до ближайшей границы ошибки. Это также похоже на то, как работает catch {} в JavaScript.
Создайте компонент ErrorBoundary
import React from 'react';
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
}
componentDidCatch(error, info) {
// 发生异常时打印错误
console.log('componentDidCatch',error)
}
render() {
return this.props.children;
}
}
Оберните тег приложения в src/index.js
import ErrorBoundary from './ErrorBoundary'
ReactDOM.render(
<ErrorBoundary>
<App />
</ErrorBoundary>
, document.getElementById('root'));
результат финального запуска
Используйте vue+node для создания внешней системы мониторинга исключений (3) — информационная отчетность
Выберите способ связи
Динамически создавать теги img
На самом деле отчетность заключается в отправке перехваченной информации об исключении на серверную часть. Наиболее часто используется первый метод динамического создания меток. Потому что таким образом не нужно загружать какую-либо коммуникационную библиотеку, и страницу не нужно обновлять. В основном на этом принципе строится статистика Google, в том числе статистика Baidu.
new Image().src = 'http://localhost:7001/monitor/error'+ '?info=xxxxxx'
Динамически создавая img, браузер отправляет запрос на получение на сервер. Вы можете поместить данные об ошибке, которые вам нужно сообщить, в строку строки запроса, и таким образом вы можете сообщить об ошибке на сервер.
Ajax-отчетность
На самом деле, мы также можем использовать ajax для сообщения об ошибках, что ничем не отличается от нашей бизнес-программы. Я не буду здесь вдаваться в подробности.
Какие данные сообщать
Давайте сначала посмотрим на параметры события ошибки:
| Имя свойства | имея в виду | Типы |
|---|---|---|
| message | сообщение об ошибке | string |
| filename | неправильный адрес ресурса | string |
| lineno | номер строки исключения | int |
| colno | номер столбца исключений | int |
| error | объект ошибки | object |
| error.message | сообщение об ошибке | string |
| error.stack | сообщение об ошибке | string |
Ядро которого должно быть стеком ошибок, На самом деле, самое важное для нас, чтобы найти ошибку, — это стек ошибок.
Стек ошибок содержит большую часть информации, связанной с отладкой. Он включает ненормальное положение (номер строки, номер столбца), ненормальную информацию
Заинтересованные студенты могут прочитать эту статью
Сериализация данных отчета
Поскольку сообщение может быть передано только в виде строки, нам необходимо сериализовать объект.
Его условно можно разделить на следующие три шага:
-
Деконструировать данные исключения из свойств в объект JSON
-
Преобразовать объект JSON в строку
-
Преобразовать строку в Base64
Разумеется, на бэкенде должна быть проделана соответствующая обратная операция, о которой мы поговорим позже.
window.addEventListener('error', args => {
console.log(
'error event:', args
);
uploadError(args)
return true;
}, true);
function uploadError({
lineno,
colno,
error: {
stack
},
timeStamp,
message,
filename
}) {
// 过滤
const info = {
lineno,
colno,
stack,
timeStamp,
message,
filename
}
// const str = new Buffer(JSON.stringify(info)).toString("base64");
const str = window.btoa(JSON.stringify(info))
const host = 'http://localhost:7001/monitor/error'
new Image().src = `${host}?info=${str}`
}
Используйте vue+node для создания внешней системы мониторинга исключений (4) — сортировка информации
Аномальные данные должны быть получены серверной службой.
Давайте возьмем более популярный фреймворк с открытым исходным кодом eggjs в качестве примера, чтобы продемонстрировать
Собрать проект eggjs
# 全局安装egg-cli
npm i egg-init -g
# 创建后端项目
egg-init backend --type=simple
cd backend
npm i
# 启动项目
npm run dev
Интерфейс загрузки с ошибкой записи
Сначала добавьте новый маршрут в app/router.js.
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
// 创建一个新的路由
router.get('/monitor/error', controller.monitor.index);
};
Создайте новый контроллер (приложение/контроллер/монитор)
'use strict';
const Controller = require('egg').Controller;
const { getOriginSource } = require('../utils/sourcemap')
const fs = require('fs')
const path = require('path')
class MonitorController extends Controller {
async index() {
const { ctx } = this;
const { info } = ctx.query
const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
console.log('fronterror:', json)
ctx.body = '';
}
}
module.exports = MonitorController;
Посмотрите на результат после получения
Журнальный файл
Следующим шагом является регистрация ошибки. Метод реализации можно написать на fs самостоятельно или использовать готовую библиотеку журналов, такую как log4js.
Конечно, eggjs поддерживает наш собственный журнал, поэтому вы можете использовать эту функцию для настройки внешнего журнала ошибок.
Добавьте пользовательскую конфигурацию журнала в /config/config.default.js.
// 定义前端错误日志
config.customLogger = {
frontendLogger : {
file: path.join(appInfo.root, 'logs/frontend.log')
}
}
Добавить вход в /app/controller/monitor.js
async index() {
const { ctx } = this;
const { info } = ctx.query
const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
console.log('fronterror:', json)
// 记入错误日志
this.ctx.getLogger('frontendLogger').error(json)
ctx.body = '';
}
Окончательный эффект
Используйте vue+node для создания внешней системы мониторинга исключений (5) — анализ исключений
Когда дело доходит до анализа исключений, самая важная работа — это восстановление кода, запутанного и сжатого webpack.
Плагин Webpack реализует загрузку SourceMap
Файл исходной карты создается при упаковке веб-пакета, и этот файл необходимо загрузить на сервер мониторинга исключений. Мы используем плагин webpack для выполнения этой функции.
Создать плагин веб-пакета
/source-map/plugin
const fs = require('fs')
var http = require('http');
class UploadSourceMapWebpackPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
// 打包结束后执行
compiler.hooks.done.tap("upload-sourcemap-plugin", status => {
console.log('webpack runing')
});
}
}
module.exports = UploadSourceMapWebpackPlugin;
Загрузите плагин веб-пакета
webpack.config.js
// 自动上传Map
UploadSourceMapWebpackPlugin = require('./plugin/uploadSourceMapWebPackPlugin')
plugins: [
// 添加自动上传插件
new UploadSourceMapWebpackPlugin({
uploadUrl:'http://localhost:7001/monitor/sourcemap',
apiKey: 'kaikeba'
})
],
Добавить логику чтения исходной карты
Добавить логику для чтения файла исходной карты в функции применения
/plugin/uploadSourceMapWebPlugin.js
const glob = require('glob')
const path = require('path')
apply(compiler) {
console.log('UploadSourceMapWebPackPlugin apply')
// 定义在打包后执行
compiler.hooks.done.tap('upload-sourecemap-plugin', async status => {
// 读取sourcemap文件
const list = glob.sync(path.join(status.compilation.outputOptions.path, `./**/*.{js.map,}`))
for (let filename of list) {
await this.upload(this.options.uploadUrl, filename)
}
})
}
Реализовать функцию загрузки http
upload(url, file) {
return new Promise(resolve => {
console.log('uploadMap:', file)
const req = http.request(
`${url}?name=${path.basename(file)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
Connection: "keep-alive",
"Transfer-Encoding": "chunked"
}
}
)
fs.createReadStream(file)
.on("data", chunk => {
req.write(chunk);
})
.on("end", () => {
req.end();
resolve()
});
})
}
Добавить интерфейс загрузки на стороне сервера
/backend/app/router.js
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.get('/monitor/error', controller.monitor.index);
// 添加上传路由
router.post('/monitor/sourcemap',controller.monitor.upload)
};
Добавить интерфейс загрузки исходной карты
/backend/app/controller/monitor.js
async upload() {
const { ctx } = this
const stream = ctx.req
const filename = ctx.query.name
const dir = path.join(this.config.baseDir, 'uploads')
// 判断upload目录是否存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir)
}
const target = path.join(dir, filename)
const writeStream = fs.createWriteStream(target)
stream.pipe(writeStream)
}
окончательный эффект:
При выполнении упаковки веб-пакета исходная карта плагина вызывается для загрузки на сервер.
Анализ стека ошибок
Учитывая, что эта функция требует больше логики, мы собираемся разработать ее как независимую функцию и использовать Jest для модульного тестирования.
Давайте посмотрим на наши потребности
| войти | стек (стек ошибок) | ReferenceError: xxx is not defined\n' + ' at http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js:1:1392' |
|---|---|---|
| SourceMap | немного | |
| выход | исходный стек ошибок | { source: 'webpack:///src/index.js', line: 24, column: 4, name: 'xxx' } |
Создайте структуру Jest
Сначала создайте файл /utils/stackparser.js.
module.exports = class StackPaser {
constructor(sourceMapDir) {
this.consumers = {}
this.sourceMapDir = sourceMapDir
}
}
Создайте тестовый файл stackparser.spec.js в том же каталоге
Мы используем Jest, чтобы выразить вышеуказанные требования как
const StackParser = require('../stackparser')
const { resolve } = require('path')
const error = {
stack: 'ReferenceError: xxx is not defined\n' +
' at http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js:1:1392',
message: 'Uncaught ReferenceError: xxx is not defined',
filename: 'http://localhost:7001/public/bundle.e7877aa7bc4f04f5c33b.js'
}
it('stackparser on-the-fly', async () => {
const stackParser = new StackParser(__dirname)
// 断言
expect(originStack[0]).toMatchObject(
{
source: 'webpack:///src/index.js',
line: 24,
column: 4,
name: 'xxx'
}
)
})
Упорядочено следующим образом:
Ниже мы запускаем Jest
npx jest stackparser --watch
Дисплей не запускается по той простой причине, что мы его еще не реализовали. Давайте реализуем этот метод ниже.
Десериализовать объект ошибки
Сначала создайте новый объект Error, установите для стека ошибок значение Error, а затем используйте библиотеку npm error-stack-parser, чтобы преобразовать его в stackFrame.
const ErrorStackParser = require('error-stack-parser')
/**
* 错误堆栈反序列化
* @param {*} stack 错误堆栈
*/
parseStackTrack(stack, message) {
const error = new Error(message)
error.stack = stack
const stackFrame = ErrorStackParser.parse(error)
return stackFrame
}
текущий результат
Анализ стека ошибок
Затем мы преобразуем местоположение кода в стеке ошибок в исходное местоположение.
const { SourceMapConsumer } = require("source-map");
async getOriginalErrorStack(stackFrame) {
const origin = []
for (let v of stackFrame) {
origin.push(await this.getOriginPosition(v))
}
// 销毁所有consumers
Object.keys(this.consumers).forEach(key => {
console.log('key:',key)
this.consumers[key].destroy()
})
return origin
}
async getOriginPosition(stackFrame) {
let { columnNumber, lineNumber, fileName } = stackFrame
fileName = path.basename(fileName)
console.log('filebasename',fileName)
// 判断是否存在
let consumer = this.consumers[fileName]
if (consumer === undefined) {
// 读取sourcemap
const sourceMapPath = path.resolve(this.sourceMapDir, fileName + '.map')
// 判断目录是否存在
if(!fs.existsSync(sourceMapPath)){
return stackFrame
}
const content = fs.readFileSync(sourceMapPath, 'utf8')
consumer = await new SourceMapConsumer(content, null);
this.consumers[fileName] = consumer
}
const parseData = consumer.originalPositionFor({ line:lineNumber, column:columnNumber })
return parseData
}
Давайте проверим это с помощью Jest
it('stackparser on-the-fly', async () => {
const stackParser = new StackParser(__dirname)
console.log('Stack:',error.stack)
const stackFrame = stackParser.parseStackTrack(error.stack, error.message)
stackFrame.map(v => {
console.log('stackFrame', v)
})
const originStack = await stackParser.getOriginalErrorStack(stackFrame)
// 断言
expect(originStack[0]).toMatchObject(
{
source: 'webpack:///src/index.js',
line: 24,
column: 4,
name: 'xxx'
}
)
})
Взгляните на результаты теста.
местоположение источника журнала
async index() {
console.log
const { ctx } = this;
const { info } = ctx.query
const json = JSON.parse(Buffer.from(info, 'base64').toString('utf-8'))
console.log('fronterror:', json)
// 转换为源码位置
const stackParser = new StackParser(path.join(this.config.baseDir, 'uploads'))
const stackFrame = stackParser.parseStackTrack(json.stack, json.message)
const originStack = await stackParser.getOriginalErrorStack(stackFrame)
this.ctx.getLogger('frontendLogger').error(json,originStack)
ctx.body = '';
}
текущий результат:
Используйте vue+node для создания внешней системы мониторинга исключений (6) — выбор платформы с открытым исходным кодом
Fundebug
FundebugСосредоточьтесь на JavaScript, апплете WeChat, мини-игре WeChat, апплете Alipay, React Native, Node.js и мониторинге ошибок онлайн-приложений Java в режиме реального времени. С момента официального запуска Double Eleven в 2016 году Fundebug обработал в общей сложности более 1 миллиарда ошибок.Платежеспособными клиентами являются Sunshine Insurance, Lizhi FM, 1-to-1 Master, Walnut Programming, Weimai и многие другие бренды. Бесплатная пробная версия приветствуется!
Sentry
Sentry — это система отслеживания ошибок в режиме реального времени с открытым исходным кодом, которая помогает разработчикам отслеживать и устранять нештатные проблемы в режиме реального времени. Основное внимание уделяется непрерывной интеграции, повышению эффективности и улучшению пользовательского опыта. Sentry разделен на SDK на стороне сервера и на стороне клиента.Первый может напрямую использовать предоставляемые им онлайн-сервисы или может быть построен локально; последний обеспечивает поддержку различных основных языков и фреймворков, включая React, Angular, Node, Django, RoR, PHP, Laravel, Android, .NET, JAVA и т. д. В то же время он предоставляет решения для интеграции с другими популярными сервисами, такими как GitHub, GitLab, bitbuck, heroku, slack, Trello и др. В настоящее время проекты компании постепенно применяют Sentry для управления журналом ошибок.
Суммировать
На данный момент мы сформировали MVP (Minimum Viable Product) с базовыми функциями внешнего мониторинга исключений. Есть еще много вещей, которые нужно обновить позже, и ELK можно использовать для анализа и визуализации журналов ошибок. Публикацию и развертывание можно выполнить с помощью Docker. Лучше увеличить функцию контроля разрешений для загрузки и отчетности по eggjs.
Расположение Справочник:Github.com/ su 37joseph x ...
Исправления приветствуются, и звезда приветствуется.