unity日志过滤器

背景:

 之前做游戏的时候和同组的同事聊过说日志过滤尽量不要限制大家怎么使用日志打印的接口,不要加额外的参数,比如多加一个标签string,或者使用特定的接口,枚举。最好就是日志大家还是用Debug.Log无感去用,然后通过勾选一些toggle去赛选你感兴趣的日志。

原理:

通过分析堆栈,判断当前日志所属哪一个模块,具体属于哪一个模块可以通过自定义模块路径配置来解决,比如LogFilter.json

如果你的日志来源于路径manager/net那么该日志属于网络模块,勾选网络则只打印网络日志,当然可以同时勾选多个日志,这取决于你上面配置了多少json数组。另外当你勾选了一些toggle之后如果希望保存,比如你只关心你的模块但是不想每次都去勾选一堆的toggle,那么点击Save tag可以保存你的修改 ,对应的配置路径在

其json格式为:

运行截图:

核心类:

#if DebugMod
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

using HotFix.Manager;
using HotFix.UtilTool;
using Newtonsoft.Json;
using UnityEngine;

namespace HotFix.Helper.LogHelper
{

    public class LogHelper : SingletonMono<LogHelper>
{
public class Log
{
public enum _LogType
{
Assert = LogType.Assert,
Error = LogType.Error,
Exception = LogType.Exception,
Log = LogType.Log,
Warning = LogType.Warning,
}

        
public _LogType logType;
public string condition;
public string stacktrace;
public string time;
public string moduleName;

            public override string ToString()
{
return time + condition + "\n" + stacktrace;
}


public Log CreateCopy()
{
return (Log)this.MemberwiseClone();
}
public float GetMemoryUsage()
{
return (float)(sizeof(int) +
sizeof(_LogType) +
condition.Length * sizeof(char) +
stacktrace.Length * sizeof(char) +
sizeof(int));
}
}

        public List<Log> logs = new();
public Dictionary<string, string> fileModule = new();

        private Dictionary<string, HashSet<string>> customModuleMap;
public Dictionary<string, HashSet<string>> CustomModuleMap
{
get
{
#if UNITY_EDITOR
if(customModuleMap == null)
{
var sourceFile = proRoot + "/environment/LogFilter.json";
if (File.Exists(sourceFile))
{
if(File.Exists(persistentDataPath + "/LogFilter.json"))
{
File.Delete(persistentDataPath + "/LogFilter.json");
}
File.Copy(sourceFile, persistentDataPath + "/LogFilter.json");
}
}

#endif
if(customModuleMap == null && File.Exists(persistentDataPath + "/LogFilter.json"))
{
customModuleMap = JsonConvert.DeserializeObject<Dictionary<string, HashSet<string>>>(File.ReadAllText(persistentDataPath + "/LogFilter.json"));

}
return customModuleMap;
}

}
public Regex pattern = new Regex(@"\bat Assets/([^:]+)\.cs:\d+\b", RegexOptions.Compiled);
public string persistentDataPath = "";
public string proRoot;
public override void Init()
{
#if UNITY_EDITOR // debug模式不监听 因为需要正则处理堆栈信息 
Application.logMessageReceivedThreaded -= CaptureLogThread;
Application.logMessageReceivedThreaded += CaptureLogThread;
#endif
persistentDataPath = Application.persistentDataPath;
proRoot = Environment.CurrentDirectory;
}
string GetModuleName(string path, int level = 2)
{
if(CustomModuleMap != null && CustomModuleMap.Count > 0)
{
string matchKey = customModuleMap.FirstOrDefault(pair => pair.Value.Any(value => path.StartsWith(value))).Key;
if(matchKey != null)
{
return matchKey;
}

            }
return "other";
//int index = 0;
//int lastIndex = 0;
//int runLevel = 0;
//for (int i = path.Length - 1; i >= 0; --i)
//{
//    if (path[i] == '/')
//    {
//        lastIndex = index;
//        index = i;
//        ++runLevel;
//        if (runLevel == level)
//        {
//            break;
//        }
//    }
//}
//int length = Math.Abs(index - lastIndex) - 1;
//length = Math.Max(0, length);
//var moduleName = path.Substring(index + 1, length);
//return moduleName;
}
void CaptureLogThread(string condition, string stacktrace, LogType type)
{

var arr = stacktrace.Split("\n");
string dir = null;
string csName = "";
string moduleName = "";
//逐行分析 日志 
for (int i = 0; i < arr.Length; i++)
{
//有些日志属于自定义日志 或者封装的日志接口 这种需要过滤堆栈 
if (arr[i].StartsWith("HotFix.UtilTool.CommonUtils"))
{
continue;
}
if (pattern.IsMatch(arr[i]))
{
var group = pattern.Match(arr[i]);
dir = group.Groups[1].Value;
csName = dir.Substring(dir.LastIndexOf("/") + 1);
lock(fileModule)
{
if (!fileModule.ContainsKey(csName))
{
moduleName = GetModuleName(dir,2);
fileModule[csName] = moduleName;
}
else
{
moduleName = fileModule[csName];
}
}
Log log = new Log() { condition = condition, stacktrace = stacktrace, logType = (Log._LogType)type ,moduleName = moduleName,time = $"[{DateTime.Now.ToString("HH:mm:ss:fff")}]" };
lock (logs) 
{
logs.Add(log);
}
break;
}
}

}

        
}
}
#endif

核心gui类:

部分接口请参考github实现。

#if DebugMod
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using HotFix.Helper.LogHelper;
using Newtonsoft.Json;
using HotFix.UtilTool;
using System.IO;
using System.Text;


//作为loghelper的显示窗口 不打算做到包体内部
// 先在编辑器上测试 

public class LogHelperWindow : EditorWindow
{

public class ShowControlInfo
{
public ShowControlInfo()
{
isShowLog = false;
Color = new Color(1, 1, 1, 1);
showDebugTrace = false;
}
public bool showDebugTrace; // 是否显示堆栈 

public int color_r = 255;
public int color_g = 255;
public int color_b = 255;
[JsonIgnore]
public Color _color;
[JsonIgnore]
private bool isColorCtor = false;
[JsonIgnore]
public Color Color
{
set
{
_color = value;
color_r = UnityEngine.Mathf.CeilToInt( _color.r * 255);
color_g = UnityEngine.Mathf.CeilToInt(_color.g * 255);
color_b = UnityEngine.Mathf.CeilToInt(_color.b * 255);
}
get
{
if(!isColorCtor)
{
isColorCtor = true;
_color = new Color(color_r / 255f, color_g / 255f, color_b / 255f, 1f);
}
return _color;
}
}
public bool isShowLog;
}
// 滚动视图的位置
private Vector2 scrollPosition;
private bool isShow = false;
public Dictionary<string, ShowControlInfo> allTags = new Dictionary<string, ShowControlInfo>();
private bool isAllSelected = false; // 全选状态
private Vector2 tagScrollPosition; // 标签滚动视图位置

private bool isInited = false;
private GUIStyle customLogStyle;
// 绘制错误图标
private Texture2D errorIcon; 
[InitializeOnLoadMethod]
static void Initialize()
{
if( EditorPrefs.GetBool("customlog"))
{
// 订阅播放模式状态改变事件
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
}


}
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode)
{
// 当进入播放模式时,打开自定义控制台窗口
ShowWindow();
}
else if(state == PlayModeStateChange.ExitingPlayMode)
{
CloseWindow();
}
}

    public static void ShowWindow()
{
// 获取或创建自定义控制台窗口实例
LogHelperWindow window = GetWindow<LogHelperWindow>(false,"Custom Console",true);
window.Show();
window.OnShow();
}

    public static void CloseWindow()
{
LogHelperWindow window = GetWindow<LogHelperWindow>(false, "Custom Console", true);
window.OnRelease();
}
private void OnShow()
{
isShow = true;
}
private void OnRelease()
{
isShow = false;
isInited = false;
}

    // 绘制窗口内容
private void OnGUI()
{
if (!Application.isPlaying || !isShow) return;

        if(!isInited)
{
isAllSelected = false;
isInited = true;
errorIcon = EditorGUIUtility.Load("icons/d_console.erroricon.sml.png") as Texture2D;
if (errorIcon == null)
{
errorIcon = new Texture2D(16, 16);
Color[] pixels = new Color[16 * 16];
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = Color.red; // 红色感叹号
}
errorIcon.SetPixels(pixels);
errorIcon.Apply();
}

customLogStyle = new GUIStyle(EditorStyles.label);
// 设置选中时的背景颜色
Texture2D selectedBackground = new Texture2D(1, 1);
selectedBackground.SetPixel(0, 0, Color.blue); // 这里将选中背景颜色设为红色
selectedBackground.Apply();
//customLogStyle.normal.background = selectedBackground;
customLogStyle.onFocused.background = selectedBackground;

            ReadTagConfig();
}


// --- 整体横向布局 ----
EditorGUILayout.BeginHorizontal(GUILayout.ExpandHeight(true));

        // ----------- 左侧:日志滚动区(自动填充剩余空间) ------------
scrollPosition = EditorGUILayout.BeginScrollView(
scrollPosition,
GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)
);
{

var logs = LogHelper.Instance.logs;
foreach (var item in LogHelper.Instance.fileModule.Values)
{
if (!allTags.ContainsKey(item))
allTags.Add(item, new ShowControlInfo());
}
Color consoleColor = GUI.color;
foreach (var log in logs)
{
var tagInfo = allTags[log.moduleName];
if (!tagInfo.isShowLog) continue;

                GUI.color = tagInfo.Color;
EditorGUILayout.BeginVertical("box");
string content = "";
if (tagInfo.showDebugTrace)
{
content = $"{log.time}{log.condition}\n{log.stacktrace}";
}
else
{
content = $"{log.time}{log.condition}";
}

                if(log.logType == LogHelper.Log._LogType.Error)
{
EditorGUILayout.BeginHorizontal();
{
GUILayout.Label(new GUIContent(errorIcon), GUILayout.Width(16), GUILayout.Height(16)); // 显示图标

                        var height = Mathf.Max(20, GUI.skin.label.CalcHeight(new GUIContent(content), position.width - 240)); //留出右侧宽度
EditorGUILayout.SelectableLabel(content, customLogStyle, GUILayout.Height(height));
EditorGUILayout.EndHorizontal();
}

}
else
{
var height = Mathf.Max(20, GUI.skin.label.CalcHeight(new GUIContent(content), position.width - 240)); //留出右侧宽度
EditorGUILayout.SelectableLabel(content, customLogStyle, GUILayout.Height(height));
}


EditorGUILayout.EndVertical();
}
GUI.color = consoleColor;
EditorGUILayout.EndScrollView();
}

        // ----------- 右侧:标签筛选区(固定宽度,竖直铺满) ------------
EditorGUILayout.BeginVertical("box", GUILayout.Width(400), GUILayout.ExpandHeight(true));
{
EditorGUILayout.LabelField("标签筛选", EditorStyles.boldLabel);

            // 标签滚动部分-竖直铺满
tagScrollPosition = EditorGUILayout.BeginScrollView(tagScrollPosition, GUILayout.ExpandHeight(true));
{
foreach (var tag in allTags)
{
GUILayout.BeginHorizontal();
{
bool newValue = GUILayout.Toggle(tag.Value.isShowLog,tag.Key);
if (newValue != tag.Value.isShowLog)
allTags[tag.Key].isShowLog = newValue;

                        GUILayout.Space(5);

                        bool showTrace = GUILayout.Toggle(tag.Value.showDebugTrace,"堆栈" );
if (showTrace != tag.Value.showDebugTrace)
allTags[tag.Key].showDebugTrace = showTrace;
GUILayout.Space(5);
GUILayout.Label("颜色", GUILayout.Width(40));
Color newColor = EditorGUILayout.ColorField(tag.Value.Color, GUILayout.Width(60));
if (newColor != tag.Value.Color)
tag.Value.Color = newColor;

                        GUILayout.EndHorizontal();
}

}
}
EditorGUILayout.EndScrollView();

            // 全选/取消全选
EditorGUILayout.Space();
if (GUILayout.Button(isAllSelected ? "取消全选" : "全选"))
{
isAllSelected = !isAllSelected;
var keys = new List<string>(allTags.Keys);
foreach (var key in keys)
allTags[key].isShowLog = isAllSelected;
}
}
EditorGUILayout.EndVertical();

        // --- End横向大布局
EditorGUILayout.EndHorizontal();

        // ------ 下方操作按钮区(单独一行) ------
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("save tag"))
SaveLogTag();
if (GUILayout.Button("copy log"))
CopyLog();
if (GUILayout.Button("Clear Logs"))
ClearLogs();
EditorGUILayout.EndHorizontal();
}
private void ReadTagConfig()
{
allTags?.Clear();
var path = tagPath;
path = CommonUtils.GetLinuxPath(path);
var content = File.ReadAllText(path);
allTags = JsonConvert.DeserializeObject<Dictionary<string, ShowControlInfo>>(content);
}
void CopyLog()
{
StringBuilder sb = new();
var logs = LogHelper.Instance.logs;
foreach(var log in logs)
{
if(allTags[log.moduleName].isShowLog)
{
sb.AppendLine(log.ToString());
}
}

        GUIUtility.systemCopyBuffer = sb.ToString();

    }

    private string tagPath = System.Environment.CurrentDirectory + "/environment/log.json";

private void SaveLogTag()
{
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented, // 格式化输出,使生成的 JSON 有缩进
};
var json = JsonConvert.SerializeObject(allTags, settings);
var path = tagPath;
path = CommonUtils.GetLinuxPath(path);
EasyUseEditorFuns.CreateDir(path);
File.WriteAllText(path, json);
this.ShowNotification(new GUIContent($"保存日志tag成功{path}"));
}    


// 清空日志的方法
private void ClearLogs()
{
LogHelper.Instance.logs?.Clear();
}
}
#endif

提示:

不一定所有人都用得上这个功能,在接入的时候需要考虑开关,比如EditorPrefs 存储一个开关的key。例如参考以下截图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/91570.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/91570.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenGL Camera

一. lookAt函数的参数含义glm::mat4 view glm::lookAt(cameraPos, // 相机在世界坐标系中的位置&#xff08;任意值&#xff09;cameraPos cameraFront, // 相机看向的目标点&#xff08;位置朝向&#xff09;cameraUp // 相机的"上方向"&#xff08;通…

Android RTMP推送|轻量级RTSP服务同屏实践:屏幕+音频+录像全链路落地方案

一、背景&#xff1a;从“移动终端”到“远程协作节点”&#xff0c;同屏音频录像为何成刚需&#xff1f; 在数字化办公、智慧医疗与远程教育等快速发展的推动下&#xff0c;手机作为随身终端&#xff0c;已不再只是“内容接收者”&#xff0c;而逐步成为远程信息发布与可视化…

NLP 和 LLM 区别、对比 和关系

理解自然语言处理(NLP)和大语言模型(LLM)的区别、对比和关系对于把握现代人工智能的发展非常重要。以下是清晰的分析: 核心定义 NLP (Natural Language Processing - 自然语言处理): 是什么: 一个广阔的计算机科学和人工智能子领域,致力于让计算机能够理解、解释、操作…

Altium 移除在原理图之外的元器件

Altium新手&#xff0c;最近在画原理图的时候&#xff0c;遇见了这种不小心拖到界面外的元器件&#xff0c;发现拖不回来了了&#xff0c;查阅了一下&#xff0c;总结在这里 官方推荐的方法----------------使用“SCH List”面板删除 链接&#xff1a;如何移除在原理图之外的元…

【Linux我做主】细说环境变量

Linux环境变量Linux环境变量github地址前言1. 基本概念环境变量的本质2. 认识常见的环境变量PATH查看PATH修改PATHHOMESHELL其他常见环境变量PWD与OLDPWDLOGNAME与USERSSH_TTY由环境变量理解权限使用系统调用获取环境变量理解权限3. 总结什么是环境变量3. 命令行参数和环境变量…

leecode-15 三数之和

我的解法&#xff08;不是完全解309/314&#xff09;我的思路是定义一个left和一个right&#xff0c;然后在向集合里去查询&#xff0c;看看有没有除了nums[left]&#xff0c;和nums[right]的第三个元素&#xff0c;把这个问题转换为一个遍历查找问题 利用List.contains()方法来…

精通分类:解析Scikit-learn中的KNN、朴素贝叶斯与决策树(含随机森林)

在机器学习领域&#xff0c;分类任务占据核心地位。Scikit-learn作为Python的机器学习利器&#xff0c;提供了丰富高效的分类算法。现在进行初步探讨三种经典算法&#xff1a;K最近邻&#xff08;KNN&#xff09;、朴素贝叶斯&#xff08;Naive Bayes&#xff09;和决策树&…

Galaxea机器人由星海图人工智能科技有限公司研发的高性能仿人形机器人

Galaxea机器人是由星海图人工智能科技有限公司研发的高性能仿人形机器人&#xff0c;具有多种型号&#xff0c;包括Galaxea R1和Galaxea R1 Pro。以下是关于Galaxea机器人的详细介绍&#xff1a; GitHub官网 产品特点 高自由度设计&#xff1a;Galaxea R1是一款全尺寸仿人型机…

python基础:用户输入和 while 循环

一、input() 函数的工作原理input() 函数让程序暂停运行&#xff0c;等待用户输入一些文本。获取用户输入后&#xff0c;Python 将其赋给一个变量&#xff0c;以便使用。message input("Tell me something, and I will repeat it back to you: ") print(message) 结…

开启云服务器mysql本地连接(is not allowed to connect to this mysql server)

is not allowed to connect tothis mmysql server 阿里云上安装的mysql&#xff0c;发现用本地电脑的navicat链接不上。通过了解知道了原因&#xff0c;小二在此写了一篇&#xff0c;省的以后自己在碰到。 错误如图。 aHR0cHM6Ly9pbWcyMDE4LmNuYmxvZ3MuY29tL2Jsb2cvMTU4MTU1My8…

电脑的时间同步电池坏掉了,每次开机都要调整时间

电脑的时间同步的电池没电了&#xff0c;每天开机时间都不对&#xff0c;要打开时间同步按钮来设置时间解决方案1.找到这个设置并打开&#xff0c;实际上&#xff0c;要打开这个界面&#xff0c;时间才会同步&#xff0c;可能是我的电脑原因&#xff0c;所以我没办法打开这个就…

mycat在游戏中的使用场景(邮件表,mysql集群,而不是邮件服)

其实还有一种是SharingJDBC&#xff0c;而且之间在B站的同学也是说用这个&#xff0c;但是我们目前项目邮件中用的却是: mycat&#xff0c;为什么呢&#xff1f;mycat其实是中间件&#xff0c;是需要独立部署的&#xff0c;是数据库服务器这块的代理&#xff0c;在应用层的话很…

TP-Link Archer C50路由器曝安全漏洞,硬编码DES密钥可解密敏感配置

漏洞概述CERT协调中心&#xff08;CERT/CC&#xff09;发布安全公告&#xff0c;披露TP-Link Archer C50路由器存在编号为CVE-2025-6982的漏洞。该漏洞源于路由器固件中使用了硬编码的DES&#xff08;数据加密标准&#xff09;解密密钥&#xff0c;这一设计缺陷使大量家庭和小型…

番茄项目3:完成了项目的数据库设计

今天抽了会时间设计了下表结构&#xff0c;并选定的使用的数据库&#xff0c;经过调查&#xff0c;我决定还是把数据存在数据库中&#xff0c;因为写SQL是我擅长的。 最终我选择使用python自带的sqlite来实现这个工具&#xff0c;具体建表语句如下&#xff1a; 基于AI生成&…

11、read_object_model_3d 读取点云

个人理解 read_object_model_3d 这个Halcon算子中的xyz_map_width这个参数设置的目的就是,把读取的点云数据中每一个点的XYZ坐标,生成一个对应的二维图像,其中图像中的坐标值就对应每一个点的索引坐标,而图像中的灰度值就对应xyz坐标??(因为得到的是三通道图像)!!并且根…

【人工智能-17】机器学习:KNN算法、模型选择和调优、朴素贝叶斯分类

上一期【人工智能-16】机器学习&#xff1a;概念、工具介绍、数据集、特征工程 文章目录一 、KNN算法1. 应用理由2. 原理核心&#xff1a;距离度量 多数投票/平均3. 优点和缺点二、模型选择和调优1.使用理由2.原理核心&#xff1a;数据划分与性能平均3.超参数搜索4. 应用场景总…

关于继承的一些知识(C++)

当我们想要设计几个类分别记录老师&#xff0c;学生的个人信息时会发现&#xff0c;像姓名、地址、身份证号、电话等等记录基础信息的成员变量是都具有的&#xff0c;重复定义会显得冗余&#xff0c;但同时它们两者又具有不同的记录信息的成员变量&#xff0c;像学生需要记录学…

永磁同步电机无速度算法--脉振方波注入法

一、原理介绍为了实现表贴式永磁电机的低速运行&#xff0c;研究一种基于高频方波测试信号注入的无位置零低速传感器控制策略。选取注入到观测直轴的脉振高频方波信号&#xff0c; 该信号注入方案可以有效避免旋转信号注入法在转子交轴分量引起转矩脉动&#xff0c; 提高系统的…

VSCode Python 与 C++ 联合调试配置指南

VSCode Python 与 C 联合调试配置指南 为了实现 Python 与 C 的联合调试&#xff0c;需要正确配置 launch.json 文件&#xff0c;具体配置如下&#xff1a; {// IntelliSense 支持查看属性描述// 更多信息请参考: https://go.microsoft.com/fwlink/?linkid830387"version…

stm32和freeRtos的can总线

STM32内置bxCAN外设&#xff08;CAN控制器、拓展CAN&#xff09;&#xff0c;支持CAN2.0A和2.0B(全部的CAN)&#xff0c;可以自动发送CAN报文和按照过滤器自动接收指定CAN报文&#xff0c;程序只需处理报文数据而无需关注总线的电平细节波特率最高可达1兆位/秒&#xff0c;高速…