作用

游戏临近上线,需要做一个日志系统,记录玩家的行为,用途如下:

  • 监控玩家状态变化,如账号登记,角色创建,上线下线,充值等;
  • 分析玩家行为,如金币钻石消耗在什么系统上了,主要参与了哪些活动和玩法;
  • 帮助分析bug,记录玩家的行为和数据变化,可以回溯bug产生的过程;
  • 方便客服,查询和处理玩家的反馈。

结构设计

首先,用一台公共的服务器左右日志的db服务器,所有游戏中产生的日志,都往这个db中写;

然后,查询系统需要一个后端,与前端交互,来处理查询逻辑,反馈数据;

最后,需要一个前端,提交查询条件,展示查询结果。

实现

日志数据库

在网易无论手游还是端游,基本上都是用mongo,出来之后游戏数据库也就用了mongo,在我看来主要基于两个优点:

  1. 游戏需求多变,mongo直接写json,省去需要建表改表的麻烦;
  2. 对于游戏数据库,无须完成逻辑,只要存数据就好,用不上复杂的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;

前端

考虑到这个工具会时常更新,多方会用到,用客户端的话,用网页比较合适,更新之后刷新就可以了。

关于日志的显示的表格,用了一个 jQeruy 的插件 jqGrid ,关于使用可以参考下我之前写过的博客

这里有点技巧就是列名需要动态的获取,否则就要在客户端写很多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);

最后上截图:

ios 游戏实时日志 游戏日志有什么用_游戏

ios 游戏实时日志 游戏日志有什么用_Data_02

ios 游戏实时日志 游戏日志有什么用_Data_03

注意:

  1. js跨域问题,处理起来要小心
  2. mongo分页查询skip,当数据过多时,会很慢