自定义基座实时采集uniapp日志
打测试包给远端现场(测试/客户)实际测试时也能实时看到日志了,也有代码行数显示。
流程设计
uniapp收集代码
重写console方法
通过条件编译,在app使用环境重写日志打印方法
// #ifdef APP-PLUS=function(...args){console.log = function (...args) {try {_this.$plugins.getUtils("consoleLog", {'level': 'log', 'args': args})} catch (e) {console.info('console.log 打印失败', e);}}console.error = function (...args) {try {_this.$plugins.getUtils("consoleLog", {'level': 'error', 'args': args})} catch (e) {console.info('console.error 打印失败', e);}}console.warn = function (...args) {try {_this.$plugins.getUtils("consoleLog", {'level': 'warn', 'args': args})} catch (e) {console.info('console.warn 打印失败', e);}}// #endif
发送给安卓层
/*** 快捷调用安卓工具类方法* this.$plugins.getUtils('method',{userId:'test'})* @param {Object} method* @param {Object} jsonObject* @param {Object} successCallback* @param {Object} errorCallback* @return {String} 原始字符串,如果是json化返回的就是一个json字符串 不是对象!!!*/
getUtils: function(method, jsonObject, successCallback, errorCallback) {try {var success = typeof successCallback !== 'function' ? null : function(args) {successCallback(args);},fail = typeof errorCallback !== 'function' ? null : function(code) {errorCallback(code);};var callbackID = plus.bridge.callbackId(success, fail);return plus.bridge.exec(_BARCODE, "getUtils", [callbackID, method, jsonObject]);} catch (e) {console.error(e)errorCallback(e)}},//初始化方法,一般是登录后调用
_this.$plugins.getUtils("initConsoleLog", {'userId': _this.GLOBAL.$USER_INFO.user_iidd})
安卓自定义基座收集日志
跳转方法
/*** 工具类获取** @param pWebview* @param array* @return*/public void getUtils(IWebview pWebview, JSONArray array) {Log.i("getUtils", "工具类获取" + array.toString());String result = null;String CallBackID = array.optString(0);try {//方法String method = array.optString(1);JSONObject json = new JSONObject(array.optString(2));result = this.utilMethood(method, json, pWebview);} catch (Exception e) {e.printStackTrace();JSUtil.execCallback(pWebview, CallBackID, e.getMessage(), JSUtil.ERROR, false);}Log.i("getUtils", "工具类返回信息:\n" + result);JSUtil.execCallback(pWebview, CallBackID, result, JSUtil.OK, true);}
初始化日志信息方法
/*** WebSocket调试信息推送客户端*/
private PushConsoleWebSocketClient pushConsoleWebSocketClient = null;/*** 初始化推送*/
public static boolean pushLogInit = false;/*** 调试日志地址*/
public static String LOG_WS_URL = "ws://127.0.0.1:5080/weblog/uniapplogv2/";/*** 调试id*/
public static String LOG_WS_USERID = null;/*** 初始化日志信息** @param params* @param pWebview* @return*/
private String initConsoleLog(JSONObject params, IWebview pWebview) {LOG_WS_USERID = params.optString("userId");Log.i(TAG, "uniapp层初始化日志信息: " + LOG_WS_USERID);if (null != LOG_WS_USERID && !"".equals(LOG_WS_USERID)) {try {new Thread(new Runnable() {@Overridepublic void run() {try {pushConsoleWebSocketClient = PushConsoleWebSocketClient.builder(LOG_WS_URL, "系统名称", LOG_WS_USERID);pushConsoleWebSocketClient.connect();pushLogInit = true;} catch (Exception e) {e.printStackTrace();}}}).start();} catch (Exception e) {Log.e(TAG, "initConsoleLog: 初始化调试信息推送服务异常", e);}}return ResultUtil.ok("日志初始化完毕");
}
推送日志调试信息方法
/*** 推送日志信息到调试页面** @param log 日志内容* @param level 日志等级*/
private void pushLogToCache(String level, JSONArray log) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {try {com.alibaba.fastjson.JSONObject params = new com.alibaba.fastjson.JSONObject();params.put("code", "push");params.put("sys", pushConsoleWebSocketClient.getSys());params.put("userId", pushConsoleWebSocketClient.getUserId());params.put("level", level);params.put("timestamp", System.currentTimeMillis());params.put("time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()));try {params.put("log", parseUniappConsoleLog(log));} catch (Exception e) {params.put("log", log);}pushConsoleWebSocketClient.send(params.toString());} catch (Exception e) {e.printStackTrace();}}});thread.start();
// executorService.submit(thread);
}
安装自定义基座
安卓WebSocket客户端
安卓WebSocket客户端推送负责将调试日志推送给后端
gradle依赖
//WebSocket连接
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
import android.util.Log;import com.inspur.mobilefsp.plugins.WfmPlugin;import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;/*** WebSocket客户端类,用于与服务器建立WebSocket连接并处理消息。* 该类实现了WebSocketClient接口,并提供了连接、消息处理和错误处理的功能。** @author 淡梦如烟* @date 20250211*/
public class PushConsoleWebSocketClient extends WebSocketClient {/*** 日志标签,用于标识日志输出的来源*/public final static String TAG = "PushLogClient";/*** WebSocket服务器的URL*/private String url;/*** 系统名称*/private String sys;/*** 用户ID*/private String userId;/*** 构造函数,初始化WebSocket客户端** @param serverUrl WebSocket服务器的URL* @param sys 系统名称* @param userId 用户ID* @throws URISyntaxException 如果提供的URL格式不正确*/public PushConsoleWebSocketClient(String serverUrl, String urlParams, String sys, String userId) throws URISyntaxException {super(new URI(serverUrl + urlParams));this.url = serverUrl;this.sys = sys;this.userId = userId;}/*** 建造者生成客户端** @param serverUrl* @param sys* @param userId* @return*/public static PushConsoleWebSocketClient builder(String serverUrl, String sys, String userId) {try {//自定义参数,自行实现JSONObject json = new JSONObject();json.put("code", "pushStart");json.put("userId", userId);json.put("sys", sys);JSONObject password = new JSONObject();password.put("userId", userId);password.put("timestamp", System.currentTimeMillis());//aes加密 ,自行实现或者用第三方包String encode = QEncodeUtil.aesEncrypt(json.toString(), "aes秘钥2");encode = URLEncoder.encode(encode, "UTF-8");//百分号不能作为参数encode = encode.replaceAll("%", "BaiFenHao");String url = serverUrl + encode;Log.e(TAG, "builder: websocket地址:" + url);PushConsoleWebSocketClient pushConsoleWebSocketClient = new PushConsoleWebSocketClient(serverUrl, encode, sys, userId);return pushConsoleWebSocketClient;} catch (Exception e) {throw new RuntimeException(e);}}/*** 获取WebSocket服务器的URL** @return WebSocket服务器的URL*/public String getUrl() {return url;}/*** 获取系统名称** @return 系统名称*/public String getSys() {return sys;}/*** 获取用户ID** @return 用户ID*/public String getUserId() {return userId;}/*** 当WebSocket连接成功建立时调用** @param handshake 握手信息*/@Overridepublic void onOpen(ServerHandshake handshake) {// WebSocket连接已成功建立// 在此执行任何必要的操作Log.i(TAG, "onOpen: " + handshake.getHttpStatus());WfmPlugin.pushLogInit = true;}/*** 当接收到来自服务器的消息时调用** @param message 收到的消息内容*/@Overridepublic void onMessage(String message) {// 处理来自服务器的传入消息Log.i(TAG, "onMessage: " + message);}/*** 当WebSocket连接关闭时调用** @param code 关闭状态码* @param reason 关闭原因* @param remote 是否由远程服务器关闭*/@Overridepublic void onClose(int code, String reason, boolean remote) {Log.e(TAG, "onClose: code[" + code + "];remote[" + remote + "];url[" + this.url + "];reason:" + reason);// WebSocket连接已关闭// 在此执行任何必要的清理操作
// this.reconnectAfterMillis(100L);}/*** 重连锁*/private static boolean reConnectLock = false;/*** 延迟重连** @param millis*/public void reconnectAfterMillis(Long millis) {try {if (reConnectLock) {return;}reConnectLock = true;new Thread(new Runnable() {@Overridepublic void run() {try {// 尝试在5秒后重新连接Thread.sleep(millis);reconnect();} catch (Exception e) {e.printStackTrace();} finally {reConnectLock = false;}}}).start();} catch (Exception e) {e.printStackTrace();reConnectLock = false;} finally {}}/*** 当WebSocket连接期间发生错误时调用** @param ex 发生的异常*/@Overridepublic void onError(Exception ex) {Log.e(TAG, "onError: ", ex);// 处理WebSocket连接期间发生的任何错误
// this.reconnectAfterMillis(5000L);}
}
后台代码
springboot接受日志和推送日志
package com.faker.weblog.websocket;import cn.hutool.core.net.URLDecoder;
import com.alibaba.fastjson2.JSONObject;
import com.faker.weblog.model.dto.PushUniappLogDto;
import com.faker.weblog.util.Toolkit;
import com.faker.weblog.wrapper.WrapMapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;@Lazy
@Component
@Slf4j
@ServerEndpoint("/uniapplogv2/{id}")
@Api(value = "websocket日志接受和推送uniapp日志工具")
public class UniappLogWebHandleV2 {@ApiModelProperty(value = "客户端id")private String id;@ApiModelProperty(value = "是否初始化", example = "true")private boolean initialized = false;@ApiModelProperty(value = "是否接受日志消息", example = "true")private boolean isPullLogs = false;@ApiModelProperty(value = "系统名称", example = "fakerSys")private String sys;@ApiModelProperty(value = "用户id", example = "test")private String userId;/*** 日志列表*/private static ConcurrentHashMap<String, ConcurrentHashMap<String, List<String>>> logListMap = new ConcurrentHashMap<>();/*** 获取日志列表** @return*/public ConcurrentHashMap<String, ConcurrentHashMap<String, List<String>>> getlogListMap() {return logListMap;}/*** 清理日志列表*/public static void cleanLogListMap() {logListMap.clear();}/*** concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象。*/private static ConcurrentHashMap<String, UniappLogWebHandleV2> webSocketMap = new ConcurrentHashMap<String, UniappLogWebHandleV2>();/*** websocket的session*/private Session session;/*** 获取session** @return*/public Session getSession() {return this.session;}/*** 新的WebSocket请求开启*/@OnOpenpublic void onOpen(Session session, @PathParam("id") String id) {log.info("新的WebSocket请求开启:" + id);try {String decode = id.replaceAll("BaiFenHao", "%");decode = URLDecoder.decode(decode, Charset.forName("UTF-8"));String aesJson = com.faker.dba.util.QEncodeUtil.aesDecrypt(decode, "aes秘钥2");JSONObject jsonObject = JSONObject.parseObject(aesJson);String userId = jsonObject.getString("userId");String password = jsonObject.getString("password");String sign = jsonObject.getString("sign");if (jsonObject.get("isPullLogs") != null) {this.isPullLogs = jsonObject.getBoolean("isPullLogs");}this.sys = jsonObject.getString("sys");this.userId = userId;this.session = session;//鉴权方法,自行实现this.validate(userId, sign, password);this.id = id;webSocketMap.put(id, this);String code = jsonObject.getString("code");if ("pushStart".equalsIgnoreCase(code)) {//app推送方法if (thisLististMap == null) {thisLististMap = new ConcurrentHashMap<>();logListMap.put(this.sys, thisLististMap);}List<String> logList = thisLististMap.get(this.userId);if (logList == null) {logList = new ArrayList<>();thisLististMap.put(this.userId, logList);}} else if ("webStart".equalsIgnoreCase(code)) {//pc端查看日志方法this.isPullLogs = true;this.sys = jsonObject.getString("watchSys");this.userId = jsonObject.getString("watchUserId");ConcurrentHashMap<String, List<String>> thisLististMap = logListMap.get(this.sys);if (thisLististMap != null) {List<String> logList = thisLististMap.get(this.userId);if (logList != null) {for (String log : logList) {try {session.getBasicRemote().sendText(log);} catch (IOException e) {e.printStackTrace();}}}}}} catch (Exception e) {log.error("鉴权错误:" + id, e);}}/*** WebSocket 请求关闭*/@OnClosepublic void onClose() {// 从set中删除log.info("WebSocket请求关闭:" + id);webSocketMap.remove(id);}/*** 发生异常*/@OnErrorpublic void onErro(Throwable throwable) {throwable.printStackTrace();}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, Session session) throws IOException {log.debug("websocket来自客户端的消息:{}", message);JSONObject jsonObject = JSONObject.parseObject(message);String code = jsonObject.getString("code");if (this.initialized) {if ("push".equalsIgnoreCase(code)) {PushUniappLogDto params = JSONObject.parseObject(message, PushUniappLogDto.class);if (Toolkit.isNullOrEmpty(params.getSys())) {log.warn("系统名称不能为空");return;}if (Toolkit.isNullOrEmpty(params.getUserId())) {log.warn("用户id不能为空");return;}if (Toolkit.isNullOrEmpty(params.getLevel())) {log.warn("日志等级不能为空");return;}if (Toolkit.isNullOrEmpty(params.getLog()) || "[]".equals(params.getLog())) {log.warn("日志信息不能为空");return;}this.sendLogs(JSONObject.toJSONString(params));}} else {log.warn("[" + this.sys + "][" + this.userId + "]未初始化" + this.initialized);}}/*** token鉴权** @param userId* @param sign* @param password* @throws IOException*/public void validate(String userId, String sign, String password) throws IOException {if (Toolkit.isNotNull(userId) && Toolkit.isNotNull(sign)) {//校验userId和密码 这里简化为校验userId和时间戳的aes加密信息,校验通过初始化连接try {String aesJson = com.faker.dba.util.QEncodeUtil.aesDecrypt(sign, "aes秘钥1");JSONObject aesJsonObject = JSONObject.parseObject(aesJson);if (aesJsonObject.get("userId") == null || aesJsonObject.get("timestamp") == null) {session.getBasicRemote().sendText("加密信息校验错误,已记录!" + "<br>" + aesJson + "<br>");session.close();}if (userId.equals(aesJsonObject.getString("userId"))) {if (aesJsonObject.getLong("timestamp") > System.currentTimeMillis() - 1000 * 60 * 5|| aesJsonObject.getLong("timestamp") < System.currentTimeMillis() + 1000 * 60 * 5) {this.initialized = true;session.getBasicRemote().sendText(JSONObject.toJSONString(WrapMapper.ok("签名[" + sign + "]正确,已记录!")));} else {session.getBasicRemote().sendText(JSONObject.toJSONString(WrapMapper.error("签名[" + sign + "]已过期,已记录!")));session.close();}} else {session.getBasicRemote().sendText(JSONObject.toJSONString(WrapMapper.error("签名[" + sign + "]错误,已记录!")));session.close();}} catch (Exception e) {log.error("加密信息[" + password + "]校验错误", e);session.getBasicRemote().sendText(JSONObject.toJSONString(WrapMapper.error("加密信息校验错误,已记录!" + "<br>" + e.getMessage())));session.close();}} else if (Toolkit.isNotNull(userId) && Toolkit.isNotNull(password)) {//todo 校验登录密码} else {log.error("登录信息错误[" + userId + "][" + password + "][" + sign + "]");session.getBasicRemote().sendText(JSONObject.toJSONString(WrapMapper.error("登录信息错误,已记录!")));session.close();}}/*** 向客户端发送消息** @param message*/public void sendLogs(String message) {ConcurrentHashMap<String, List<String>> thisLististMap = logListMap.get(this.sys);if (thisLististMap == null) {thisLististMap = new ConcurrentHashMap<>();}List<String> logList = thisLististMap.get(this.userId);if (logList == null) {logList = new ArrayList<>();}logList.add(message);//日志暂存最新的100条if (logList.size() > 100) {logList.remove(0);}this.sendToUser(message);}/*** 向指定客户端发送消息** @param message*/private void sendToUser(String message) {for (UniappLogWebHandleV2 webSocket : webSocketMap.values()) {if (webSocket.isInitialized() && webSocket.isPullLogs() && webSocket.getSys().equals(this.sys) && webSocket.getUserId().equals(this.userId)) {log.debug("【websocket消息】广播消息, message={}", message);try {Session session = webSocket.getSession();session.getBasicRemote().sendText(message);} catch (Exception e) {log.error("【websocket消息】广播消息, message={}", message);}}}}/*** 向所有客户端发送消息** @param message*/public void sendToAll(String message) {for (UniappLogWebHandleV2 webSocket : webSocketMap.values()) {if (!webSocket.isInitialized()) {continue;}log.debug("【websocket消息】广播消息, message={}", message);try {Session session = webSocket.getSession();session.getBasicRemote().sendText(message);} catch (Exception e) {log.error("【websocket消息】广播消息, message={}", message);}}}public String getId() {return id;}public String getSys() {return sys;}public String getUserId() {return userId;}public boolean isInitialized() {return initialized;}public boolean isPullLogs() {return isPullLogs;}
}
html页面渲染日志
const id = JSON.stringify({code: 'webStart',userId: userToken.userId,sign: userToken.token,isPullLogs: true,watchSys: $('#sys').val(),watchUserId: $('#userId').val()
})
//aes加密
const aes = aesEncrypt(id)
//替换百分号
const url = encodeURIComponent(aes).replaceAll('%', 'BaiFenHao')
console.log('[信息]传输协议秘钥', id, aes, url)
// 指定websocket路径
var wsUrl = 'ws://' + location.host + '/weblog/uniapplogv2/' + url;try {if (null != websocket && undefined != websocket) {websocket.close();}
} catch (e) {console.warn(e)
}
websocket = new WebSocket(wsUrl);
websocket.onmessage = function (event) {// 接收服务端的实时日志并添加到HTML页面中$("#log-container div").append(showColorLog(event.data));$("#log-container div").append('<br/>')if (localStorage.autoJump == '是') {// 滚动条滚动到最低部$("#log-container").scrollTop($("#log-container div").height() - $("#log-container").height());}
};
websocket.onopen = function (event) {reloadLock = false;
}websocket.onerror = function (error) {console.log('onerror', error)// $("#log-container div").append('<br/><br/>连接已断开... 5秒后尝试重新连接........ <br/><br/>');// setTimeout(reloadWebSocket(), 5000)
}
websocket.onclose = function (event) {console.log('onclose', event)$("#log-container div").append('<br/><br/>连接已关闭... 5秒后尝试重新连接........ <br/><br/>');setTimeout(reloadWebSocket, 5000)
}
总结
给远程调试提供方便,websocket推送消耗较少,也是有序推送,完善好重连机制比post提交更方便查看。