предисловие
Как говорится, проектирование системы, не зависящее от бизнес-сценариев, называется хулиганством.
Идея и дизайн этой системы также имеют бизнес-подтекст, а продукция компании ориентирована как на сторону С, так и на сторону В. Когда мы развиваем лендинговый бизнес, мы часто сталкиваемся с очень головной болью:
客户:这个页面怎么报错了?让你们开发给看一下。
客户:为什么这个模块没数据,控制台还有报错,怎么回事?
苦逼coder: 请问您能具体描述一些出错的场景和步骤吗?最好能帮忙截图一些页面细节。
等客户操作半天, 给来一堆有用没用的信息......
感叹!写代码为什么这么难,业务怎么这么让人头疼...
Поэтому я решил создать систему мониторинга для сбора ошибок и поиска проблем с возвратом. Сэкономим время и силы при отслеживании прошлых проблем и сможем подсчитать уязвимости системы.
Преимущества чтения
«Какая польза от того, что я читаю статьи, которые вы написали?»
- может освоить, какСбор ошибок внешнего интерфейса.
- узнать, как спроектироватьплагин веб-пакета
- Как отлаживать плагины
- К пониманиюУзловой серверосновное содержание
- К пониманиюбаза данныхосновная операция
- К пониманиюdockerПростой в использовании
- начать одинnginxсервер
- выучить позу для себяСоздайте производственную среду
- учитьразвертыватьСлужить
Системный дизайн
Основная идея системного проектирования отличается от решения бизнес-логики.В системном проектировании мы должныОт большого к маленькому,Зависит отцелое на части, рассмотрите следующие вопросы:
шаг | вопрос | строить планы |
---|---|---|
1 | Какая у нас была проблема? | Ошибки внешнего интерфейса трудно отследить |
2 | Общая идея решения проблемы | Собирать ошибки, анализировать ошибки, отображать ошибки |
3 | Преобразование решения в модель системы | Нужен набор систем, которые умеют собирать, хранить, анализировать и отображать ошибки |
4 | Разделенная подсистема | интерфейсная система, серверная система, подключаемая система; |
5 | Разделение блока подсистемы | В соответствии с характеристиками каждой системы и конкретными решаемыми задачами |
6 | Реализация модуля | код |
7 | Последовательная отладка системы | Оперативная отладка между системами |
8 | Оптимизация системы | Подумайте, может ли то, что было реализовано, решить исходную проблему, и где это могло бы быть лучше? |
В этой системе мы будем собирать ошибки из внешнего интерфейса, загружать их на сервер, анализировать и хранить их через сервер и предоставлять интерфейс потребления, а также в определенной степени анализировать исходную карту для вывода информации об ошибках исходного кода.
Основываясь на вышеизложенном, мы создаем интерфейсный проект в качестве лаборатории для получения данных об ошибках, загрузки исходной карты через плагин веб-пакета и ее анализа на стороне сервера. На стороне сервера мы используем node в качестве языка разработки и используем базу данных mysql для хранения информации об ошибках и, наконец, отображаем собранные ошибки во внешнем интерфейсе.
Интерфейсная реализация
Чтобы быстро построить интерфейсный проект, мы используем Create-React-App в качестве каркаса для инициализации проекта.
1. 先安装 Cra
npm i -g create-react-app
2. 初始化项目
npx create-react-app react-repo
3. 启动应用
cd react-repo
npm start
Сюда легко добраться, посмотреть большеОфициальный сайт.
тип ошибки
Для интерфейсных ошибок мы разделяем их на две категории, одна из которыхошибка страницы, такие как ряд ошибок, вызывающих исключения страниц и белые экраны страниц;Ошибка сети, то есть ошибки, вызванные исключениями на стороне сервера, или ошибки, которые не соответствуют установленным внешним и внутренним ограничениям.
Структура сообщения об ошибке
Тело сообщения об ошибке содержит следующую информацию:
- Информация о пользователе
- сообщение об ошибке
- Информация об устройстве пользователя
вИнформация о пользователеа такжесообщение об ошибкеНам нужно собирать и загружать самим, и мы можем получить информацию об устройстве пользователя на стороне сервера, не тратя ресурсы на загрузку.
/**
*/
interface ErrorInfo {
/**
* 用户id
*/
userId: string;
/**
* 账户名称
*/
username: string;
/**
* 租户
*/
tenant: string;
/**
* 请求源地址
*/
origin: string;
/**
* 用户设备
*/
userAgent: string;
/**
* 错误信息单元
*/
error: {
/**
* 错误信息
*/
message: string;
/**
* 错误栈,详细信息
*/
stack: string;
/**
* 错误文件名称
*/
filename: string;
/**
* 错误行
*/
line?: number;
/**
* 错误列
*/
column?: number;
/**
* 错误类型
*/
type: string;
};
// 发生错误的时间戳
timestamp: number;
};
Перехватывать глобальные ошибки
Первое, что приходит на ум, это то, что для обработки глобальных ошибок в среде браузера мы можем слушатьonErrorмероприятие
window.addEventListener(
"error",
(msg, url, row, col, error) => {
// 错误信息处理通道
processErrorInfo(msg, url, row, col, error);
},
);
использовать здесьaddEventListener, Вы можете гарантировать, что это не повлияет на выполнение другой ошибки монитора событий.
Использование ErrorBoundary
В React есть жизненный цикл componentDidCatch, который может перехватывать ошибки, выдаваемые подкомпонентами.Здесь мы используем его для перехвата ошибок внутренних компонентов и добавления удобных подсказок об ошибках.
componentDidCatch(error, errorInfo) {
processErrorInfo(error);
}
ErrorBoundary может захватывать только те ошибки, которые не фиксируются внутренним уровнем.В некоторых компонентах с четкой логикой мы можем активно сообщать об ошибках посредством логического суждения и по-прежнему использовать канал обработки информации об ошибках processErrorInfo.
Блокировать сетевые ошибки
В этом проекте я использую axios в качестве нашей библиотеки ajax, которая предоставляет перехватчик-перехватчик для предварительной обработки запроса и ответа, поэтому здесь мы можем выполнить унифицированный перехват сетевых ошибок.
В нашем проекте рекомендуется единообразно инкапсулировать ajax, что очень удобно для согласованной обработки запросов.
import axios from "axios";
axios.interceptors.response.use(
response => response,
error => {
// 对网络错误进行拦截
processErrorInfo(error);
return Promise.reject(error);
}
);
Здесь мы решили продолжать выдавать ошибки после того, как ошибка была перехвачена, чтобы обеспечить непрерывность запроса, поскольку нам может потребоваться обработать информацию об ошибке на конкретном бизнес-уровне. Конечно, вы также можете внести соответствующие коррективы в соответствии с вашим конкретным бизнесом.
неправильное форматирование
Наблюдая за вышеперечисленными слоями методов перехвата, мы можем обнаружить, что все мы используем функцию processErrorInfo. Из-за множества типов ошибок, которые мы собрали, их необходимо было отформатировать перед загрузкой на сервер.
// 生成 YYYY-MM-DD hh:mm:ss 格式的时间
function datetime() {
const d = new Date();
const time = d.toString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1");
const day = d.toJSON().slice(0, 10);
return `${day} ${time}`;
}
// 生产最终的上报数据,包含了用户信息和错误信息
const processErrorInfo = (info) => {
let col;
let line;
let filename;
let message;
let stack;
let type = "error";
if (info instanceof ErrorEvent) {
col = info.colno;
line = info.lineno;
filename = info.filename;
message = info.message;
stack = info.error.stack;
type = info.type;
} else if (info instanceof Error) {
message = info.message;
stack = info.stack;
}
// 伪造一份用户信息
// 这里应该对接我们实际业务中的用户信息
const userInfo = {
user_id: "ein", // 用户id
user_name: "ein@mail.com", // 用户名称
tenant: "mail" // 租户名称
}
return {
...userInfo,
col,
line,
filename,
message,
stack,
type,
timestamp: datetime()
};
}
После сборки сообщения об ошибке сообщите об ошибке ниже.
неправильная загрузка
/**
* @param {格式化后的错误信息} error
*/
export const uploadError = error => {
axios
.post("/errors/upload", JSON.stringify(data))
.then(res => {
console.log("upload result", res);
})
.catch(err => {
console.log("upload error", err);
});
};
Мы устанавливаем внутренний маршрут /errors/upload для получения информации об ошибках, На этом внешний сбор, форматирование и загрузка ошибок в основном завершены.
Среди этих сообщений об ошибках наиболее важным является поле стека, которое содержит конкретную информацию о нашей ошибке, и эта часть не должна отсутствовать.
Внимательные одноклассники могут обнаружить, что в загруженном нами сообщении об ошибке есть несколько полей, и эти поля представляют собой ошибочные имена файлов и слои строк, то есть конкретное местонахождение ошибки. В некоторых сценариях мы не можем получить эти поля напрямую из параметров события обратного вызова, но нет способа решить, как это решить? Давайте продолжим чтение.
Реализация сервера
Сначала задумайтесь над вопросом: зачем нам сервер в этой системе? Может ли эта система быть завершена только на переднем конце?
“ 浏览器不是有 localStorage, sessionStorage 这样的 API 吗? 也有 indexdb 这样的浏览器数据库。
我们可以用它来存储错误,然后进行集中展示。 ”
Может быть, у некоторых студентов есть вышеперечисленные вопросы? В среде браузера нам не хватало уровня сохраняемости данных, поэтому в ходе непрерывного развития браузеров были добавлены некоторые API, которые можно использовать для хранения данных.
Однако сами эти методы хранения имеют ограничения по объему хранилища, кроме того, браузеры, используемые пользователями, богаты и разнообразны, как мы синхронизируем эти данные? Как обеспечить согласованность интерфейса?
Что нам нужно, так это хранилище данных, которое может работать со всеми пользователями и может обеспечить высокий уровень параллелизма, поэтому для выполнения этой работы нам нужна серверная служба.
Подводя итог, мы выбираем Node в качестве языка разработки бэкенда.Во-первых, он имеет хорошую поддержку параллелизма.Во-вторых,Node прост в использовании для внешнего интерфейса.Мы выбираем mysql для базы данных.
построить инфраструктуру
Чтобы сделать серверную службу, сначала нам нужно создать проект узла. Вот, я выбираюKoa2В качестве бэкэнд-фреймворка это фреймворк узла, созданный оригинальной командой Express и поддерживающий асинхронный синтаксис. Конечно, вы также можете выбратьExpressилиEgg.
Можно использовать строительные инструментыkoa-generatorИли другие варианты быстрого создания скелета проекта Node.
предложениеНезнакомый с серверной разработкой или незнакомый с Node разрабатывал, он построил проект, здесь мы должны построить проект.
1. 创建一个工程目录并初始化
mkdir error-monitor-node-server
cd error-monitor-node-server
npm init
git init
2. 安装依赖项
// 我们先安装核心的几个依赖
npm i koa koa-router mysql -S
3. 目录结构
- config // 系统相关设置
- controller
- logs // 日志
- middleware // 中间件
- mysql // 数据库
- routers // 路由
- utils // 工具类
index.js // 入口文件
Конкретные зависимости показаны на рисунке ниже. Мы будем использовать их позже. Вы можете установить их заранее или установить их при использовании.
В системе мы используем много синтаксиса старшей версии ES, поэтому нам нужно ввестиbabelсделать преобразование синтаксиса.
Koa2
koa2 инкапсулирует собственный API Node, в основном работая с запросами и ответами. Предоставляется переменная времени выполнения, называемая контекстом, которая монтирует некоторые общие операции с этим свойством объекта. И договорились об организации промежуточного ПО, с известнымлуковая модельпорядок выполнения промежуточного программного обеспечения.
Заинтересованные студенты могут прочитатьисходный код, который является относительно коротким и лаконичным, гдеkoa-composeПакеты — это реализации промежуточного программного обеспечения.
Ошибка получения интерфейса
const router = require("koa-router")();
const errorsController = require("../controller/c-error");
// 上传错误信息
router.post("/errors/upload", errorsController.uploadErrors);
const uploadErrors = async ctx => {
try {
// request body
const body = ctx.request.body;
// 将错误信息写入表中
await ctx.mysql.whriteError(body);
ctx.response.type = "json";
ctx.body = "true";
} catch (e) {
ctx.statusCode = 500;
ctx.response.type = "json";
ctx.body = "false";
}
};
const Koa = require("Koa");
const mysql = require("./mysql/index");
const app = new Koa();
app.context.mysql = mysql;
Здесь мы используем koa-router для управления маршрутизацией и разделяем маршрутизатор и контроллер, чтобы сохранить структуру ясности.
Функция uploadError обрабатывает специфическую бизнес-логику, здесь мы принимаем параметры из запроса, то есть информацию об ошибке, которую мы передали через интерфейс на фронтенде.
Здесь ctx — это упомянутый выше контекст, который будет передаваться между промежуточным программным обеспечением. Мы также привязываем экземпляр mysql к ctx, чтобы нам не требовался каждый файл.
mysql
Далее нам нужно написать логику части базы данных.
安装 mysql ---
npm i mysql -S
数据库配置 ---
databaseConfig: {
database: "error_monitor_ci",
user: "root",
password: "1234567890",
host: "localhost"
}
操作数据库 ---
const mysql = require("mysql");
const { databaseConfig } = require("../config/default");
const sqls = require("./sqls");
const { logger } = require("../middleware/log");
const connection = mysql.createConnection(databaseConfig);
class MySQL {
constructor() {
this.table = "errors";
this.init();
}
init = () => {
// 初始化表
connection.query(sqls.createTable(this.table), (err, res, fields) => {
if (err) {
logger.error("connect errors table failed...", err);
}
});
};
whriteError = error =>
new Promise((r, j) => {
connection.query(sqls.writeError(this.table), error, (err, res) => {
if (err) {
logger.error(err);
j(err);
} else {
r(res);
}
});
});
}
Как видите, оперируя базой данных, мы разделяемся на следующие части:
- Подключиться к базе данных
- построить таблицу
- выполнить оператор вставки
Мы ставим sql-оператор отдельно, если нет плана расширения, вы также можете поставить sql-оператор и логику работы с базой данных вместе.
/**
* 注意:
* 1. 表名带引号为小写,不带默认大写
* 2. 列名带引号为小写,不带默认大写
* 3. 字段类型标注需要大写
* 4. 建表语句末尾不能加逗号
* 6. 默认取时间戳 current_timestamp
* 7. 长文本不适合用char来存储,可以选择使用txt类型
*/
module.exports = {
createTable: tb => `create table if not exists ${tb}(
id int primary key auto_increment,
user_id varchar(255) not null,
user_name varchar(255) not null,
tenant varchar(255) not null,
timestamp datetime default now(),
col int(1),
line int(1),
filename varchar(255) ,
message varchar(255) not null,
stack text not null,
type varchar(255) not null,
sourcemap text
) engine=InnoDB auto_increment=0 default charset=utf8`,
writeError: tb => `INSERT INTO ${tb} SET ?`,
};
Когда экземпляры класса MySQL мы будем выполнять метод init сначала, на этот раз будет запланировано.
Когда таблица не существует, мы создадим табличную операцию.
writeErrorКак раз сейчас мы получаем/errors/uploadПо запросу выполняетсяctx.mysql.whriteErrorметод.
CORS
Изображение ниже кажется вам знакомым?
Поскольку наш интерфейс и сервер работают на двух портах, мы столкнемся с междоменными проблемами при вызове интерфейса.
Не паникуйте, мы можем решить это с помощью следующей позы.
const Koa = require("Koa");
const cors = require("koa2-cors");
const app = new Koa();
app.use(
cors({
origin: "*",
credentials: true, //是否允许发送Cookie
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], //设置所允许的HTTP请求方法
allowHeaders: ["Content-Type", "Authorization", "Accept"] //设置服务器支持的所有头信息字段
})
);
Здесь мы разрабатываем локально, вы можете установить cors origin на «*». ноИзбегайте всеми средствамиВы не можете установить это в производственной среде. Необходимо указать имя внешнего домена производственной среды. В противном случае ваш сервис будет уязвим для атак.
список ошибок запроса
Теперь часть, где мы загружаем ошибку, почти завершена. Далее вам также понадобится интерфейс для запроса списка ошибок, который предоставляется интерфейсу для отображения информации об ошибках. Давайте закончим эту часть вниз:
增加一条路由控制 ---
// 获取错误信息
router.get("/errors/list", getErrors);
// 获取错误列表
getErrors = async ctx => {
const webErrors = await ctx.mysql.query();
ctx.body = {
code: 200,
msg: "success",
data: webErrors
};
}
// mysql
query = () =>
new Promise((r, j) => {
connection.query(sqls.all(this.table), (err, res) => {
if (err) {
logger.error(err);
j(err);
} else {
r(res);
}
});
});
sqls.all = tb => `SELECT * from ${tb}`,
Теперь, когда интерфейс для запроса списка ошибок завершен, мы создадим несколько простых компонентов отображения во внешнем интерфейсе для отображения этой информации.
getList = () =>
axios
.get("/errors/list")
.then(res => res.data.data)
.catch(err => []);
const request = async setList => {
const list = await getList();
setList(list);
};
function App() {
const [list, setList] = useState([]);
useEffect(() => {
request(setList);
}, []);
return (
<div className="App">
<header className="App-header">
<p>Error Monitor</p>
<List list={list} />
</header>
</div>
);
}
Вы можете видеть, что информация, которую мы загрузили, получена через интерфейс.
Студенты, которые внимательно наблюдают, могут обнаружить, что над ними есть линия.ошибка исходного файлалиния информации, что это такое? Давайте продолжим чтение.
плагин исходной карты
На данный момент наш интерфейс, серверная часть и база данных завершены, и весь процесс сообщения об ошибках завершен. Но, как видите, информация о стеке ошибок, представляющая собой наборchunk.jsдокумент.
Сейчас наша фронтенд-разработка в основном использует фреймворки или библиотеки, такие как React, Vue, Less, Sass, а также множество новых версий синтаксиса и кучу различных сторонних зависимых SDK.
Однако при развертывании службы в сети код будет упакован в блоки, сжат и объединен. Поэтому код онлайн-среды не может удовлетворить нас для анализа причины ошибки.
Следовательно, нам также необходимо восстановить эту сжатую информацию, чтобы мы могли точно определить конкретное место ошибки.
“ 如果能够像浏览器这样,显示具体的错误位置,那简直太好了 ”
анализ проблемы
Принимая во внимание вышеприведенное видение, давайте проведем анализ, чтобы определить наше решение.
1. 我们想要什么 ?
我们想要确定错误的具体位置
2. 我们有什么 ?
我们有压缩后的错误信息
3. 我们可以做什么 ?
尝试通过压缩后的信息,还原出来原始的错误信息
Основываясь на вышеизложенных идеях, после исследования сообщества мы решили получить исходное сообщение об ошибке, проанализировав исходную карту.
шаг | действовать |
---|---|
1 | Собирайте файлы карт во время упаковки |
2 | Загрузите файл карты на сервер |
3 | Проанализируйте исходную информацию о файле при получении сообщения об ошибке от внешнего интерфейса. |
4 | Исходное сообщение об ошибке репозитория |
5 | При получении списка ошибок во внешнем интерфейсе вместе верните исходное сообщение об ошибке. |
Поскольку наши файлы исходных карт создаются на этапе упаковки, мы не застрахованы от разработки плагина для этого.
webpack plugin
Наш интерфейсный проект упакован с помощью веб-пакета, поэтому давайте разработаем плагин веб-пакета, чтобы завершить работу по загрузке исходной карты.
Чтобы разработать плагин веб-пакета, позвольте мне сначала кратко понятьплагин веб-пакета.
webpack 插件用于在打包期间来扩展 webpack 打包行为。
如果你在使用 webpack,可能对 html-webpack-plugin, HappyPack, DllReferencePlugin 这些已经比较熟悉了。
На уровне дизайна мы по-прежнему поддерживаем идею построения сверху вниз, сначала описываем наш интерфейс, а затем пишем конкретную логику.
/* config-overrides.js */
一个 webpack plugin 应该长这个样子:
1. 它是一个类,可以被实例化
2. 可以接收一些配置参数
const path = require("path");
const EmWebpackPlugin = require("error-monitor-webpack-plugin");
const pathResolve = p => path.join(process.cwd(), p);
module.exports = function override(config) {
//do some stuff with the webpack config...
config.plugins.push(
new EmWebpackPlugin({
url: "localhost:5000/sourcemap/upload", // 后端上传 source-map 接口
outputPath: config.output.path // 打包 output 路径
})
);
return config;
};
Давайте реализуем внутреннюю логику плагина.
const { uploadSourceMaps, readDir } = require("./utils");
/**
* @param {插件配置桉树} options
*/
function errorMonitorWebpackPlugin(options = {}) {
this.options = options;
}
// 插件必须实现一个 apply 方法,这个会在 webpack 打包时被调用
errorMonitorWebpackPlugin.prototype = {
/**
* @param {编译实例对象} compiler
*/
apply(compiler) {
const { url, outputPath } = this.options;
/**
* compiler hook: done
* 在打包结束时执行
* 可以获取到访问文件信息的入口
* https://webpack.js.org/api/compiler-hooks/#done
*/
if (url && outputPath) {
compiler.hooks.done.tap("upload-sourcemap-plugin", status => {
// 读入打包输出目录,提取 source-map 文件
const sourceMapPaths = readDir(outputPath);
sourceMapPaths.forEach(p =>
uploadSourceMaps({
url: `${url}?fileName=${p.replace(outputPath, "")}`,
sourceMapFile: p
})
);
});
}
}
};
module.exports = errorMonitorWebpackPlugin;
Отфильтруйте и извлеките файлы исходной карты:
const p = require('path');
const fs = require('fs');
// 我们仅取出 .map 文件和 manifest.json 文件
const sourceMapFileIncludes = [/\.map$/, /asset-manifest\.json/];
/**
* 递归读取文件夹
* 输出source-map文件目录
*/
readDir: path => {
const filesContent = [];
function readSingleFile(path) {
const files = fs.readdirSync(path);
files.forEach(filePath => {
const wholeFilePath = p.resolve(path, filePath);
const fileStat = fs.statSync(wholeFilePath);
// 查看文件是目录还是单文件
if (fileStat.isDirectory()) {
readSingleFile(wholeFilePath);
}
// 只筛选出manifest和map文件
if (
fileStat.isFile() &&
sourceMapFileIncludes.some(r => r.test(filePath))
) {
filesContent.push(wholeFilePath);
}
});
}
readSingleFile(path);
return filesContent;
}
Загрузите файлы на сервер, здесь мы используем http для завершения загрузки файлов, или вы можете использовать другие фреймворки RPC для завершения этого шага.
const { request } = require("http");
const uploadSourceMaps = options => {
const { url, sourceMapFile } = options;
if (!url || !sourceMapFile)
throw new Error("params 'url' and 'sourceMapFile' is required!!");
const [host, o] = url.split(":");
const i = o.indexOf("/");
const port = o.slice(0, i);
const path = o.slice(i);
const req = request({
host,
path,
port,
method: "POST",
headers: {
"Content-Type": "application/octet-strean",
// 由于我们的文件通过二进制流传输,所以需要保持长连接
// 设置一下request header
Connection: "keep-alive",
"Transfer-Encoding": "chunked"
}
});
fs.createReadStream(sourceMapFile)
.on("data", chunk => {
// 对request的写入,会将数据流写入到 request body
req.write(chunk);
})
.on("end", () => {
// 在文件读取完成后,需要调用req.end来发送请求
req.end();
});
},
На этом наш плагин source-map для загрузки webpack завершен, давайте разберемся с логикой сервера.
Плагин локальной отладки
После того, как подключаемый модуль веб-пакета будет реализован, как нам подключиться к внешнему проекту для использования и отладки?
方法:
1. 通过相同路径引用
这种方式很直接,也不需要额外操作,但是调试效果比较差
2. npm link
npm 提供了用于开发 npm 模块时的调试方案
Сначала добавьте ссылку на проект плагина webpack
Спуститесь и свяжите наш разработанный плагин веб-пакета во внешнем проекте.
Вы можете видеть, что наш плагин уже существует в node_modules.
Затем импортируйте его в конфигурацию веб-пакета, импортируйте его напрямую с абсолютным путем и используйте его.
const EmWebpackPlugin = require("error-monitor-webpack-plugin");
Получение и разрешение Source-Map
Во-первых, нам нужно добавить новый маршрут на серверную часть для получения запросов от плагинов.
const router = require("koa-router")();
const sourcemapController = require("../controller/c-sourcemap");
// 上传sourcemap文件
router.post("/sourcemap/upload", sourcemapController.uploadSourceMap);
module.exports = router;
const qs = require("querystring");
const path = require("path");
const { sourceMapConfig } = require("../config/default");
const { writeFile, delDir } = require("../utils/writeFile");
exports.uploadSourceMap = ctx => {
ctx.req
.on("data", data => {
// 接收到的data会是一串二进制流
// 我们进行序列化
const souremapContent = data.toString("utf8");
const { querystring } = ctx.request;
// 并从请求 url 中提取出 outputPath 参数
const { fileName } = qs.parse(querystring);
// 我们将收集到的 source-map 以文件形式写入
writeFile(path.join(sourceMapConfig.dir, fileName), souremapContent);
})
.on("close", () => {})
.on("error", () => {})
.on("end", () => {});
};
хранить исходную карту
Сохраняем source-map на сервере на уровне исходной директории, что удобно для последующего парсинга source-map
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
exports.writeFile = (fileName, content, options = {}) => {
if (!content || !fileName) {
throw new Error("'content', 'fileName' is required!!");
}
try {
const { prefixDir = process.cwd() } = options;
const pieces = fileName
.replace(prefixDir, "")
.split(/\//)
.filter(p => !!p);
let i = 0;
if (pieces.length > 1) {
let currentPath = prefixDir;
// 自动创建空目录
while (i < pieces.length - 1) {
const checkedPath = path.resolve(currentPath, pieces[i]);
if (!fs.existsSync(checkedPath)) {
fs.mkdirSync(checkedPath);
}
currentPath = checkedPath;
i++;
}
}
fs.writeFile(fileName, content, e => {
if (e) throw e;
});
} catch (e) {
throw new Error("write file failed, beacuse of these:", e);
}
};
использовать файл карты
Теперь, когда файл карты сохранен, его можно использовать для потребления.
Теперь рассмотрим вопрос, мы анализируем при сообщении об ошибках или когда интерфейс получает список ошибок?
Представьте себе конкретный сценарий: когда система сообщает об ошибках, она сообщается одна за другой, а когда список получен, он получается пакетами. Очевидно, что мы должны разобрать исходную карту и сохранить ее в базе данных при создании отчета.
Давайте реализуем конкретную логику:
// 扩展刚才的uploadErrors
const uploadErrors = async ctx => {
try {
const body = ctx.request.body;
const { stack } = body;
// 解析 source-map
const sourceInfo = findTheVeryFirstFileInErrorStack(stack);
const sourceMapInfo = await soucemapParser(sourceInfo);
// 将 source-map 信息插入表中
await ctx.mysql.whriteError({ ...body, sourcemap: sourceMapInfo });
...
} catch (e) {
...
}
};
В начале мы упомянули, что много раз сообщаемая нами информация об ошибке не содержала номера строки и неправильного имени файла. В настоящее время мы можем выбрать извлечение информации из стека ошибок.
Мы нацеливаемся на первый файл в верхней части стека ошибок, потому что этот файл, как правило, является файлом, который мы фактически закодировали, и извлекаем для него файл ошибки, а также номера строк и столбцов. Получите три основных параметра, а затем проанализируйте их.
анализ исходной карты
Для работы по анализу исходной карты мы решили использоватьsource-mapЭто делается с помощью sdk, который представляет собой модуль узла, предоставляемый Mozilla.
Конечно, если вам интересно, вы можете сами реализовать анализ исходной карты, и у вас будет более глубокое понимание этой части, 23333...
const fs = require("fs");
const path = require("path");
const sourceMapTool = require("source-map");
const { sourceMapConfig } = require("../config/default");
// 检验是否为文件夹
const notStrictlyIsDir = p => !/\./.test(p);
// 检测manifest文件
const isManifest = p => /manifest\.json/.test(p);
// 从sourcemap目录中找到sourcemap文件
const findManifest = baseDir => {
const files = fs.readdirSync(baseDir);
if (files.some(f => isManifest(f))) {
return path.join(baseDir, files.filter(f => isManifest(f))[0]);
}
files.forEach(f => {
if (notStrictlyIsDir(f)) {
findManifest(path.join(baseDir, f));
}
});
};
/**
*
* @param {sourcemap 文件} sourcemapFile
* @param {行号} line
* @param {列号} col
*
* 通过 sourec-map 来解析错误源码
*/
const parseJSError = (sourcemapFile, line, col) => {
// 选择抛出一个 promise 方便我们使用 async 语法
return new Promise(resolve => {
fs.readFile(sourcemapFile, "utf8", function readContent(
err,
sourcemapcontent
) {
// SourceMapConsumer.with 是该模块提供的消费 source-map 的一种方式
sourceMapTool.SourceMapConsumer.with(sourcemapcontent, null, consumer => {
const parseData = consumer.originalPositionFor({
line: parseInt(line),
column: parseInt(col)
});
resolve(JSON.stringify(parseData));
});
});
});
};
/**
* 根据 sourcemap 文件解析错误源码
* 1. 根据传入的错误信息确定sourcemap文件
* 2. 根据错误行列信息转换错误源码
* 3. 将转换后的错误源码片段入库
*/
module.exports = (info = []) => {
const [filename, line, col] = info;
// 错误文件的 map 文件
const sourcemapFileName = `${sourceMapConfig.dir}${filename}.map`;
if (fs.existsSync(sourcemapFileName)) {
return parseJSError(sourcemapFileName, line, col);
}
return Promise.resolve(JSON.stringify({}));
};
То, что анализируется потребителем исходной карты, будет объектом js, включая источник, строку, столбец, имя и некоторую информацию, включая имя исходного файла, номер проанализированной строки и номер столбца.
Таким образом, мы получаем неправильный исходный файл с неправильным расположением.
{
source: 'http://example.com/www/js/two.js',
line: 2,
column: 10,
name: 'n'
}
Мы решили сериализовать этот объект, сохранить его непосредственно в поле базы данных, а затем отобразить во внешнем интерфейсе.
Вы можете видеть, что поле исходной карты в интерфейсе — это информация об исходном файле ошибки, которую мы наконец проанализировали.
Наконец, давайте взглянем на наш интерфейсный эффект.
На данный момент мы полностью восстановили сцену ошибки и можем с радостью вернуться к проблеме.
Congratulations ~~~~
Создайте местную производственную среду
В проекте разработки я столкнулся с проблемой, то есть при анализе source-map было обнаружено, что нужный файл карты не найден. В конце концов выяснилось, что поскольку мыdevОшибка возникает из-за схемы, но файл картыproductionВыкройка напечатана из упаковки.
Are you kidding me ?
Поэтому нам необходимо создать производственную среду для моделирования всего онлайн-процесса.
Но у меня только одна машина, что мне делать? Теперь у меня есть интерфейс, серверная часть, база данных и плагин для веб-пакетов.
Не паникуйте, контейнеризация помогает решать проблемы с микросервисами,Dockerдать...
Dokcer
До этого какое-то время использовались docker и k8s, а наши собственные продукты также контейнеризированы и автоматизированы. Это может помочь вам запустить приложение очень быстро и легко.
На фабриках первого уровня средства контейнеризации также полностью укомплектованы, и на них работает множество интернет-продуктов. С его помощью вы сможете быстро создатьUbuntu OSоперационная среда, аNginxсервер, аmysqlБаза данных, аRedisПамять. Так что, если вы еще этого не знаете, приходите и узнаете. (Я действительно не Amway...)
Здесь мы выбираем Nginx в качестве внешнего сервера, балансировки нагрузки, высокопроизводительного HTTP и обратного прокси-сервера. Я считаю, что большинство ваших собственных продуктов также работают на сервере Nginx.
1. 首先我们需要安装 [docker](https://www.docker.com/)
2. 下来拉取 nginx 镜像。
docker pull nginx
3. 创建 nginx 相关目录
mkdir -p /data/nginx/{conf, conf.d,logs}
这里我们在宿主机的 /data/nginx 目录放置 nginx 相关的文件,这个目录是可自定义的,但后续的目录映射一定要保证和这个目录相同。
4. 新建 nginx 配置文件
touch /data/nginx/conf/nginx.conf
vim /data/nginx/conf/nginx.conf
```conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
```
5. 新建 default.conf
touch /data/nginx/conf.d/default.conf
vim /data/nginx/conf.d/default.conf
```conf
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
autoindex on;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
```
到这里,有关于 nginx 配置的处理就完成了,下来我们要做的就是进行 docker 容器与宿主机的目录映射
6. 将 nginx 内容挂载到宿主机
docker run -p 80:80 -d -v /Users/xxx/Documents/lab/error-monitor/react-repo/build:/usr/share/nginx/html -v /data/nginx/logs:/var/log/nginx -v /data/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v /data/nginx/conf.d:/etc/nginx/conf.d docker.io/nginx
这里可以看到我们映射了两个目录和两个配置文件,包括了前端 html 文件目录,log 目录以及两个 nginx 配置文件。这里我直接将我们前端项目的打包目录映射到了容器中的 html 目录中,这样会比较方便一些。
这里我们选择宿主机的 80 端口映射 nginx 容器的 80 端口,我们直接打开本机的浏览器访问 localhost ,就可以看到打包完后的前端项目运行起来了。如果 80 端口有其他用途 ,可以自行切换到其他端口。
Суммировать
На данный момент наша система внешнего мониторинга в основном завершена.
Подарите себе аплодисменты~~~
Не волнуйтесь, на самом деле эта версия является только первой версией, и весь процесс изначально завершен.
Есть некоторые, которые нуждаются в доработке, будут дополнены в дальнейшем. В сценарии использования нам все еще нужно дальнейшее тестирование.
Заранее пройдено, я надеюсь, что сначала можно подвести итог всему проекту, потому что вся система более сложная, две надежды поделиться некоторым контентом могут иметь немного пользы.
На самом деле в статье размещена лишь малая часть содержания.Если вам интересно, вы можете узнать об этом подробнее.README содержит некоторые знания, идеи построения и опыт обучения.
код склада
внешний интерфейс:error-monitor-frontend
задняя часть:error-monitor-node-server
плагин исходной карты:error-monitor-webpack-plugin
Продолжать
Внимательные студенты могут обнаружить, что схема архитектуры, размещенная в заголовке статьи, немного отличается от содержания статьи, а в заголовке есть метка (top).
Поэтому, после доработки системы в будущем, должен быть (следующий) кусок, чтобы компенсировать это.Исправления приветствуются! ! ! Добро пожаловать лайк и подписка! ! ! (Не для прямого эфира) 👏👏👏👏