一 前言 :
最近公司希望我做一个日志系统,用来排查手手游Bug用的。因为前些时候实现了vConsole在手机上的显示,所以觉得是轻车熟路了。麻烦的是 : 需要玩家在出现bug后打开记录截图给我方策划,供前端开发人员分析,Low是Low了点,但是好实现。但是没过几天新的情况出现了:游戏闪退。Oh , My God!玩家截图的机会都没有了。只有硬着头皮搞正真的Log方案了。
我在GitHub上搜到Log4js,但是专门为node.js做的类库,放在Egret前端上折腾了大半天,因为Log4js依赖太多的node.js库,所以无赖放弃。只有在互联网上自找其他出路。
二 方案(H5前端):
Ⅰ:Log数据的来源
①,重写window.console方法
②,监听window.onerror方法
思想:当日志信息的条数达到一定的数量,或者有重要日志信息,立即向Web服务器发送请求,要求服务器保存日志。
如下代码:
(function(){
var ___log___ = console.log;
var ___error___ = console.error;
var ___warn___ = console.warn;
var ___info___ = console.info;
var ___trace___ = console.trace;
console.error = function(errMessage){
_cacheLog( "error" , errMessage );
___error___.apply(console,arguments);
};
console.log = function(logMessage){
_cacheLog( "log" , logMessage );
___log___.apply(console,arguments);
};
console.warn = function(warnMessage){
_cacheLog( "warn" , warnMessage );
___warn___.apply(console,arguments);
};
console.info = function (infoMessage) {
_cacheLog( "info" , infoMessage );
___info___.apply( console , arguments );
};
console.trace = function ( traceMessage ) {
_cacheLog( "trace" , traceMessage );
___trace___.apply( console , arguments );
}
})();
window.onerror = function(msg, url, line, col, error) {
var extra = !col ? '' : '\ncolumn: ' + col;
extra += !error ? '' : '\nerror: ' + error;
_cacheLog( "system_error" , msg + "\nurl: " + url + "\nline: " + line + extra );
var suppressErrorAlert = true;
return suppressErrorAlert;
};
Ⅱ:请求服务器保存Log
var _saveLog = function ( $logList = null ) {
var $logMsg = _getLogList( $logList );
if( $logMsg ){
var $jsonTotal = ___config___["data_json"];
$jsonTotal = $jsonTotal.replace("{1}" , ___playerID___);
$jsonTotal = $jsonTotal.replace("{2}" , ___platfrom___);
$jsonTotal = $jsonTotal.replace("{3}" , ___serverID___);
$jsonTotal = $jsonTotal.replace("{4}" , $logMsg);
// var $blob = new Blob([$jsonTotal], { type: "application/json" });//text/plain;charset=utf-8
var $oXHR = new XMLHttpRequest();
$oXHR.responseType = "text";
$oXHR.open(___config___["upload_method"], ___servicePath___);
$oXHR.setRequestHeader( "Content-Type", "application/x-www-form-urlencoded" );//application/x-www-form-urlencoded
$oXHR.addEventListener('load', function(event){
console.log("upload--ok--");
}, true);
$oXHR.send( $jsonTotal );//$blob
}
}
Ⅲ:请求服务器提供相关的Log
思想:服务器将某一个渠道旗下的一个服务器下的某一个玩家的所有的日志文件达成一个Zip压缩包 ,供H5前端客户下载
//放到公司的管理网站上
var _downLog = function () {
var $jsonTotal = ___config___["down_json"];
$jsonTotal = $jsonTotal.replace("{1}" , ___playerID___);
$jsonTotal = $jsonTotal.replace("{2}" , ___platfrom___);
$jsonTotal = $jsonTotal.replace("{3}" , ___serverID___);
var xhr = new XMLHttpRequest();
xhr.open('post', "https://localhost:44370/Home/DownLog", true);
xhr.responseType = 'blob';
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
// var name = xhr.getResponseHeader('content-disposition');
// var filename = name.substring(20, name.length);
var blob = new Blob([xhr.response]);
let link = document.createElement('a');
let url = URL.createObjectURL(blob);
link.style.display = 'block';
link.href = url;
link.download = "Log.zip";
document.body.appendChild(link);
link.click();
}
}
xhr.send($jsonTotal);
}
需要注意的是 , content-disposition header字段受到了限制。调用就会报Refused to get unsafe header的错误。
三 方案(C# .Net Core):
之所以选择C# , 是因为我对C#比较熟悉。
Ⅰ:写Log的思想如下图所示:
①,平台 ,服务器ID , 玩家ID都是H5客户端传来的
②,生成TXT日志文件,需要注意的是:同一天的放在都一个Txt中。
③,过期Txt日志文件需要删除,以释放服务器的空间。
Ⅱ:C#后端处理H5前端的下载请求
[HttpPost]
public ActionResult DownLog(string data)
{
LogTxt log = JsonConvert.DeserializeObject<LogTxt>(data);
string path = $"{this._settings.Value.Root}/pf_{log.Platform}/server_{log.ServerID}/player_{log.PlayerID}";
byte[] bts = null;
System.Net.Mime.ContentDisposition cd = null;
if (Directory.Exists(path))
{
ZipFile.CreateFromDirectory(
path,
$"{path}.zip"
);
bts = System.IO.File.ReadAllBytes($"{path}.zip");
System.IO.File.Delete($"{path}.zip");
cd = new System.Net.Mime.ContentDisposition
{
FileName = $"player_{log.PlayerID}.zip",
Inline = false // false = prompt the user for downloading; true = browser to try to show the file inline
};
}
else {
if (!System.IO.File.Exists($"{this._error.Value.Root}/{this._error.Value.Sub}.zip"))
{
IO.Instance.CreateFolder(this._error.Value.Root);
IO.Instance.CreateFolder($"{this._error.Value.Root}/{this._error.Value.Sub}");
DirectoryInfo root = new DirectoryInfo($"{this._error.Value.Root}/{this._error.Value.Sub}");
FileInfo[] files = root.GetFiles();
if (files == null || files.Length == 0)
{
IO.Instance.CreateFile($"{this._error.Value.Root}/{this._error.Value.Sub}/error.txt");
IO.Instance.WriteCommon($"{this._error.Value.Root}/{this._error.Value.Sub}/error.txt", this._error.Value.Txt, true);
}
ZipFile.CreateFromDirectory(
$"{this._error.Value.Root}/{this._error.Value.Sub}",
$"{this._error.Value.Root}/{this._error.Value.Sub}.zip"
);
}
bts = System.IO.File.ReadAllBytes($"{this._error.Value.Root}/{this._error.Value.Sub}.zip");
cd = new System.Net.Mime.ContentDisposition
{
FileName = $"{this._error.Value.Sub}.zip",
Inline = false // false = prompt the user for downloading; true = browser to try to show the file inline
};
}
Response.Headers.Add("Content-Disposition", cd.ToString());
Response.Headers.Add("X-Content-Type-Options", "nosniff");
return File(bts, "application/zip");
}
思想 :
①,根据前端提供的平台 ,服务器ID , 玩家ID来寻找相关日志
②,如果找到则将日志所在的整个文件夹打包压缩
③,如果没找到,查找有无error.zip 如有,则返回error.zip,没有就构建error.zip并返回