自定义基座实时采集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() {
@Override
public 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() {
@Override
public 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 握手信息
*/
@Override
public void onOpen(ServerHandshake handshake) {
// WebSocket连接已成功建立
// 在此执行任何必要的操作
Log.i(TAG, "onOpen: " + handshake.getHttpStatus());
WfmPlugin.pushLogInit = true;
}
/**
* 当接收到来自服务器的消息时调用
*
* @param message 收到的消息内容
*/
@Override
public void onMessage(String message) {
// 处理来自服务器的传入消息
Log.i(TAG, "onMessage: " + message);
}
/**
* 当WebSocket连接关闭时调用
*
* @param code 关闭状态码
* @param reason 关闭原因
* @param remote 是否由远程服务器关闭
*/
@Override
public 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() {
@Override
public 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 发生的异常
*/
@Override
public 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请求开启
*/
@OnOpen
public 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 请求关闭
*/
@OnClose
public void onClose() {
// 从set中删除
log.info("WebSocket请求关闭:" + id);
webSocketMap.remove(id);
}
/**
* 发生异常
*/
@OnError
public void onErro(Throwable throwable) {
throwable.printStackTrace();
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public 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提交更方便查看。