游戏日志系统设计与实现
发表于2017-12-12
作用
游戏临近上线,需要做一个日志系统,记录玩家的行为,用途如下:
- 监控玩家状态变化,如账号登记,角色创建,上线下线,充值等;
- 分析玩家行为,如金币钻石消耗在什么系统上了,主要参与了哪些活动和玩法;
- 帮助分析bug,记录玩家的行为和数据变化,可以回溯bug产生的过程;
- 方便客服,查询和处理玩家的反馈。
结构设计
首先,用一台公共的服务器左右日志的db服务器,所有游戏中产生的日志,都往这个db中写;
然后,查询系统需要一个后端,与前端交互,来处理查询逻辑,反馈数据;
最后,需要一个前端,提交查询条件,展示查询结果。
实现
日志数据库
在网易无论手游还是端游,基本上都是用mongo,出来之后游戏数据库也就用了mongo,在我看来主要基于两个优点:
- 游戏需求多变,mongo直接写json,省去需要建表改表的麻烦;
- 对于游戏数据库,无须完成逻辑,只要存数据就好,用不上复杂的SQL语句;
查询系统后端
系统后端直接用了nodejs,主要是基于经验和前段吧,因为之前用nodejs和python写过http服务器,可选的就是这两个了,加上前段用的网页,前后端统一就都用js了。
// dao.js var MongoClient = require('mongodb').MongoClient; var StrUtils = require("./StrUtils"); var TIMEOUT = 3000;// 毫秒 function getConnStr(host, port, rpSetName, dbname) { return StrUtils.format("mongodb://{0}:{1}/{2}?connectTimeoutMS={3}&replicaSet={4}", host, port, dbname, TIMEOUT, rpSetName); }; function findDocuments(conditions, db, col, startIndex, rows, callback) { db.slaveOk = true; var collection = db.collection(col); var cursor = collection.find(conditions).sort({tm: -1}).skip(startIndex).limit(rows); cursor.toArray(function(err, docs) { if (err) { console.error(err); callback([], 0); return; } cursor.count(false, function(err, count) { if (err) { console.error(err); callback([], 0); return; } callback(docs, count); }); }); } function findRecord(host, port, rpSetName, dbname, colname, conditions, startIndex, rows, callback) { var conn = getConnStr(host, port, rpSetName, dbname, colname); MongoClient.connect(conn, function(err, db) { if (err) { console.error(err); callback([], 0); } else { findDocuments(conditions, db, colname, startIndex, rows, function(docs, cnt) { callback(docs, cnt); db.close(); }); } }); } exports.findRecord = findRecord;
前端
考虑到这个工具会时常更新,多方会用到,用客户端的话,用网页比较合适,更新之后刷新就可以了。
这里有点技巧就是列名需要动态的获取,否则就要在客户端写很多Grid模板了,主要代码如下:
// log.js function createGrid(colNames, colModel, url) { var reader = { root: "rows",// 包含实际数据的数组 page: "page",// 当前页 total: "total",// 总页数 records: "records",// 查询出的记录数 repeatitems: true,// 每行的数据是可以重复的 cell: "cell",// 当前行所有单元格的数据数组 id: "id",// 行id userdata: "userdata"// 额外参数 }; var options = { // 请求 url: url, autoencode: true, datatype: "json", mtype: "GET", // 表格显示 caption: "查询结果", colNames: colNames, colModel: colModel, // 页数 rowNum: DEFAULT_ROW, rowList: [30, 50], pager: '#pager', page: 1, // 排序 sortable: false, sortname: 'accout', sortorder: "desc", // 尺寸 height: 'auto', width: 'auto', shrinkToFit: true, autowidth: true, // 附加功能 viewrecords: true, rownumbers: true, multiselect: false, cellEdit: false, hidegrid: false, // 数据解析 jsonReader: reader, loadComplete: function (jsonData) { if (jsonData.error) { alert(jsonData.error); return; } } }; var grid = $("#grid"); grid.jqGrid(options); } function getColModel(colNames, colWidth) { var colModel = []; for (var i = 0; i < colNames.length; i++) { var name = colNames[i]; var width = colWidth[i] || 10; colModel.push({name: name, sortable: false, width: width}); } return colModel; } function getUrlArgs(getCol, isExport) { var acc = $("input#account").val(); var tp = $("#sel_op").val(); var args = { usr: getCookie(COOKIE_KEY), getCol: getCol, acc: acc, tp: tp }; if (getCol) { return args; } else { args.sid = $("#sel_server").val(); args.channel = $("#sel_channel").val(); args.pkg = $("#sel_pkg").val(); args.fdate = $("#date_picker_from").datepicker('getDate').getTime(); args.tdate = $("#date_picker_to").datepicker('getDate').getTime(); args.name = $("input#name").val(); args.id = $("input#id").val(); if (isExport) { args.page = 1; args.rows = 0; } return args; } } function onClickQuery() { $.jgrid.gridUnload("#grid"); var url = getURL(HOST, PORT, "/roleinfo", getUrlArgs(true, false)); $.get(url, function(jsonData){ if (jsonData.error) { alert(jsonData.error); return; } var colNames = jsonData.colNames; var colWidth = jsonData.colWidth; if (colNames && colNames.length) { var colModel = getColModel(colNames, colWidth); var url = getURL(HOST, PORT, "/roleinfo", getUrlArgs(false, false)); createGrid(colNames, colModel, url); } else { alert("未知操作类型"); } }, "json"); }
另外,前端还有个导出csv的小功能,代码如下:
// export // 参考:http://jsfiddle.net/pxfunc/aa2t3ntt/1/ function JSONToCSVConvertor(arrData, title) { var CSV = ''; // 表头 CSV += title + '\r\n\n'; // 列名 var thList = []; var colNames = ""; for (var colName in arrData[0]) { colNames += colName + ','; thList.push(colName); } colNames = colNames.slice(0, -1); CSV += colNames + '\r\n'; // 数据 for (var i = 0; i < arrData.length; i++) { var data = arrData[i]; var line = ""; for (var j = 0; j < thList.length; j++) { var key = thList[j]; line += '"' + data[key] + '",'; } line.slice(0, line.length - 1); CSV += line + '\r\n'; } if (CSV === '') { alert("Invalid data"); return; } // 创建一个标签并自动点击下载,然后删除 var fileName = title.replace(/ /g,"_"); var uri = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(CSV); var link = document.createElement("a"); link.href = uri; link.style = "visibility:hidden"; link.download = fileName + ".csv"; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
webserver
同样也用nodejs简单地实现了一个,省去配置Apache或Nginx的麻烦,代码如下:
// webserver.js var http = require('http'); var url = require('url'); var fs = require('fs'); var path = require('path'); var PORT = 9950; var mime = { "css": "text/css", "gif": "image/gif", "html": "text/html", "ico": "image/x-icon", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "text/javascript", "json": "application/json", "pdf": "application/pdf", "png": "image/png", "svg": "image/svg+xml", "swf": "application/x-shockwave-flash", "tiff": "image/tiff", "txt": "text/plain", "wav": "audio/x-wav", "wma": "audio/x-ms-wma", "wmv": "video/x-ms-wmv", "xml": "text/xml", "woff": "application/x-woff", "woff2": "application/x-woff2", "tff": "application/x-font-truetype", "otf": "application/x-font-opentype", "eot": "application/vnd.ms-fontobject" }; var server = http.createServer(function(request, response) { var pathname = url.parse(request.url).pathname || "/index.html"; var realPath = path.join(".", pathname); var ext = path.extname(realPath); if (!ext) { pathname = "/index.html"; realPath = path.join(".", pathname); ext = path.extname(realPath); } ext = ext ? ext.slice(1) : 'unknown'; fs.exists(realPath, function(exists) { if (exists) { fs.readFile(realPath, "binary", function(err, file) { if (err) { response.writeHead(500, {'Content-Type': 'text/plain'}); response.end(err); } else { var contentType = mime[ext] || "text/plain"; response.writeHead(200, {'Content-Type': contentType}); response.write(file, "binary"); response.end(); } }); } else { response.writeHead(404, {'Content-Type': 'text/plain'}); response.write("This request URL " + pathname + " was not found on this server."); response.end(); } }); }); server.listen(PORT);
最后上截图:
注意:
- js跨域问题,处理起来要小心
- mongo分页查询skip,当数据过多时,会很慢