前端线上监控布置与使用(一)

518 阅读8分钟

回忆这一年,匆匆如白驹过隙。不经想起杨绛先生曾说:“我们曾如此渴望命运的波澜,到最后才发现:人生最曼妙的风景,竟是内心的淡定与从容。”

Hi,大家下午好,今天是周五咯,也是21年的最后一天啦。趁着年末的尾巴。给大家来一个小知识分享~

是关于什么的呢?光看标题就知道啦,这么明显。哈哈哈哈,可恶,关子没卖出来。

好啦,那就直接进入正题吧~

在说之前,首先了解下大家对 puppeteer 这玩意了解多少。我最近用了用,是真的香。然后这篇文章的主旋律也是使用这个来做的。

哦,对了,主要的语言是nodejs。接下来就正式开始啦~

首先我们先新建一个项目:

  // 新建文件夹
  mkdir onlinerun
  // 进入文件夹
  cd onlinerun
  // 初始化
  npm init -y
  // touch test.js
  

然后我们安装一些要用到的依赖:

  npm install puppeteer node-schedule request -D 

  // 这里要补充一个小点嗷,就是如果npm很慢,或者失败,可以试一下指定一下淘宝镜像(如果你不想改npm源的话
  // 具体如下:
  npm install puppeteer --registry=https://registry.npm.taobao.org
  

我个人比较建议这种方式嗷,因为在开发的过程中,一般都是公司项目和个人项目混着开发,那么设置这个源到公司,或者淘宝,都太麻烦。

嘿嘿,不过也看个人喜好啦~

然后依赖装完了,就可以进入项目开发了,除了puppeteer 之外的两个依赖是干嘛用的,后面用到了,我们再讲。

先来看下puppeteer怎么玩,如果你之前有用过这个那就可以跳过这一段嗷。

// 一个爬虫的功能
const Puppeteer = require("puppeteer");
const log = console.log;
(async () => {
  const browser = await Puppeteer.launch({
    headless: true,
  }).catch(() => browser.close);

  const page = await browser.newPage();
  
  await page.goto("https://www.baidu.com/").catch(() => browser.close);

  let urls = await page.$$eval("a", (el) =>//图片节点,API可查看官方介绍
       el.map((x) => x.href)
  );
       const obj = []
       urls.forEach((item) => {
           obj.push(item)
       })
  log(obj)
  await browser.close();
})();

  

上面这个简单的例子,大家可以试一下,就是爬虫了。如果没问题,我们就开始说本期要说的监控了嗷。

既然是监控,那么监控什么呢。还需要使用到puppeteer?

不知道大家平时有没有在线上看到过,项目的资源文件挂掉了,尤其是图片资源这样的静态文件,然后就会像这样:

image.png

并且这种,你不可控,发生了,你不去管,没有用户上报,你压根就不知道,然后人有它在线上挂。哈哈哈哈哈哈

还有另一种情况就是,现在开发的前端框架,要么是vue,要么是react。那么就会有一些console上的依赖报错啦,告警啦。当然刚刚那种资源挂掉了,console也是会暴露的。举个例子:

image.png

image.png

那么遇到告警warning,其实不那么讲究,也可以不去管了,毕竟不影响主进程,程序一样的在用的。

那么要是讲究一点,就可以把这些给解决了。强迫症表示,console干干净净的,看着心里舒坦。哈哈哈哈哈哈

但是报错,error 有一说一,绝对不能忍。因为这个说明你的程序有问题了,可能页面正常渲染了,功能看下来也没问题,但是隐藏的bug,是肯定存在的。

那么就关于这两个问题,都是那种开头说的,不去管,肯定不知道,除非等用户上报。那么怎么去管呢?线上那么多页面呢。这不是开玩笑呢吗。哎嘿,这个时候,puppeteer ,就派上用场了。

还记得我们刚开始小试牛刀的爬虫吗?如果没好好看建议再去看一眼嗷,接下来会用上。

那么回到上个问题,怎么去监控线上那么多页面,首先需要用到爬虫,先给几个核心页面接入进去。改动就是把刚刚的代码封装一下,url写到conf中去。

然后在这些核心页面中爬出来的url如果是二级页面,继续遍历。如果只是跳转链接,那就直接return。

这些操作过后你会得到一个url的集合。这些url后面会有很大的用处。

说到这边url遍历存储啊,推荐三种方法去做

  1. 落库,你可以使用mysql,或者mongo等,将页面中的url全都放到数据库中,然后在设计一个接口返回出来;

  2. 写文件,就是将url写到本地项目的某个文件,在后面去做监控的时候,直接读文件,code如下:

       const fs = require("fs");
       fs.appendFile(imgUrl + 'urls.json', JSON.stringify(obj), 'utf8', function(err, ret) {
        if (err) {
           throw err
        }
           console.log('success')
        })
    
  3. 就是每次都爬一次最新的,爬完就去监控。

好了,这三种方法,我最推荐的是第一种方法,其实说到做法,可以分为两步,我的方案啊,大家看情况参考即可:暴露一个接口,这个接口干嘛用呢?

这个接口,就是给监控调用,来获取url去进行循坏便利监控。然后,在请求这个接口的时候,去获取的数据是数据库里已有的数据,但是同时,会触发爬虫的方法。

为什要出发爬虫的方法呢?因为你本地调用的数据是上次爬虫到的,那么这次你请求了,就要把数据库里的数据跟新一下。给下次用。

那么有同学会说,那你这个没有时效性啊。并且,数据的读写会影响吗?

首先啊,时效性其实不用担心,因为你部署上服务了,那么肯定就是一直在跑的。数据也不存在很久的空窗期。然后读写一致的问题,你每次读完接口,吐完数据,才会去更新你数据库的数据,这个时候,你都拿到已有的数据了,库里该更新更新。也不必担心啦。

好了,关于这块,我就说完啦,如果你有更好的方法,可以交流嗷。接下来我么说关键的。直接看代码:

/*
 * IMT project
 */
const puppeteer = require("puppeteer");
var sendrequest = require("request");
const schedule = require("node-schedule");
let nowurls = [];

const log = console.log;

function delay(time) {
  return new Promise(function (resolve) {
    setTimeout(resolve, time);
  });
}

// get page dom
const asynconlineasserts = async (newurl) => {
  try {
    const cookiesarr = [
      {
        name: "uid",
        value: "xxxxxxxxxxx",
        domain: ".xxxxxxx.com",
        path: "/",
        expires: Date.now() + 3600 * 1000,
      },
      ...
    ];
    puppeteer
      .launch({
        headless: true, // 开启界面,
        timeout: 60 * 1000,
        // devtools: true,  // 开启开发者控制台
        //设置每个步骤放慢200毫秒
        slowMo: 200,
        ignoreHTTPSErrors: true,
        //设置打开页面在浏览器中的宽高
        defaultViewport: null,
        args: ["--start-maximized", "--no-sandbox"],
        ignoreDefaultArgs: ["--enable-automation"],
        // executablePath: "/usr/bin/google-chrome",
      })
      .then(async (browser) => {
        try {
          const page = await browser.newPage();
          await page.setDefaultNavigationTimeout(0);
          const headlessUserAgent = await page.evaluate(
            () => navigator.userAgent
          );
          const chromeUserAgent = headlessUserAgent.replace(
            "HeadlessChrome",
            "Chrome"
          );
          await page.setUserAgent(chromeUserAgent);
          await page.setExtraHTTPHeaders({
            "accept-language": "zh-CN,zh;q=0.9",
          });
          const openoptions = {
            timeout: 0,
            waitUntil: [
              "load", //等待 “load” 事件触发
              "domcontentloaded", //等待 “domcontentloaded” 事件触发
              "networkidle0", //在 500ms 内没有任何网络连接
              "networkidle2", //在 500ms 内网络连接个数不超过 2 个
            ],
          };
          // 设置cookie
          await page.setCookie(...cookiesarr);
          await delay(5000);
          await page.reload();
          await delay(5000);
          page.on("response", (response) => {
            const status = response.status().toString();
            if (status.startsWith("4")) {
              // log(response.url());
              // log(response.status());
              var differror = {
                method: "POST",
                url: "https://xxxxxxxxxxxxxx",
                headers: {
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({
                  msgtype: "text",
                  text: {
                    content:
                      newurl +
                      ` 该URL在请求过程中存在部分资源异常,详情如下: ` +
                      response.url() +
                      `  状态:  ` +
                      response.status(),
                    mentioned_list: [""],
                    mentioned_mobile_list: [""],
                  },
                }),
              };
              sendrequest(differror, function (error, response) {
                if (error) throw new Error(error);
                log(response.body);
              });
            }
          });
          page.on("console", (msg) => {
            // log("msg._type: ", msg._type);
            if (msg._type === "error" || msg._type === "warning") {
              // log("errormsg: ", JSON.stringify(msg));
              var differror = {
                method: "POST",
                url: "https://xxxxxxxxxxx",
                headers: {
                  "Content-Type": "application/json",
                },
                body: JSON.stringify({
                  msgtype: "text",
                  text: {
                    content:
                      newurl +
                      ` 该URL请求过程中console存在部分报错具体如下: ` +
                      JSON.stringify(msg),
                    mentioned_list: [""],
                    mentioned_mobile_list: [""],
                  },
                }),
              };
              sendrequest(differror, function (error, response) {
                if (error) throw new Error(error);
                log(response.body);
              });
            }
          });
          await page.goto(newurl, openoptions).catch(() => browser.close);
          await browser.close();
          try {
          } catch (e) {
            log(e);
          }
        } catch (e) {
          log(e);
        }
        await delay(3000);
        await browser.close();
      });
  } catch (e) {
    log(e + "//////////");
  }
};

function geturls() {
  var options = {
    method: "GET",
    url: "http://10.xxx.xxx.xxx:2727/getonlineurls",
    headers: {},
  };
  sendrequest(options, function (error, response) {
    if (error) throw new Error(error);
    const obj = JSON.parse(response.body).data;
    nowurls = obj;
  });
}

let startnum = 0;

let rule = new schedule.RecurrenceRule();
let times = [];
for (let i = 1; i < 60; i++) {
  times.push(i);
}
rule.minute = times;

// 启动任务
let job = schedule.scheduleJob(rule, async () => {
  // foreach
  try {
    log("startnum: ", startnum);
    let oldlen = nowurls.length;
    log("oldlen: " + oldlen);
    if (oldlen > 0 && startnum < oldlen) {
      if (nowurls[startnum].includes("login")) {
        startnum++;
      } else {
        await asynconlineasserts(nowurls[startnum]);
        startnum++;
      }
    } else {
      startnum = 0;
      geturls();
    }
  } catch (e) {
    log(e + "---------");
  }
});

好啦,这里也要解释下,刚刚除了puppeteer意外的依赖是干嘛用的了,一个是发送请求的,request,这个主要是我使用的刚刚爬虫数据存储的第一种方法,所以暴露了一个接口出来。

另一个就是,就是用作内部的告警使用,来发送报警请求,企业微信。哈哈哈哈哈。

另一个依赖呢,就是任务,这个是干嘛的呢,其实就是对node服务的一个任务命令,你可以根据你的需求来制定一个定时任务,抽离出来看一下:

const schedule = require("node-schedule");
let rule = new schedule.RecurrenceRule();
const log = console.log;
let times = [];
for (let i = 1; i < 60; i++) {
  times.push(i);
}
rule.minute = times;

// 启动任务
let job = schedule.scheduleJob(rule, async () => {
  // foreach
  try {
    log('新年快乐')
  } catch (e) {
    log(e + "---------");
  }
});

大概意思,就是每一分钟打印一次 ‘’新年快乐‘’。

好了,回到主旋律,刚刚贴出来的代码的核心其实就是两块:

    page.on("response", (response) => {
        const status = response.status().toString();
        if (status.startsWith("4")) {
          log(response.url());
          log(response.status());
        }
      });
      

这个主要是监控每个请求下去的全量response的返回,可以根据自己的需求,监控response里面的内容,然后去做逻辑。另一个呢就是:

    page.on("console", (msg) => {
        // log("msg._type: ", msg._type);
        if (msg._type === "error" || msg._type === "warning") {
          log("errormsg: ", JSON.stringify(msg));
        }
      });
      

监控每个页面请求完成了,console下面的情况,监控报错啊,告警啊,这些。这些相关也是根据自己的业务场景需求去做相关的逻辑设计啦。

关于这块的文档,给大家贴出来 使用手册

然后关于最后的告警方式,其实根据自己的需求来做,你可以选择报告,可以选择邮件,可以选择告警机器人。

个人觉得告警或者邮件,比较合适,本身就是监控线上相关的,那么出问题第一时间告警,及时解决呀~

关于puppeteer,说一个小点,就是关于 executablePath 这个参数。这个主要是干嘛呢,就是去指定你的服务上的chrome路径的,如果不指定也没关系,他会用自带的chromnium

但是我不喜欢用自带的,哈哈哈哈。这一切搞好之后,就可以往你的服务上部署啦。

最后执行;

    node test.js
    

关于node启动,大家有时间可以去看下 forever 这个工具。

最后的效果:

image.png

好啦,关于这一期的内容就说完啦,谢谢你能看到这里嗷~

今天是21年的最后一天啦,提前祝大家,跨年开心,新年开心。

嘿嘿~ 拜拜👋~