先上大王博客,说的很有道理,我都快信了,所以一直想把这个项目写出来,怎奈大王一直不讲,唉,实在没办法只好自己写了。理念就是模仿大王的,实际操作流程有些不一样:
- 还是从前端js获取一些相关的前端性能指标
- 讲这些性能指标统一发送给我提供的接口
- 这个接口会对发送过来的数据进行一些处理
- 将处理的数据扔进kafka队列
- 从kafka队列里取出数据存进influxdb
- 从influxdb取出数据进行展示
我的流程就是这样,其实都是很简单易懂的,但是这里面有一些小处理,大概有:
- js我是百度了大佬的,他的js可以取出前端性能相关的指标,可是取出来的加载时间他喵的都是负值…..实在是心情复杂,还好最后改好了,这是最坑我的地方,因为我前端不好
- 本来是想直接把数据存进influxdb,但是仔细想想还是应当先放入kafka,然后由需要的地方自己去取就是了,当然我现在只有向influxdb存,之后可以继续加,这样比较好拓展
- 基于第二点,我需要一个接收数据并扔进kafka的api(这个很随意)以及一个(或多个)从kafka里取数据并存储到对应后端的进程,这个进程是一直监听kafka队列运行的
因为是放在业务前端里获取的数据,那么数据量肯定随着业务峰谷变化。再一个我原本想集成在我的django里,但是查了半天资料也没找到如何让django运行期间一直保持另外几个进程一直运行(本来是想用threading)。最后想想算了,直接用go写不就行了,性能好,开几个goroutine问题全都解决了,我只需要把前端展示集成下不就好了嘛。语言选好了,逻辑流程清晰了,那就开搞
从前端开始,我直接把改好的js贴进来,大家复制就是了,唯一需要更改的是第179行改成你的api地址(也就是go提供的接口)
1 (function(window) { 2 'use strict'; 3 4 /** 5 * https://developer.mozilla.org/zh-CN/docs/Web/API/Window/performance 6 */ 7 var performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance || {}; 8 performance.now = (function() { 9 return performance.now || 10 performance.webkitNow || 11 performance.msNow || 12 performance.oNow || 13 performance.mozNow || 14 function() { return new Date().getTime(); }; 15 })(); 16 17 /** 18 * 默认属性 19 */ 20 var defaults = { 21 performance: performance, // performance对象 22 ajaxs: [], //ajax监控 23 //可自定义的参数 24 param: { 25 // rate: 0.5, //随机采样率 26 // src: 'http://127.0.0.1:8000/thief/a', //请求发送数据 27 // download: {img:'http://h5dev.eclicks.cn/libs/common/img/bandwidth-5.png', size:4511798}//网速设置 28 } 29 }; 30 31 if(window.primus.param) { 32 for(var key in window.primus.param) { 33 defaults.param[key] = window.primus.param[key]; 34 } 35 } 36 var primus = defaults; 37 var firstScreenHeight = window.innerHeight;//第一屏高度 38 var doc = window.document; 39 40 /** 41 * 异常监控 42 * https://github.com/BetterJS/badjs-report 43 * @param {String} msg 错误信息 44 * @param {String} url 出错文件的URL 45 * @param {Long} line 出错代码的行号 46 * @param {Long} col 出错代码的列号 47 * @param {Object} error 错误信息Object https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error 48 */ 49 window.onerror = function(msg, url, line, col, error) { 50 var newMsg = msg; 51 if (error && error.stack) { 52 var stack = error.stack.replace(/\n/gi, "").split(/\bat\b/).slice(0, 9).join("@").replace(/\?[^:]+/gi, ""); 53 var msg = error.toString(); 54 if (stack.indexOf(msg) < 0) { 55 stack = msg + "@" + stack; 56 } 57 newMsg = stack; 58 } 59 // if (Object.prototype.toString.call(newMsg) === "[object Event]") { 60 // newMsg += newMsg.type ? ("--" + newMsg.type + "--" + (newMsg.target ? (newMsg.target.tagName + "::" + newMsg.target.src) : "")) : ""; 61 // } 62 63 var obj = {msg:newMsg, target:url, rowNum:line, colNum:col}; 64 alert(obj.msg); 65 }; 66 67 /** 68 * ajax监控 69 * https://github.com/HubSpot/pace 70 */ 71 var _XMLHttpRequest = window.XMLHttpRequest;// 保存原生的XMLHttpRequest 72 // 覆盖XMLHttpRequest 73 window.XMLHttpRequest = function(flags) { 74 var req; 75 // 调用原生的XMLHttpRequest 76 req = new _XMLHttpRequest(flags); 77 // 埋入我们的“间谍” 78 monitorXHR(req); 79 return req; 80 }; 81 var monitorXHR = function(req) { 82 req.ajax = {}; 83 //var _change = req.onreadystatechange; 84 req.addEventListener('readystatechange', function() { 85 if(this.readyState == 4) { 86 req.ajax.end = primus.now();//埋点 87 88 if ((req.status >= 200 && req.status < 300) || req.status == 304 ) { //请求成功 89 req.ajax.endBytes = _kb(req.responseText.length * 2);//KB 90 //console.log('响应数据:'+ req.ajax.endBytes);//响应数据大小 91 }else { //请求失败 92 req.ajax.endBytes = 0; 93 } 94 req.ajax.interval = req.ajax.end - req.ajax.start; 95 primus.ajaxs.push(req.ajax); 96 //console.log('ajax响应时间:'+req.ajax.interval); 97 } 98 }, false); 99 100 // “间谍”又对open方法埋入了间谍101 var _open = req.open;102 req.open = function(type, url, async) {103 req.ajax.type = type;//埋点104 req.ajax.url = url;//埋点105 return _open.apply(req, arguments);106 };107 108 var _send = req.send;109 req.send = function(data) {110 req.ajax.start = primus.now();//埋点111 var bytes = 0;//发送数据大小112 if(data) {113 req.ajax.startBytes = _kb(JSON.stringify(data).length * 2 );114 }115 return _send.apply(req, arguments);116 };117 };118 119 /**120 * 计算KB值121 * http://stackoverflow.com/questions/1248302/javascript-object-size122 */123 function _kb(bytes) {124 return (bytes / 1024).toFixed(2);//四舍五入2位小数125 }126 127 /**128 * 给所有在首屏的图片绑定load事件,计算载入时间129 * TODO 忽略了异步加载130 * CSS背景图 是显示的在param参数中设置backgroundImages图片路径数组加载131 */132 var imgLoadTime = 0;133 function _setCurrent() {134 var current = Date.now();135 current > imgLoadTime && (imgLoadTime = current);136 }137 doc.addEventListener('DOMContentLoaded', function() {138 var imgs = doc.querySelectorAll('img');139 imgs = [].slice.call(doc.querySelectorAll('img'));140 if(imgs) {141 imgs.forEach(function(img) {142 if(img.getBoundingClientRect().top > firstScreenHeight) {143 return;144 }145 // var image = new Image();146 // image.src = img.getAttribute('src');147 if(img.complete) {148 _setCurrent();149 }150 //绑定载入时间151 img.addEventListener('load', function() {152 _setCurrent();153 }, false);154 });155 }156 157 //在CSS中设置了BackgroundImage背景158 if(primus.param.backgroundImages) {159 primus.param.backgroundImages.forEach(function(url) {160 var image = new Image();161 image.src = url;162 if(image.complete) {163 _setCurrent();164 }165 image.onload = function() {166 _setCurrent();167 };168 });169 }170 }, false);171 172 window.addEventListener('load', function() {173 //测试网速174 //_measureConnectionSpeed();175 setTimeout(function() {176 var time = primus.getTimes();177 178 $.ajax({179 url: 'http://192.168.56.1:8080/',180 type: 'POST',181 dataType: 'json',182 data: time,183 success: function (data) {184 185 }186 });187 188 //通过网页大小测试网速189 // var duration = time.domReadyTime / 1000;190 // var pageSize = doc.documentElement.innerHTML.length * 2 * 8;191 // var speedBps = pageSize / duration;192 // console.log(speedBps/(1024*1024));193 194 var data = {ajaxs:primus.ajaxs, dpi:primus.dpi(), time:time};195 primus.send(data);196 }, 500);197 });198 199 /**200 * 打印特性 key:value格式201 */202 primus.print = function(obj, left, right, filter) {203 var list = [], left = left || '', right = right || '';204 for(var key in obj) {205 if(filter) {206 if(filter(obj[key]))207 list.push(left + key + ':' + obj[key] + right);208 }else {209 list.push(left + key + ':' + obj[key] + right);210 }211 }212 return list;213 };214 215 /**216 * 请求时间统计217 * 需在window.onload中调用218 * https://github.com/addyosmani/timing.js219 */220 primus.getTimes = function() {221 var timing = performance.timing;222 if (timing === undefined) {223 return false;224 }225 var api = {};226 //存在timing对象227 if (timing) {228 // All times are relative times to the start time within the229 // 白屏时间,也就是开始解析DOM耗时230 var firstPaint = 0;231 232 // Chrome233 if (window.chrome && window.chrome.loadTimes) {234 // Convert to ms235 firstPaint = window.chrome.loadTimes().firstPaintTime * 1000;236 api.firstPaintTime = firstPaint;237 }238 // IE239 else if (typeof timing.msFirstPaint === 'number') {240 firstPaint = timing.msFirstPaint;241 api.firstPaintTime = firstPaint;242 }243 else {244 api.firstPaintTime = timing.navigationStart;245 }246 // Firefox247 // This will use the first times after MozAfterPaint fires248 //else if (window.performance.timing.navigationStart && typeof InstallTrigger !== 'undefined') { 249 // api.firstPaint = window.performance.timing.navigationStart;250 // api.firstPaintTime = mozFirstPaintTime - window.performance.timing.navigationStart;251 //}252 253 /**254 * http://javascript.ruanyifeng.com/bom/performance.html255 * 加载总时间256 * 这几乎代表了用户等待页面可用的时间257 * loadEventEnd(加载结束)-navigationStart(导航开始)258 */259 api.loadTime = timing.loadEventEnd - timing.navigationStart;260 261 /**262 * Unload事件耗时263 */264 api.unloadEventTime = timing.unloadEventEnd - timing.unloadEventStart;265 266 /**267 * 执行 onload 回调函数的时间268 * 是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?269 */270 api.loadEventTime = timing.loadEventEnd - timing.loadEventStart;271 272 /**273 * 用户可操作时间274 */275 api.domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;276 277 /**278 * 首屏时间279 * 用户在没有滚动时候看到的内容渲染完成并且可以交互的时间280 * 记录载入时间最长的图片281 */282 if(imgLoadTime == 0) {283 api.firstScreen = api.domReadyTime;284 }else {285 api.firstScreen = imgLoadTime - timing.navigationStart;286 }287 288 /**289 * 解析 DOM 树结构的时间290 * 期间要加载内嵌资源291 * 反省下你的 DOM 树嵌套是不是太多了292 */293 api.parseDomTime = timing.domComplete - timing.domInteractive;294 295 /**296 * 请求完毕至DOM加载耗时297 */298 api.initDomTreeTime = timing.domInteractive - timing.responseEnd;299 300 /**301 * 准备新页面时间耗时302 */303 api.readyStart = timing.fetchStart - timing.navigationStart;304 305 /**306 * 重定向的时间307 * 拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com308 */309 api.redirectTime = timing.redirectEnd - timing.redirectStart;310 311 /**312 * DNS缓存耗时313 */314 api.appcacheTime = timing.domainLookupStart - timing.fetchStart;315 316 /**317 * DNS查询耗时318 * DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?319 * 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)320 */321 api.lookupDomainTime = timing.domainLookupEnd - timing.domainLookupStart;322 323 /**324 * TCP连接耗时325 */326 api.connectTime = timing.connectEnd - timing.connectStart;327 328 /**329 * 内容加载完成的时间330 * 页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?331 */332 api.requestTime = timing.responseEnd - timing.requestStart;333 334 /**335 * 请求文档336 * 开始请求文档到开始接收文档337 */338 api.requestDocumentTime = timing.responseStart - timing.requestStart;339 340 /**341 * 接收文档342 * 开始接收文档到文档接收完成343 */344 api.responseDocumentTime = timing.responseEnd - timing.responseStart;345 346 /**347 * 读取页面第一个字节的时间348 * 这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?349 * TTFB 即 Time To First Byte 的意思350 * 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte351 */352 api.TTFB = timing.responseStart - timing.navigationStart;353 }354 return api;355 };356 357 /**358 * 与performance中的不同,仅仅是做时间间隔记录359 * https://github.com/nicjansma/usertiming.js360 */361 var marks = {};362 primus.mark = function(markName) {363 var now = performance.now();364 marks[markName] = {365 startTime: Date.now(),366 start: now,367 duration: 0368 };369 };370 371 /**372 * 计算两个时间段之间的时间间隔373 */374 primus.measure = function(startName, endName) {375 var start = 0, end = 0;376 if(startName in marks) {377 start = marks[startName].start;378 }379 if(endName in marks) {380 end = marks[endName].start;381 }382 return {383 startTime: Date.now(),384 start: start,385 end: end,386 duration: (end - start)387 };388 };389 390 /**391 * 资源请求列表392 * Safrai以及很多移动浏览器不支持393 * https://github.com/nurun/performance-bookmarklet394 * http://nicj.net/resourcetiming-in-practice/395 */396 primus.getEntries = function() {397 if (performance.getEntries === undefined) {398 return false;399 }400 401 var entries = performance.getEntriesByType('resource');402 var statis = [];403 entries.forEach(function(t, index) {404 var isRequest = t.name.indexOf("http") === 0;console.log(t.name)405 // if (isRequest) { 406 // urlFragments = t.name.match(/:\/\/(.[^/]+)([^?]*)\??(.*)/);407 // 408 // maybeFileName = t.name.split("/").pop();409 // fileExtension = maybeFileName.substr((Math.max(0, maybeFileName.lastIndexOf(".")) || Infinity) + 1);410 // } else { 411 // urlFragments = ["", window.location.host];412 // fileExtension = t.name.split(":")[0];413 // }414 var cur = {415 name: t.name,416 fileName: t.name.split("/").pop(),417 //initiatorType: t.initiatorType || fileExtension || "SourceMap or Not Defined",418 duration: t.duration419 //isRequestToHost: urlFragments[1] === location.host420 };421 422 if (t.requestStart) {423 cur.requestStartDelay = t.requestStart - t.startTime;424 // DNS 查询时间425 cur.lookupDomainTime = t.domainLookupEnd - t.domainLookupStart;426 // TCP 建立连接完成握手的时间427 cur.connectTime = t.connectEnd - t.connectStart;428 // TTFB429 cur.TTFB = t.responseStart - t.startTime;430 // 内容加载完成的时间431 cur.requestTime = t.responseEnd - t.requestStart;432 // 请求区间433 cur.requestDuration = t.responseStart - t.requestStart;434 // 重定向的时间435 cur.redirectTime = t.redirectEnd - t.redirectStart;436 }437 438 if (t.secureConnectionStart) {439 cur.ssl = t.connectEnd - t.secureConnectionStart;440 }441 442 statis.push(cur);443 });444 return statis;445 };446 447 /**448 * 标记时间449 * Date.now() 会受系统程序执行阻塞的影响不同450 * performance.now() 的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)451 */452 primus.now = function() {453 return performance.now();454 };455 456 /**457 * 网络状态458 * https://github.com/daniellmb/downlinkMax459 * http://stackoverflow.com/questions/5529718/how-to-detect-internet-speed-in-javascript460 */461 primus.network = function() {462 //2.2--4.3安卓机才可使用463 var connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;464 var types = "Unknown Ethernet WIFI 2G 3G 4G".split(" ");465 var network = {bandwidth:null, type:null}466 if(connection && connection.type) {467 network.type = types[connection.type];468 }469 470 return network;471 };472 473 /**474 * 测试网速475 */476 function _measureConnectionSpeed() {477 var startTime, endTime;478 var download = new Image();479 download.onload = function () {480 endTime = primus.now();481 var duration = (endTime - startTime) / 1000;482 var bitsLoaded = downloadSize * 8;483 var speedBps = (bitsLoaded / duration).toFixed(2);484 var speedKbps = (speedBps / 1024).toFixed(2);485 var speedMbps = (speedKbps / 1024).toFixed(2);486 console.log(speedMbps);487 }488 startTime = primus.now();489 var cacheBuster = "?rand=" + startTime;490 download.src = imageAddr + cacheBuster;491 }492 493 /**494 * 代理信息495 */496 primus.ua = function() {497 return USERAGENT.analyze(navigator.userAgent);498 // var parser = new UAParser();499 // return parser.getResult();500 };501 502 /**503 * 分辨率504 */505 primus.dpi = function() {506 return {width:window.screen.width, height:window.screen.height};507 };508 509 /**510 * 组装变量511 * https://github.com/appsignal/appsignal-frontend-monitoring512 */513 function _paramify(obj) {514 return 'data=' + JSON.stringify(obj);515 }516 517 /**518 * 推送统计信息519 */520 primus.send = function(data) {521 var ts = new Date().getTime().toString();522 //采集率523 if(primus.param.rate > Math.random(0, 1)) {524 var img = new Image(0, 0);525 img.src = primus.param.src +"?" + _paramify(data) + "&ts=" + ts;526 }527 };528 529 var currentTime = Date.now(); //这个脚本执行完后的时间 计算白屏时间530 window.primus = primus;531 })(this);
然后是你要检测的前端页面,直接把该js引用就ok了
Title Index
这样,每当有人访问该页面时,就会取出他此次访问页面相关质量然后post到我们的api上
然后是api进行处理,代码在git上:https://github.com/bfmq/Hermes
首先是前端质量的基本字段,然后再加上这个页面的url(因为你肯定不止检测一个页面嘛,之后前端展示的时候根据url进行不同的查询语句就可以了),最后根据访问ip调阿里的api获取了这个ip所属的地区,这样就可以知道地区进行一些判断了。在程序开始运行的时候就会打开一个一直从kafka队列取数据并录入influxdb的goroutine,你可以再加一些自己的插件进去,录入到大数据里一类的。当然conf下的配置你得改成你自己的服务器的。
后台内部处理完成了,就要在前端展示了,这个还是使用了python继承在django里了,前端出图用的还是echarts,数据就是从influxdb里取得(这里肯定又是python的api),数据还是那个数据,具体想用什么图展示就看你开心了,我就简单的直接全部展示了下,设置的是每一分钟会自动ajax再去取后刷新下xy轴(还是echarts里的功能)
当然了,现在展示的数据只是我测试页面的测试数据,所以都是几ms级别的,检测的页面几乎没写内容嘛毕竟,但是经过使用,还是ok的
最后再附上各名词对应关系跟我python获取数据的代码
"firstPaint":"白屏时间""loadTime":"加载总时间""unloadEventTime":"Unload事件耗时""loadEventTime":"onload"回调函数时间""domReadyTime":"用户可操作时间""firstScreen":"首屏时间""parseDomTime":"DOM树结构解析时间""initDomTreeTime":"请求完毕至DOM加载耗时""readyStart":"准备新页面时间耗时""redirectTime":"重定向的时间""appcacheTime":"DNS缓存耗时""lookupDomainTime":"DNS查询耗时""connectTime":"TCP连接耗时""requestTime":"内容加载完成的时间""requestDocumentTime":"请求文档时间""responseDocumentTime":"接收文档时间""TTFB":"读取页面第一个字节的时间"
def get_influxdb_data(url, city): """ 从hermes库里获取数据 :param url: 索引,你要查看的url :param city: 表名,你要查看的城市 :return: """ data = {} query = """select TTFB,appcacheTime,connectTime,domReadyTime,firstScreen,initDomTreeTime,loadEventTime, loadTime,lookupDomainTime,parseDomTime,readyStart,redirectTime,requestDocumentTime,requestTime,responseDocumentTime, unloadEventTime from "{0}" where url = '{1}' and time > now() - 1h;""".format(city, url) influxdb_obj = InfluxDBCFactory('hermes') query_ret = influxdb_obj.query(query) all_data = query_ret.raw['series'][0] all_data_columns = all_data['columns'] all_data_values = all_data['values'] for key in all_data_columns: key_index = all_data_columns.index(key) if key != 'time': key_list = [x[key_index] for x in all_data_values] else: key_list = [utc2local(x[key_index], local_format='%H:%M:%S') for x in all_data_values] data[FrontendData[key]] = key_list return data
#!/usr/bin/env python# -*- coding:utf8 -*-# __author__ = '北方姆Q'from influxdb import InfluxDBClientfrom plugins.duia.singleton import Singletonfrom django.conf import settingsclass InfluxDBCFactory(InfluxDBClient, Singleton): def __init__(self, database, host=settings.INFLUXDB_SERVER, port=settings.INFLUXDB_PORT): super().__init__(host=host, port=port, database=database)
#!/usr/bin/env pythonimport timeimport datetime# 格式自改UTC_FORMAT = '%Y-%m-%dT%H:%M:%SZ'LOCAL_FORMAT = '%Y-%m-%d %H:%M:%S'def utc2local(utc_str, utc_format=UTC_FORMAT, local_format=LOCAL_FORMAT): utc_st = datetime.datetime.strptime(utc_str, utc_format) local_time = datetime.datetime.fromtimestamp(time.time()) utc_time = datetime.datetime.utcfromtimestamp(time.time()) time_difference = local_time - utc_time local_st = utc_st + time_difference return local_st.strftime(local_format)