U8SDK——远程日志打印功能
在发布U8SDK之后,使用U8SDK做SDK接入的同学,反馈比较多的一个问题就是调试困难。这其实不是U8SDK的毛病,而是Android开发的通病。做Android开发,我们通常都是结合logcat来调试,如果是原生的应用,有时我们还可以直接通过Debug断点来调试,但是,做游戏开发,我们一般采用U3D,Cocos2dx游戏引擎开发之后,发布到对应的移动平台。所以,断点调试就不太好实现了。很多时候,我们都是打印一些日志,然后在logcat中定位对应的日志,来查找错误信息。
之前也一直是使用logcat,但是logcat中日志太多,过滤功能虽然有,但是有时候,有些机型,打印的日志太多,你还没有看清楚,日志就被各种系统日志给顶掉了。
而且,很多做游戏开发的同学,可能也是游戏快要做好了,要接各个平台的渠道SDK的时候,才开始接触Android开发。导致对logcat如何正确使用,就更找不到门路。
所以,我想干脆在原有日志的基础上,再加一个远程日志打印,再接将日志输出到网页上,这样日志方便搜索,也不用理会各种乱七八糟的系统日志。
然后,我们在U8SDK中将原有日志简单封装了下,让日志打印可以支持远程打印。远程打印,顾名思义,我们需要一个服务器端,来接收这些日志,并输出到网页上。
首先想到的是,用java写,部署到tomcat,但是想想,如此简单的一个逻辑,用java真是有点浪费不说,还有点臃肿。最后,因为U8SDK打包工具用python写的,那么想就用python搭建一个吧,了解了下python中搭建一个web服务器端,有几种框架可以使用,最终选择了一个最最简单的web.py框架,这个框架要实现我们这个功能,真是再合适不过了,轻量的不能再轻量了。
我们只需要实现两个功能接口, 一个是上报日志的接口,一个是显示日志的接口。上报日志,我们采用Http Post方式, 显示日志,采用Http Get浏览器中直接访问。
为了让不同级别的日志显示区别开来,我们给不同级别的日志,配上不同的颜色。
客户端上报的日志,先在内存中放着,网页上我们启动一段js代码,每隔1秒钟自动刷新一次,并将滚动位置,始终保持在最后。这样,我们的一个简单的日志收集和展示的web 服务器就好了,代码片段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
import json import web web.config.debug = False urls = ( '/','index' ) localLogs = "" class index: def GET(self): htmlFormat = "<html><head><title></title></head><body>%s <script type=\"text/javascript\">function myrefresh(){window.location.reload();window.scrollTo(0,document.body.scrollHeight);}setTimeout('myrefresh()',1000); </script></body></html>" global localLogs localLogs = localLogs.encode('gbk') return htmlFormat % localLogs def POST(self): inputs=web.input() content = inputs.get('log') if content.startswith('{') and content.endswith('}'): content = '[' + content + ']' logs = json.loads(content) for log in logs: if 'stack' not in log: log['stack'] = " " color = '#808080' if log['level'] == 'INFO': color = '#008000' elif log['level'] == 'WARNING': color = '#FFA500' elif log['level'] == 'ERROR': color = '#FF0000' strLog = '<div style="color:%s">%s %s: [%s] %s </div>' % (color, log['time'],log['level'], log['tag'], log['msg']) stacks = log['stack'].split('\n') strLog = strLog + ('<div color="%s">' % color) for s in stacks: strLog = strLog + ('<div>%s</div>' % (s.strip())) strLog = strLog + '</div>' global localLogs localLogs = localLogs + strLog return "" if __name__ == '__main__': app = web.application(urls, globals()) app.run() |
上面这段代码,就是我们这个日志服务器的全部代码了,我们将这个文件保存为uconsole.py,然后,打开命令终端,运行:
1 2 |
python uconsole.py |
这样,就可以启动该服务器了,默认监听的端口是8080。如果该端口被占用了,会启动失败,那么我们可以换一个端口
1 2 |
python uconsole.py 8082 |
这样,我们就变成监听8082这个端口了。你可以打开http:localhost:8082/ 你会发现网页是空白的,但是每隔一秒网页会自动刷新一下,说明当前服务器已经启动好了,正在等待日志的到来。
接下来,我们就要设计下客户端部分的日志框架,让日志以Http Post的方式上报到我们这个日志服务器端。
我们打算将Android原有的日志(com.android.util.Log)进行一个简单的封装,因为我们仅仅要加一个远程日志接口,所以,我们不希望把它弄得过于复杂。
我们定义一个ILog接口,将日志接口提取出来,这样,我们将会有这个两个实现,一个LocalLog,一个RemoteLog。LocalLog中,我们依然调用Android自带的Log进行打印,RemoteLog中,我们将日志放到一个队列中,每隔一定的间隔,就将存储的日志一次性上报到日志服务器。
ILog接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public interface ILog { public void d(String tag, String msg); public void i(String tag, String msg); public void w(String tag, String msg); public void w(String tag, String msg, Throwable e); public void e(String tag, String msg); public void e(String tag, String msg, Throwable e); public void destory(); } |
ULocalLog实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
/** * * Android本地日志输出 * */ public class ULocalLog implements ILog{ @Override public void d(String tag, String msg) { Log.d(tag, msg); } @Override public void i(String tag, String msg) { Log.i(tag, msg); } @Override public void w(String tag, String msg) { Log.w(tag, msg); } @Override public void e(String tag, String msg) { Log.e(tag, msg); } @Override public void w(String tag, String msg, Throwable e) { Log.w(tag, msg, e); } @Override public void e(String tag, String msg, Throwable e) { Log.e(tag, msg, e); } @Override public void destory() { } } |
URemoteLog实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public class URemoteLog implements ILog{ private URemoteLogPrinter printer; public URemoteLog(String url, int interval){ printer = new URemoteLogPrinter(url, interval); } @Override public void d(String tag, String msg) { printer.print(new ULog(ULog.L_DEBUG, tag, msg)); } @Override public void i(String tag, String msg) { printer.print(new ULog(ULog.L_INFO, tag, msg)); } @Override public void w(String tag, String msg) { printer.print(new ULog(ULog.L_WARN, tag, msg)); } @Override public void w(String tag, String msg, Throwable e) { printer.print(new ULog(ULog.L_WARN, tag, msg, e)); } @Override public void e(String tag, String msg) { printer.print(new ULog(ULog.L_ERROR, tag, msg)); } @Override public void e(String tag, String msg, Throwable e) { printer.print(new ULog(ULog.L_ERROR, tag, msg, e)); } @Override public void destory() { printer.stop(); } } |
远程日志实现中,我们采用一个URemoteLogPrinter来临时存储日志,并定时传到日志服务器,该类实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
public class URemoteLogPrinter { private List<ULog> logs; private String url; private int interval = 1000; //单位 毫秒 private Timer timer; private boolean running; public URemoteLogPrinter(){ } public URemoteLogPrinter(String remoteUrl, int interval){ this.logs = Collections.synchronizedList(new ArrayList<ULog>()); this.url = remoteUrl; this.interval = interval; } public void print(ULog log){ start(); synchronized (logs) { logs.add(log); } } public void printImmediate(String url, ULog log){ Map<String, String> params = new HashMap<String,String>(); params.put("log", log.toJSON()); U8HttpUtils.httpPost(url, params); } public List<ULog> getAndClear(){ synchronized (logs) { List<ULog> all = new ArrayList<ULog>(logs); logs.clear(); return all; } } public void start(){ if(running){ return; } running = true; TimerTask task = new LogPrintTask(); timer = new Timer(true); timer.scheduleAtFixedRate(task, 100, interval); } public void stop(){ if(timer != null){ timer.cancel(); } running = false; } class LogPrintTask extends TimerTask{ @Override public void run() { try{ List<ULog> logs = getAndClear(); if(logs.size() > 0){ StringBuilder sb = new StringBuilder(); sb.append("["); for(ULog log : logs){ sb.append(log.toJSON()).append(","); } sb.deleteCharAt(sb.length()-1).append("]"); Map<String, String> params = new HashMap<String,String>(); params.put("log", sb.toString()); U8HttpUtils.httpPost(url, params); } }catch(Exception e){ e.printStackTrace(); stop(); } } } } |
这样,我们整个日志封装就可以,但是我们远程日志中,有几个参数我们需要设置下,比如,远程打印的时间间隔,远程日志服务器地址等参数。这些参数,我们后面放到AndroidManifest.xml中的meta-data中,同时,我们需要有一个调用的接口,给应用来调用日志。所以,我们封装一个Log类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 |
public class Log{ private static Log instance = new Log(); private List<ILog> logPrinters; private boolean isInited = false; private boolean enable = false; private String level = ULog.L_DEBUG; private boolean local = true; private boolean remote = true; private int remoteInterval = 1000; private String remoteUrl = ""; private Log(){ logPrinters = new ArrayList<ILog>(); } public static void d(String tag, String msg) { try{ if(!ULog.L_DEBUG.equalsIgnoreCase(instance.level)){ return; } for(ILog printer: instance.logPrinters){ printer.d(tag, msg); } }catch(Exception e){ e.printStackTrace(); } } public static void i(String tag, String msg) { try{ if(!ULog.L_DEBUG.equalsIgnoreCase(instance.level) && !ULog.L_INFO.equalsIgnoreCase(instance.level)){ return; } for(ILog printer: instance.logPrinters){ printer.i(tag, msg); } }catch(Exception e){ e.printStackTrace(); } } public static void w(String tag, String msg) { try{ if(ULog.L_ERROR.equalsIgnoreCase(instance.level)){ return; } for(ILog printer: instance.logPrinters){ printer.w(tag, msg); } }catch(Exception e){ e.printStackTrace(); } } public static void w(String tag, String msg, Throwable e) { try{ if(ULog.L_ERROR.equalsIgnoreCase(instance.level)){ return; } for(ILog printer: instance.logPrinters){ printer.w(tag, msg, e); } }catch(Exception e2){ e2.printStackTrace(); } } public static void e(String tag, String msg) { try{ for(ILog printer: instance.logPrinters){ printer.e(tag, msg); } }catch(Exception e){ e.printStackTrace(); } } public static void e(String tag, String msg, Throwable e) { try{ for(ILog printer: instance.logPrinters){ printer.e(tag, msg, e); } }catch(Exception e2){ e2.printStackTrace(); } } /** * 在Application的attachBaseContext中调用 * @param context */ public static void init(Context context){ try{ if(instance.isInited){ return; } instance.parseConfig(context); instance.logPrinters.clear(); if(!instance.enable){ android.util.Log.d("ULOG", "the log is not enabled."); return; } if(instance.local){ instance.logPrinters.add(new ULocalLog()); } if(instance.remote){ instance.logPrinters.add(new URemoteLog(instance.remoteUrl, instance.remoteInterval)); } Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, final Throwable e) { new Thread(new Runnable() { @Override public void run() { try{ URemoteLogPrinter printer = new URemoteLogPrinter(); printer.printImmediate(instance.remoteUrl, new ULog(ULog.L_ERROR, "Crash", "Application Crashed!!!", e)); }catch(Exception e){ e.printStackTrace(); }finally{ System.exit(0); } } }).start(); try { Thread.sleep(500); } catch (InterruptedException e1) { e1.printStackTrace(); } } }); instance.isInited = true; }catch(Exception e){ e.printStackTrace(); } } /** * 在Application的onTerminate中调用销毁 */ public static void destory(){ try{ if(instance.logPrinters != null){ for(ILog printer : instance.logPrinters){ printer.destory(); } } }catch(Exception e){ e.printStackTrace(); } } private void parseConfig(Context ctx){ try{ ApplicationInfo appInfo = ctx.getPackageManager().getApplicationInfo(ctx.getPackageName(), PackageManager.GET_META_DATA); if(appInfo != null && appInfo.metaData != null){ if(appInfo.metaData.containsKey("ulog.enable")){ enable = appInfo.metaData.getBoolean("ulog.enable"); } if(appInfo.metaData.containsKey("ulog.level")){ level = appInfo.metaData.getString("ulog.level"); } if(appInfo.metaData.containsKey("ulog.local")){ local = appInfo.metaData.getBoolean("ulog.local"); } if(appInfo.metaData.containsKey("ulog.remote")){ remote = appInfo.metaData.getBoolean("ulog.remote"); } if(appInfo.metaData.containsKey("ulog.remote_interval")){ remoteInterval = appInfo.metaData.getInt("ulog.remote_interval"); } if(appInfo.metaData.containsKey("ulog.remote_url")){ remoteUrl = appInfo.metaData.getString("ulog.remote_url"); } } }catch(Exception e){ e.printStackTrace(); } } } |
除了日志打印的几个接口,我们增加了两个接口,一个是init,一个是destroy。我们需要在调用Log打印之前,调用init方法进行初始化,因为我们需要读取配置参数,根据配置来设定对应的参数。同样的,在应用退出的时候,我们需要调用destroy来回收资源。
一般init我们可以在Application的onCreate或者attachBaseContext中调用;destroy可以在Application的onTerminate中调用
一切准备好之后,我们还需要在应用的AndroidManifest.xml中增加几个日志的配置参数:
1 2 3 4 5 6 7 |
<meta-data android:name="ulog.enable" android:value="true" /> <!--是否开启日志,关闭之后,不会输出到logcat也不会输出到远程--> <meta-data android:name="ulog.level" android:value="DEBUG" /> <!--日志级别(DEBUG|INFO|WARNING|ERROR)--> <meta-data android:name="ulog.local" android:value="true" /> <!--是否在logcat中打印--> <meta-data android:name="ulog.remote" android:value="true" /> <!--是否远程打印--> <meta-data android:name="ulog.remote_interval" android:value="500" /> <!--远程打印时,日志上报间隔,单位毫秒--> <meta-data android:name="ulog.remote_url" android:value="http://192.168.18.9:8080/" /> <!--远程日志服务器地址,就是uconsole监听的地址--> |
整个日志框架和服务器代码,已经放在github上,有需要的同学可以直接使用:
本文出自 U8SDK技术博客,转载时请注明出处及相应链接。
本文永久链接: http://www.uustory.com/?p=2049