马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:uo_eventlog 事件日志读取全解 —— 一个方法名三重重载、静默降级与截断判据(PB10 基准 · PB12.5 实测)
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-10-13;当次核验的 PBIDEA 核心运行库:18,133,504 字节 / 2572 个导出,md5 4437a43b3e40…(PB 12.5 轨为 18,129,920 字节 / 2548 个导出,md5 123f53b12c49…)。
- 版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
- 源码核验:随版发布的源码导出目录共 8 个库、575 个 .sru,本文所引 uo_eventlog 声明即取自其中的 websuite.pbl 导出源码。
- 适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
- PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。 阅读说明
- 适用版本:一律以顶部【版本声明】为准;PB 侧写法 PB 8 及以上通用,本文以 PowerBuilder 10 为语法基准,代码在 PB 10 与 PB 12.5 两条轨道上各自实机跑通并逐字节核对读数。
- 涉及对象:uo_eventlog(PBIDEA 自定义非可视对象,随 PBIDEA 主运行库一起分发)。
- 支持数据库:本文不涉及任何数据库。
- 操作系统与环境要求:Windows XP 及以上;需安装 PBIDEA 运行库(PbIdea.dll 必须在可执行文件同级目录或 PATH 中,否则 create uo_eventlog 直接抛 PB runtime error: Error opening DLL library PbIdea.dll,本文实测复现过这一条);PB 侧需引入 PBIDEA 的 websuite.pbl。
- 难度系数:★★☆☆☆(调用本身极其简单,只有 1 个方法名;但「读不全」和「日志名被静默吞掉」两个坑会安静地给你错数据,第四、五节必须看)
- 前置知识:会 create / destroy 非可视对象、会用 uo_json 的 Get 取值即可。
- 其它阅读说明:本文所有读数都来自 PB10 与 PB12.5 双轨实机运行,不是文档转述。凡是「实测」二字后面跟数字的,都是本机跑出来的原值;凡是本机没测到的边界,都会明确写「未实测」。
一、先说结论:这个对象能做什么、不能做什么
uo_eventlog 是 PBIDEA 用来读 Windows 事件日志的封装。它小得出人意料——整个对象对外只有一个方法名:EventLogToJson,带三个重载。除这三条签名外,PowerScript 侧没有任何其它公开方法。
先把能力边界划清楚,这决定了后面所有写法:
| 能力 | 实机结果 | 说明 | | 读取整本日志(不带过滤) | ⚠️ 只给「最近约 1 万条」 | 不是全量,见第五节 | | 按单个事件 ID 过滤 | ✅ 可用 | 但参数是 int,16 位,见第六节 | | 按事件 ID 数组过滤 | ✅ 可用 | 同上受 16 位限制 | | 按时间区间过滤 | ❌ 无参数 | 三个重载都没有时间参数 | | 分页 / 分批取全量 | ❌ 无参数 | 没有任何 offset / page 概念 | | 返回失败原因 | ❌ 只有 boolean | 想区分「日志名错」和「没记录」只能自己造指纹 | | 写入 / 清除 / 备份日志 | ❌ 没有方法 | 纯只读,一个写方法都没有 | | 拿到事件正文(Message / Description) | ❌ 返回结构里没有 | 只有 7 个字段,见第三节 | | 非法日志名报错 | ❌ 不报错,静默回落到 Application | 全文最危险的坑,见第四节 |
★一句话总结:它是「读取器」,不是「日志管理器」。 你能做的只有「按名字把一批事件取成 JSON」,剩下的判断(读全了没有、名字对不对、级别怎么算)全部要自己写。
它是 from nonvisualobject 的 NVO、非 autoinstantiate,必须自己 create / destroy:
- uo_eventlog luo_el
- luo_el = create uo_eventlog
- // ... 使用 ...
- destroy luo_el
复制代码
二、签名全貌:1 个方法名、3 条签名
下面三条是从 PBIDEA 随版源码 uo_eventlog.sru 的 type prototypes 段原样抄下来的,一条不多、一条不少:
- function boolean EventLogToJson(readonly string name, readonly uo_json jsLog) system library "pbidea.dll" alias for "elEventLogToJson"
- function boolean EventLogToJson(readonly string name, readonly uo_json jsLog, int filter_id) system library "pbidea.dll" alias for "elEventLogToJson"
- function boolean EventLogToJson(readonly string name, readonly uo_json jsLog, int filter_ids[]) system library "pbidea.dll" alias for "elEventLogToJson"
复制代码
三条都指向同一个导出函数 elEventLogToJson,靠参数个数与类型区分重载。
参数语义:
| 参数 | 类型 | 说明 | | name | readonly string | 日志名。支持英文名与其中文别名,大小写不敏感(实测) | | jsLog | readonly uo_json | 结果容器。声明是 readonly,但函数是往里写数据的——实测传入的 uo_json 会被整体填充 | | filter_id | int | 只保留 eventID 等于该值的事件。int 是 16 位有符号,这是本文第二个大坑,见第六节 | | filter_ids | int 数组 | 保留 eventID 落在数组里的事件;一次可以给多个 |
关于 readonly:这是 PBIDEA 源码声明的写法,不影响你传一个普通 uo_json 进去。实测三类传参都正常:传已有的 uo_json 实例、传刚 create 的空实例,行为一致。
返回值的真实含义(实测,不是文档说法):
| name 取值 | 返回 | 结果条数 | | 合法日志名('Application' / 'System' / 'Security' …) | true | 实际条数 | | 非法日志名('NoSuchLogXYZ') | true | 非 0(回落到 Application) | | 'Windows PowerShell'(本机真实存在但组件读不到) | true | 0 | | 合法名但该 ID 无匹配(filter_id = 999999) | true | 0 | | 空字符串 '' | false | 0 |
★注意倒数第二行与第三行:返回 true 不等于「读到了东西」。整张表里唯一能拿到 false 的写法是空名字。所以 EventLogToJson 的返回值只能用来判断「参数名是不是完全没给」,不能用来判断成败。
三、返回结构:正好 7 个字段,顺序固定
jsLog 是一个数组型 uo_json:顶层是数组,每个元素是一个对象。用 ArrCount() 取条数,用 Get(下标, 子对象) 取第 n 条。
单条事件的结构实测如下(用 Get(nIndex, ref strKey, ref any val, ref strType) 逐项读出类型):
| 序号 | 字段名 | JSON 类型 | 含义 | 实测样例 | | 1 | xh | number | 本次结果里的序号,1 基,永远从 1 开始 | 1 | | 2 | no | number | 事件记录号(Windows 事件索引),越大越新 | 26544 | | 3 | time | string | 发生时间,格式 YYYY-MM-DD HH:MM:SS | 2026-10-13 06:58:44 | | 4 | eventID | number | 事件 ID | 16384 | | 5 | type | number | 级别码 | 4 | | 6 | typeName | string | 级别中文名 | 信息 | | 7 | source | string | 提供程序(写日志的那个组件) | Software Protection Platform Service |
没有的字段(重要):没有 Message、没有 Description、没有 User、没有 Category、没有 Task、没有 Keywords、没有 Computer。
也就是说:你拿得到「谁、什么时候、什么级别、什么 ID」,但拿不到「发生了什么事」的正文。 如果你需要事件描述文字,必须自己按 source + eventID 去建映射表,或者绕开这个组件用 Win32 API 自己读。
三个结构上的实测事实,写代码时可以直接依赖:
- xh 严格递增(System 日志 5878 条逐条校验通过)。
- no 严格递减(同上),因为顺序就是「从最新往回」。
- typeName 恒非空(同上),不需要做空值兜底。
取字段的写法(注意 Get 的第二参是引用出参,变量必须先声明好类型):
- uo_json luo_item
- string ls_no
- string ls_time
- string ls_id
- ajs_root.Get(1, luo_item) // 取第 1 条(最新那条)
- luo_item.Get('no', ls_no) // 记录号
- luo_item.Get('time', ls_time) // 时间
- luo_item.Get('eventID', ls_id) // 事件 ID
复制代码★一个容易忽略的细节:Get 出来的数值字段是按 string 接的(实测把 no / eventID / type 取到 string 变量里都能正常拿到 '26544'、'16384'、'4')。要比较大小就自己 Long() 一次。不要直接用字符串比较,原因见第七节那个 string(0) 的坑。
四、★最大的坑:非法日志名静默降级到 Application
这是本文最值钱的一条实测结论,也是我在写正文前做签名探测时意外撞到的。
现象:把一个根本不存在的日志名喂进去,它不报错、不返回 false、也不返回空,而是安静地给你 Application 日志的内容。
实测三组读数:
| 调用 | 返回 | 条数 | | EventLogToJson('Application', js) | true | 10000 | | EventLogToJson('NoSuchLogXYZ', js) | true | 10000 | | EventLogToJson('System', js) | true | 5878 |
'NoSuchLogXYZ' 的条数与 'Application' 完全相同。光看条数还不够,本轮进一步比对了「首条的记录号 + source + 时间」三处指纹,三处全部相同——这才确认它不是「恰好条数一样」,而是真的回落到同一本日志。
更隐蔽的一点:连读两本不同的日志之后,再喂非法名,拿到的仍然是上一次那本。实测序列是:
- 系统日志 = 5878 条 → 非法名 = 10000 条(= Application) → 系统日志 = 5878 条
复制代码
也就是说它的回落目标不是固定的「永远是 Application」,而是上一次成功读取的那本日志。
唯一的例外是空名字:EventLogToJson('', js) 返回 false、条数 0。这是全部实测里唯一能拿到 false 的写法。
还有一个反例是「存在但读不到」:本机确实有 Windows PowerShell 这本日志(用 PowerShell 的 Get-EventLog -LogName 'Windows PowerShell' 能列出 4600 多条),但组件读它返回 true 且条数为 0,同进程连续两次都是 0,稳定可重现。也就是说条数 0 也可能是「日志存在但我读不到」,而不是「日志是空的」。
工程结论:name 这个参数,你必须自己保证它是对的。任何来自配置、界面下拉框、用户输入的日志名,都要先在允许列表里过一遍。写成方法就是这样:
- public function boolean of_name_ok (string as_log);
- // as_log 待校验的日志名
- // 返回 true = 在本组件确认可读的日志名白名单内
- // 备注 非法名会被静默回落到 Application,所以白名单必须由调用方自己守住
- string ls_allowed
- string ls_use
- ls_allowed = '|application|system|security|setup|'
- ls_use = Lower(Trim(as_log))
- if Pos(ls_allowed, '|' + ls_use + '|') = 0 then
- return false
- end if
- return true
- end function
复制代码
实测(双轨一致):Application → true、system → true(小写也认)、NoSuchLogXYZ → false、空串 → false。这个方法本体与本文其余代码一起进了第九节的工具类,双轨实机跑通。
而如果你一定要「读到什么就信什么」,那就必须加指纹校验:比对「首条 no + source + time」是否与期望那本日志一致。本文第九节的 of_check_log 就是这个思路——它会在读完之后再读一次 Application,用指纹比对来自证「我刚才读的是不是 Application」。
五、★读不全:它不是「全量读取」,而是「最近约 1 万条」
第二危险的一条。EventLogToJson 不带过滤读取时,返回的不是整本日志。
实测对照(PB 侧读数 vs 用 PowerShell 的 Get-EventLog 取到的真实总量):
| 日志 | 真实总条数 | PB 实读条数 | 是否完整 | | System | 5878 | 5878 | ✅ 完整 | | Application | 26546 | 10000 | ❌ 截断 | | Security | 28340 | 10006 ~ 10009 | ❌ 截断 |
规律清楚:当总量小于约 1 万条时读到全部;超过就只给最近约 1 万条。
关键在于最后那句「约」。同一份二进制、同一条命令、同一轨道,在 06:53 ~ 07:54 这一小时里我读到过这些值:
| 时间 | Application 读数 | 首条记录号 | | 06:53 | 10035 | 26544 | | 07:17 | 10037 | 26544 | | 07:26 起(连测 3 次) | 10000 | 26546 | | 07:54 | 10002 | 26548 |
同一时间窗里 Security 的读数也在 10005 ~ 10009 之间浮动。
★所以:「上限约 1 万条」是量级,不是可以依赖的常量。 你在不同机器、不同日志内容上拿到的值都可能不同。任何「条数等于某个期望值就算读全」的代码,都是不可靠的。
那么怎么判断读全了没有?用这个公式
当且仅当 ArrCount() = Number(首条的 no) 时,这次读到的是完整日志。
原理很简单:no 是 Windows 事件日志的全局记录号,从日志建立开始单调递增、永不回退。如果本次读取没有被截断,那么「最新的那条记录号」就等于「日志里的总条数」;一旦被截断,首条的 no 就会大于本次返回的条数。
实机验证:
| 日志 | 首条 no | 返回条数 | 判定 | 与真实情况是否相符 | | System | 5878 | 5878 | COMPLETE | ✅ 相符(真实 5878) | | Application | 26546 | 10000 | TRUNCATED | ✅ 相符(真实 26546) |
这个判据的好处是:它不依赖「1 万」这个数字。上限改成 5 万也好、版本升级后取消上限也好,判据本身依然成立。
一个能直接用的体检方法
- public function string of_check_log (string as_log);
- // as_log 要体检的日志名
- // 返回 COMPLETE / TRUNCATED / EMPTY 之一,附条数与首条记录号
- //
- // 判据:Windows 事件日志的记录号 no 全局单调递增且不回退,
- // 未被截断时「最新一条的 no」恰好等于「日志总条数」。
- uo_eventlog luo_el
- uo_json luo_js
- uo_json luo_item
- string ls_no
- long ll_cnt
- boolean lb_ret
- luo_el = create uo_eventlog
- luo_js = create uo_json
- lb_ret = luo_el.EventLogToJson(as_log, luo_js)
- ll_cnt = luo_js.ArrCount()
- if lb_ret = false or ll_cnt = 0 then
- destroy luo_js
- destroy luo_el
- return 'EMPTY rows=' + string(ll_cnt)
- end if
- luo_js.Get(1, luo_item)
- luo_item.Get('no', ls_no)
- destroy luo_js
- destroy luo_el
- if Long(ls_no) = ll_cnt then
- return 'COMPLETE rows=' + string(ll_cnt) + ' headNo=' + ls_no
- end if
- return 'TRUNCATED rows=' + string(ll_cnt) + ' headNo=' + ls_no
- end function
复制代码
实测输出:
- Application : TRUNCATED rows=10000 headNo=26546
- System : COMPLETE rows=5878 headNo=5878
复制代码
顺带一提:读取本身几乎不花时间
用墙钟量过一轮(同一个进程,只改读取内容):
| 场景 | 墙钟耗时 | | 基线(只启动 PBVM,不读日志) | 7.79 秒 | | 读 System(5878 条) | 8.44 秒 | | 读 Application(1 万条) | 8.10 秒 | | 读带过滤(6005) | 8.14 秒 |
大部分时间花在进程启动上,读日志本身不到 1 秒。 所以「多读几次换正确性」在性能上是划算的——本文第九节的指纹自证就多读了一次 Application,代价可以忽略。
六、过滤读取:int 是 16 位,32768 以上的事件 ID 永远读不到
三条签名里有两条带过滤参数,参数类型都是 int。在 PowerScript 里 int 是16 位有符号整数,范围 −32768 ~ 32767。这不是一个可以忽略的实现细节,它直接决定了「哪些事件 ID 能查、哪些永远查不到」。
实测:越界的过滤值全部静默变成空集
在 Application 日志上扫了一圈边界:
| filter_id | 返回 | 结果条数 | 说明 | | 0 | true | 400 | 真有一批 eventID = 0 的记录 | | 16384 | true | 10009 | 正常命中 | | 32767 | true | 0 | 上边界 | | 32768 | true | 0 | 越界,静默空集 | | 40000 | true | 0 | 越界,静默空集 |
再验证一次「越界值到底被怎么处理」:把 40000 装进 int 变量再打印出来,得到的是 -25536(40000 − 65536 的二进制补码解释),import 0 错、rebuild 0 错、运行不报任何错、结果就是 0 条。用 int 数组装 40000 同样得到 -25536、同样 0 条;把字面量 40000 直接写在调用里也一样是 0 条。
★所以:「事件 ID 大于 32767 的记录,用这个组件永远过滤不出来」,而且它一个字都不提示你。 常见的高位 ID(各类 40000+ 的应用日志 ID、部分审核事件)全部落在这一侧。想查它们,只能用不带过滤的读取(受 1 万条上限约束),或者自己用 Win32 API 读。
顺带补一个「传参类型放宽」的实测:把一个 long 变量直接传给 int 参数,编译器不报错,运行时按低位取值——传 6005(在范围内)能正确拿到 18 条。也就是说编译期给不了你任何越界保护,int 这个类型是唯一的约束。
过滤路径的其它实测行为
| 场景 | 实测结果 | | filter_id = 999999(合法 int、但日志里没有这个 ID) | 返回 true、0 条 | | 过滤 System 日志 eventID = 6005 | 18 条 | | 手工遍历 System 数 eventID = 6005 的条数 | 18 条 → 与过滤结果完全一致 | | 过滤 System 日志 ID 数组 {6005, 6006} | 32 条(6005 的 18 + 6006 的 14) | | 过滤后的 xh | 重新从 1 编号(1、2、3…) | | 过滤后的 no | 保持原值(5760、4083、3721…),所以过滤结果的 no 是不连续的 | | 带过滤的条数 | 也会浮动(Security 同一 ID 两次读到 10007 / 10009) |
★注意最后两行合起来看:过滤结果里的 xh 是「结果内序号」,no 是「日志内位置」,两者不是一回事。 判断完整性时用的是 no,别拿 xh 去比对。
另外,filter = 0 是个特例:它返回 400 条,比不带过滤时的读数(10000)少很多,却比「手工数出来的 eventID = 0 条数」多——手工数得到 118 条,过滤得到 400 条。这是口径差异:过滤走的是日志自己的索引,手工数的是「落在那 1 万条窗口里的 0 号事件」。两者都对,只是问的不是同一个问题。
而 0 号事件的手工统计还有一个隐藏陷阱,见下一节。
七、级别映射:type 与 typeName,以及 string(0) 的坑
type 是数值级别码,typeName 是它的中文名。实测对照(全部取自真实日志、typeName 逐项确证):
| type | typeName | 说明 | | 0 | 未知 | 本机 Application 里真有一批 eventID = 258、type = 0 的记录 | | 1 | 错误 | | | 2 | 警告 | 实测 System 里 eventID = 10016 的正是这一类 | | 4 | 信息 | 最常见,Application 里 1 万条中有 9927 条是这个 | | 8 | 成功审核 | 实测 Security 里 eventID = 4798 | | 16 | 失败审核 | 实测 Security 里 eventID = 4625 |
本机 Application 日志(读数 10000 条)的级别分布实测为:
- rows=10000 err=81 warn=0 info=9890
复制代码
★注意「三类之和」不等于总条数:81 + 0 + 9890 = 9971,比 10000 少 29 条。这 29 条既不是错误、不是警告、也不是信息——它们是 type = 0(未知)以及其它未纳入统计的级别码。所以如果你要算「错误率 = 错误数 ÷ 总条数」,分母是 10000,而分子只覆盖三种级别,统计口径必须自己说清楚,否则这个比率天生偏小。
eventID = 0 怎么统计:两种写法实测等效
Application 日志里真有一批 eventID = 0 的记录,统计它们时你会面临一个选择:把 eventID 取到字符串里跟 '0' 比,还是转成数值跟 0 比。
网上流传的一种说法是「PowerScript 的 string(0) 返回空串,所以字符串比较会漏掉 0 值」。本机没有复现出这条——PB10 与 PB12.5 双轨实测 string(0) 返回的都是 '0'(长度 1):
- string(0) 长度 = 1,值就是 0
- String(0,'0') 长度 = 1,值就是 0
- ls_plain = '0' 比较结果 = true
复制代码
两轨读数逐字节相同。进一步在真实的 Application 结果上对照两种统计写法:
| 写法 | 统计出的 eventID = 0 条数 | | 字符串比较:ls_id = '0' | 117 | | 数值比较:Long(ls_id) = 0 | 117 |
逐条一致。 所以这一处你不用担心兼容性,两种写法都可以。本文工具类统一用数值比较,理由是级别判定(type)本来就要转数值,风格上保持一致:
- string ls_id
- long ll_v
- luo_item.Get('eventID', ls_id)
- ll_v = Long(ls_id)
- if ll_v = 0 then
- ll_zero = ll_zero + 1
- end if
复制代码顺带交代一句边界:string(0) 这一条只在 PB10 与 PB12.5 两条轨道上验过,其它 PB 版本未实测。如果你在更老的 PB 上遇到「0 值统计少了」,可以在自己的环境里用上面那行探针复验一次,成本极低。
八、★uo_json 的子对象不能跨对象传参,会直接崩进程
这一条不是 uo_eventlog 的问题,是 uo_json 的问题,但因为读事件日志必须用它,所以放在这里。
现象:把 ajs_root.Get(i, luo_item) 取到的子对象,作为参数传给另一个函数——进程直接崩溃,exit code 3221225477(0xC0000005,访问违例),没有任何 PowerBuilder 错误信息,TRY-CATCH 也兜不住(它不是异常,是访问违例)。
隔离过程(一类一方法、一例一次独立运行):
| 用例 | 内容 | 结果 | | p34 | 父对象内 Get 出子对象并读字段 | ✅ PASS | | p35 | 把子对象按值传给另一个函数 | ❌ 崩(0xC0000005) | | p36 | 把子对象按 readonly 传给另一个函数 | ❌ 崩 | | p37 | 把根对象传给另一个函数 | ✅ PASS | | p38 | 传根对象 + 下标,在被调方内部 Get | ✅ PASS |
★结论:uo_json 只有「根对象」可以跨对象传递,子对象一旦离开产生它的那个对象,就是雷。
所以工具类的签名应该这样设计——永远传根对象 + 下标,不要传子对象:
- // ❌ 不要这样:接收方拿到的子对象会崩
- public function string of_item_line (uo_json ajs_item)
- // ✅ 要这样:传根 + 下标,接收方自己 Get
- public function string of_item_line (uo_json ajs_root, long al_index);
复制代码
对应实现:
- public function string of_item_line (uo_json ajs_root, long al_index);
- // ajs_root 事件日志根对象(必须传根对象,不能传 Get() 出来的子对象)
- // al_index 结果内序号,1 基,范围 1 ~ ajs_root.ArrCount()
- // 返回 xh | no | time | id | type | typeName | source
- uo_json luo_item
- string ls_xh
- string ls_no
- string ls_time
- string ls_id
- string ls_type
- string ls_tn
- string ls_src
- ajs_root.Get(al_index, luo_item)
- luo_item.Get('xh', ls_xh)
- luo_item.Get('no', ls_no)
- luo_item.Get('time', ls_time)
- luo_item.Get('eventID', ls_id)
- luo_item.Get('type', ls_type)
- luo_item.Get('typeName', ls_tn)
- luo_item.Get('source', ls_src)
- return ls_xh + ' | ' + ls_no + ' | ' + ls_time + ' | id=' + ls_id + ' | type=' + ls_type + ' | ' + ls_tn + ' | ' + ls_src
- end function
复制代码
九、封装成一个能直接用的工具类
把上面所有结论收拢,得到一个 nvo_eventlog_demo,7 个方法,覆盖「日志名校验、读、按 ID 读、按 ID 数组读、取单行、统计级别、体检」七件事。
9.1 日志名白名单校验
第四节给出的 of_name_ok 就是这一件,直接收进工具类:
- public function boolean of_name_ok (string as_log);
- // as_log 待校验的日志名
- // 返回 true = 在本组件确认可读的日志名白名单内
- // 备注 非法名会被静默回落到 Application,所以白名单必须由调用方自己守住
- string ls_allowed
- string ls_use
- ls_allowed = '|application|system|security|setup|'
- ls_use = Lower(Trim(as_log))
- if Pos(ls_allowed, '|' + ls_use + '|') = 0 then
- return false
- end if
- return true
- end function
复制代码
实测:Application → true、system → true(小写也认)、NoSuchLogXYZ → false、空串 → false,双轨一致。
9.2 基础读取(含指纹摘要)
- public function boolean of_read_basic (string as_log, ref uo_json ajs_out, ref string as_info);
- // as_log 要读取的日志名,必须自己保证合法(非法名会静默回落到上一本日志)
- // ajs_out 输出:事件日志结果数组,调用方负责 create 和 destroy
- // as_info 输出:本次读取的条数摘要
- uo_eventlog luo_el
- uo_json luo_item
- boolean lb_ret
- long ll_cnt
- string ls_no
- luo_el = create uo_eventlog
- lb_ret = luo_el.EventLogToJson(as_log, ajs_out)
- ll_cnt = ajs_out.ArrCount()
- ls_no = ''
- if ll_cnt > 0 then
- // 取第一条也就是最新一条的记录号,用它和条数比对即可判断是否被截断
- ajs_out.Get(1, luo_item)
- luo_item.Get('no', ls_no)
- end if
- as_info = 'log=' + as_log + ' ret=' + string(lb_ret) + ' rows=' + string(ll_cnt) + ' headNo=' + ls_no
- destroy luo_el
- return lb_ret
- end function
复制代码
9.3 按单个事件 ID 过滤
- public function boolean of_read_by_id (string as_log, long al_eventid, ref uo_json ajs_out, ref string as_info);
- // as_log 日志名
- // al_eventid 只保留该事件 ID 的记录;★必须落在 -32768 ~ 32767 内,超出会静默变空集
- // ajs_out 输出:过滤后的事件数组
- // as_info 输出:摘要
- uo_eventlog luo_el
- boolean lb_ret
- long ll_cnt
- luo_el = create uo_eventlog
- lb_ret = luo_el.EventLogToJson(as_log, ajs_out, al_eventid)
- ll_cnt = ajs_out.ArrCount()
- as_info = 'log=' + as_log + ' eventid=' + string(al_eventid) + ' ret=' + string(lb_ret) + ' rows=' + string(ll_cnt)
- destroy luo_el
- return lb_ret
- end function
复制代码
用它可以取开关机记录——System 日志的 6005 是「事件日志服务已启动」(开机)、6006 是「事件日志服务已停止」(关机):
- long ll_cnt
- string ls_info
- uo_json luo_js
- luo_js = create uo_json
- of_read_by_id('System', 6005, luo_js, ref ls_info) // 实测 18 条
- ll_cnt = luo_js.ArrCount()
- destroy luo_js
复制代码
实测读数:log=System eventid=6005 ret=true rows=18。
9.4 按事件 ID 数组过滤
- public function boolean of_read_by_ids (string as_log, int ai_ids[], ref uo_json ajs_out, ref string as_info);
- // as_log 日志名
- // ai_ids 事件 ID 数组,一次取多个;元素类型是 int,同样受 16 位限制
- // ajs_out 输出:过滤后的事件数组
- // as_info 输出:摘要
- uo_eventlog luo_el
- boolean lb_ret
- long ll_cnt
- long ll_i
- string ls_ids
- luo_el = create uo_eventlog
- lb_ret = luo_el.EventLogToJson(as_log, ajs_out, ai_ids)
- ll_cnt = ajs_out.ArrCount()
- ls_ids = ''
- for ll_i = 1 to UpperBound(ai_ids)
- ls_ids = ls_ids + string(ai_ids[ll_i]) + ','
- next
- as_info = 'log=' + as_log + ' ids=' + ls_ids + ' ret=' + string(lb_ret) + ' rows=' + string(ll_cnt)
- destroy luo_el
- return lb_ret
- end function
复制代码
一次取开机 + 关机:
- int li_ids[2]
- string ls_info
- uo_json luo_js
- li_ids[1] = 6005
- li_ids[2] = 6006
- luo_js = create uo_json
- of_read_by_ids('System', li_ids, luo_js, ref ls_info) // 实测 32 条
- destroy luo_js
复制代码
实测读数:log=System ids=6005,6006, ret=true rows=32。
9.5 按级别统计
- public function string of_level_count (uo_json ajs_root, ref long al_err, ref long al_warn, ref long al_info);
- // ajs_root 事件日志根对象(传根对象,不要传子对象)
- // al_err 输出:type=1 错误条数
- // al_warn 输出:type=2 警告条数
- // al_info 输出:type=4 信息条数
- // 返回 一行摘要
- uo_json luo_item
- string ls_type
- long ll_i
- long ll_cnt
- ll_cnt = ajs_root.ArrCount()
- al_err = 0
- al_warn = 0
- al_info = 0
- for ll_i = 1 to ll_cnt
- ajs_root.Get(ll_i, luo_item)
- ls_type = '0'
- luo_item.Get('type', ls_type)
- if Long(ls_type) = 1 then al_err = al_err + 1
- if Long(ls_type) = 2 then al_warn = al_warn + 1
- if Long(ls_type) = 4 then al_info = al_info + 1
- next
- return 'rows=' + string(ll_cnt) + ' err=' + string(al_err) + ' warn=' + string(al_warn) + ' info=' + string(al_info)
- end function
复制代码
实测(Application,同一批结果内统计):rows=10000 err=81 warn=0 info=9890。
9.6 完整性 + 回落体检
- public function string of_check_log (string as_log);
- // as_log 要体检的日志名
- // 返回 COMPLETE / TRUNCATED / FALLBACK / EMPTY 之一,附条数与首条记录号
- //
- // COMPLETE = 读到全量(首条 no 等于条数)
- // TRUNCATED = 只读到最近一批(首条 no 大于条数)
- // FALLBACK = 读到的其实是 Application(非法名被静默回落)
- // EMPTY = 返回 false 或 0 条
- uo_eventlog luo_el
- uo_json luo_js
- uo_json luo_js2
- uo_json luo_item
- string ls_no
- string ls_xh
- string ls_src
- string ls_time
- string ls_no2
- string ls_src2
- long ll_cnt
- long ll_cnt2
- boolean lb_ret
- luo_el = create uo_eventlog
- luo_js = create uo_json
- lb_ret = luo_el.EventLogToJson(as_log, luo_js)
- ll_cnt = luo_js.ArrCount()
- if lb_ret = false or ll_cnt = 0 then
- destroy luo_js
- destroy luo_el
- return 'EMPTY rows=' + string(ll_cnt) + ' ret=' + string(lb_ret)
- end if
- luo_js.Get(1, luo_item)
- luo_item.Get('no', ls_no)
- luo_item.Get('source', ls_src)
- luo_item.Get('time', ls_time)
- // 读的不是 Application 时,再读一次 Application 做指纹比对,用来识破静默回落
- if Lower(as_log) <> 'application' then
- luo_js2 = create uo_json
- luo_el.EventLogToJson('Application', luo_js2)
- ll_cnt2 = luo_js2.ArrCount()
- if ll_cnt2 > 0 then
- luo_js2.Get(1, luo_item)
- luo_item.Get('no', ls_no2)
- luo_item.Get('source', ls_src2)
- if ls_no2 = ls_no and ls_src2 = ls_src then
- destroy luo_js2
- destroy luo_js
- destroy luo_el
- return 'FALLBACK rows=' + string(ll_cnt) + ' name=' + as_log
- end if
- end if
- destroy luo_js2
- end if
- if Long(ls_no) = ll_cnt then
- destroy luo_js
- destroy luo_el
- return 'COMPLETE rows=' + string(ll_cnt) + ' headNo=' + ls_no
- end if
- luo_js.Get(ll_cnt, luo_item)
- luo_item.Get('xh', ls_xh)
- destroy luo_js
- destroy luo_el
- return 'TRUNCATED rows=' + string(ll_cnt) + ' headNo=' + ls_no + ' tailXh=' + ls_xh + ' headSrc=' + ls_src + ' headTime=' + ls_time
- end function
复制代码
实测输出(同一次运行):
- Application : TRUNCATED rows=10000 headNo=26546 tailXh=10000 headSrc=Software Protection Platform Service headTime=2026-10-13 06:58:44
- System : COMPLETE rows=5878 headNo=5878
- NoSuchLogXYZ : FALLBACK rows=10000 name=NoSuchLogXYZ
复制代码
★最后一行是全文的浓缩:你喂了一个不存在的名字,它给了你 1 万条真实数据、返回 true、一声不响。 没有指纹校验的代码,会把 NoSuchLogXYZ 的核心事件当成 Application 的事件报给用户。
9.7 一个最小窗口
把 7 个方法接到一个窗口上,4 个按钮,每个按钮以 MessageBox 结尾(便于观察):
- // 窗口实例变量(在 declare 区声明)
- // nvo_eventlog_demo inuo_el
- // ---- 窗口 create 事件 ----
- inuo_el = create nvo_eventlog_demo
- // ---- 窗口 destroy 事件 ----
- destroy inuo_el
- // ---- 按钮 1:读取 Application 日志 ----
- uo_json luo_js
- string ls_info
- long ll_cnt
- long ll_i
- luo_js = create uo_json
- inuo_el.of_read_basic('Application', luo_js, ref ls_info)
- ll_cnt = luo_js.ArrCount()
- Parent.mle_output.text = 'of_read_basic -> ' + ls_info + '~r~n'
- for ll_i = 1 to 5
- Parent.mle_output.text = Parent.mle_output.text + inuo_el.of_item_line(luo_js, ll_i) + '~r~n'
- next
- destroy luo_js
- MessageBox('1、读取 Application', ls_info)
- // ---- 按钮 2:按级别统计 ----
- uo_json luo_js
- string ls_info
- string ls_sum
- long ll_err
- long ll_warn
- long ll_info
- luo_js = create uo_json
- inuo_el.of_read_basic('Application', luo_js, ref ls_info)
- ls_sum = inuo_el.of_level_count(luo_js, ref ll_err, ref ll_warn, ref ll_info)
- Parent.mle_output.text = ls_info + '~r~n' + ls_sum
- destroy luo_js
- MessageBox('2、级别统计', '错误=' + string(ll_err) + ' 警告=' + string(ll_warn) + ' 信息=' + string(ll_info))
- // ---- 按钮 3:按 ID 数组过滤 ----
- int li_ids[2]
- uo_json luo_js
- string ls_info
- li_ids[1] = 6005
- li_ids[2] = 6006
- luo_js = create uo_json
- inuo_el.of_read_by_ids('System', li_ids, luo_js, ref ls_info)
- Parent.mle_output.text = ls_info
- destroy luo_js
- MessageBox('3、按 ID 数组过滤', ls_info)
- // ---- 按钮 4:完整性体检 ----
- string ls_1
- string ls_2
- string ls_3
- ls_1 = inuo_el.of_check_log('Application')
- ls_2 = inuo_el.of_check_log('System')
- ls_3 = inuo_el.of_check_log('NoSuchLogXYZ')
- Parent.mle_output.text = 'Application : ' + ls_1 + '~r~n' + 'System : ' + ls_2 + '~r~n' + 'NoSuchLogXYZ : ' + ls_3
- MessageBox('4、完整性体检', Parent.mle_output.text)
复制代码提醒一处 PB 的书写习惯:在控件自己的事件里引用父窗口上的控件,要走 Parent.(上面按钮脚本里的 Parent.mle_output.text 就是这个用法);在窗口自己的脚本里则可以直接写控件名 mle_output.text。写混了不一定报错,但运行时可能拿不到你以为的那个控件。另外窗口 destroy 事件里要 destroy inuo_el,否则工具对象泄漏。
十、坑清单
- ★非法日志名不报错、静默回落到「上一次成功读取的那本日志」。实测 'NoSuchLogXYZ' 返回 true 且条数与 Application 完全相同,no/source/time 三处指纹也全同。必须自己做名字白名单或指纹校验。
- ★返回值 true 不代表读到了东西。'Windows PowerShell' 返回 true 但 0 条;filter_id = 999999 返回 true 但 0 条。整轮实测里只有空名字 '' 能拿到 false。
- ★不带过滤的读取不是全量,只给「最近约 1 万条」。本机 Application 真实 26546 条、只读到 10000 条。
- ★「约 1 万条」不是常量。同一二进制、同一命令,半小时内 Application 从 10035 变到 10000;Security 在 10005 ~ 10009 之间浮动。别用条数判断完整性。
- ★完整性判据只有一个:ArrCount() = Number(首条的 no)。成立即完整,不成立即被截断。该判据与 1 万这个数字无关。
- ★filter_id / filter_ids 是 int,16 位有符号,上限 32767。32768 / 40000 一律静默变空集(返回 true、0 条)。事件 ID 大于 32767 的记录永远过滤不出来。
- ★越界值不会报错,而是按补码被截断:40000 存进 int 得 -25536,import 0 错、rebuild 0 错、运行无错。
- ★long 变量传给 int 参数编译器不报错(实测传 6005 正确命中 18 条),编译期给不了越界保护。
- ★uo_json 子对象不能跨对象传参,会直接崩进程(exit code 3221225477 / 0xC0000005),无任何错误信息、TRY-CATCH 兜不住。只有根对象可以传;要取子对象内容就传「根 + 下标」,在被调方内部 Get。
- string(0) 在 PB10 与 PB12.5 上返回的是 '0'(实测),不是空串;因此「字符串比较」与「数值比较」统计 eventID = 0 的结果逐条一致(117 / 117)。这一处不需要写兼容代码。
- 返回结构只有 7 个字段(xh / no / time / eventID / type / typeName / source),拿不到事件正文,没有 Message / User / Category。
- xh 与 no 不是一回事:xh 是结果内序号(每次从 1 重编),no 是日志内记录号(全局递增、过滤后仍保持原值)。
- 过滤后的 no 不连续,是按记录号从新往旧扫出来的顺序。
- filter = 0 的口径与手工统计不同:过滤得 400 条,手工在那 1 万条窗口里数得 118 条,两者都对,问的不是同一个问题。
- 同一实例反复调用是「替换」不是「追加」:连续两次读同一日志,第一次和第二次条数相同(实测 first=10035 second=10035),旧内容被整体覆盖,不会累积。
- Security 日志的读取需要权限,本机在管理员上下文能读到约 1 万条;普通用户上下文未实测。
- type = 0 的 typeName 是「未知」,不是「错误」。本机 Application 里真有一批 type = 0 的记录(eventID = 258)。
- 缺 PbIdea.dll 会直接抛运行错误(实测把 DLL 移走复现):PB runtime error: Error opening DLL library PbIdea.dll for external function at line 1 in constructor event of object uo_json。要连 Pbidea_cs.dll / SciLexer.dll / WebBrowser.dll 一起放在可执行文件同级目录。
- 日志名大小写不敏感(application / system / SYSTEM 均读取成功),不要在这个方向上浪费时间。
- 中文别名可用(应用程序 / 应用 / 系统 / 安全 / 安装 等),与英文名等价。
十一、PB12.5 差异
本轮一共 23 组关键用例 × 2 条轨道(PB10 / PB12.5)各自独立运行一次,逐条比对输出。
结论:uo_eventlog 的签名、返回值、结构、异常行为在 PB10 与 PB12.5 之间没有发现任何版本差异。 23 例全部 PASS,其中 16 例读数逐字节完全相同;剩余 7 例的差异全部是活日志增长造成的(两次运行之间 Application 日志又新增了几条,读数从 10035 变 10037 之类),不是行为差异。
| 维度 | PB10 | PB12.5 | | type prototypes 段三条声明 | import 0 错 | import 0 错 | | rebuild --type full | 0 错 | 0 错 | | 23 例运行 | 全部 PASS | 全部 PASS | | 逐字节一致 | — | 16 / 23 一致,7 例差异均源于活日志增长 | | 非法名静默回落 | 存在 | 存在 | | 非法 ID 静默空集 | 存在 | 存在 | | int 过滤参数 16 位 | 存在 | 存在 | | uo_json 子对象传参崩溃 | 存在 | 存在 | | 7 个字段结构 | 一致 | 一致 | | string(0) 返回值 | '0' | '0'(一致) | | 字符串比较与数值比较统计 0 值 | 117 / 117 一致 | 117 / 117 一致 |
也有一个与版本无关、但与机器有关的细节要交代清楚:两条轨道挂载的 PBIDEA 运行库是各自版本(PB10 轨 18,133,504 字节 / 2572 导出;PB12.5 轨 18,129,920 字节 / 2548 导出),但两者导出的 elEventLogToJson 行为在本轮全部用例上一致。
十二、实机验证情况
测试环境
| 项 | 值 | | PB10 轨 | import 0 错;rebuild --type full 0 错;PBVM 探测用例 23 例全部 PASS;演示对象 6 例全部 PASS | | PB12.5 轨 | 同上,pbl list 对象齐全(双轨各 4 个对象) | | 探测对象 | 41 个 NVO(每个只放一个方法,一例一次独立运行) | | 关键用例 | 23 组(读全、结构、非法名、非法 ID、filter 边界、int 越界、级别映射、完整性判据等),每例独立成一次进程运行 | | 演示对象 | nvo_eventlog_demo + w_eventlog_demo + a_eventlog_demo,7 个方法 / 4 个按钮功能,双轨各 6 例 | | 双轨比对 | 23 例中 16 例输出逐字节相同,7 例差异全部是活日志增长 | | 运行库放置 | 按轨分别放 PbIdea.dll(两轨版本号见顶部版本声明)+ Pbidea_cs.dll / SciLexer.dll / WebBrowser.dll |
演示对象双轨实机读数(同一次复跑,两轨逐字节一致)
- d_read : log=Application ret=true rows=10002 headNo=26548
- L1 | 26548 | 2026-10-13 07:54:07 | id=16384 | type=4 | 信息 | Software Protection Platform Service
- L2 | 26547 | 2026-10-13 07:53:36 | id=16394 | type=4 | 信息 | Software Protection Platform Service
- L3 | 26546 | 2026-10-13 07:25:09 | id=16384 | type=4 | 信息 | Software Protection Platform Service
- d_id : log=System eventid=6005 ret=true rows=18
- d_ids : log=System ids=6005,6006, ret=true rows=32
- d_level : rows=10002 err=81 warn=0 info=9892
- d_check : Application -> TRUNCATED rows=10002 headNo=26548
- System -> COMPLETE rows=5878 headNo=5878
- NoSuchLogXYZ -> FALLBACK rows=10002 name=NoSuchLogXYZ
- d_name : Application=true system=true NoSuchLogXYZ=false empty=false
复制代码
两个轨道各跑一遍,6 例全部 PASS,且两轨输出逐字节相同(一致 6 / 差异 0)。
顺带说明为什么 d_read 这次是 rows=10002 而不是第五节表里的 10000:因为 Application 日志在这两次运行之间又长了两条。 这正好是第五节的现场演示——读数是活的,判据才是不变的。
新增的补测(写正文时为了钉住边界而补跑)
| 补测项 | 读数 | | filter_id 边界扫描(Application) | 0 → 400 条;16384 → 10009 条;32767 → 0 条;32768 → 0 条;40000 → 0 条 | | int 溢出取证 | 字面量 40000 → 0 条;long 变量 6005 → 18 条;int 变量装 40000 读回 -25536 → 0 条;int 数组同理 | | Application 最大事件 ID 扫描 | 读数内 app_maxID=16394,eventID > 32767 的记录 0 条 | | no 顺序性校验 | System 记录 5878 条逐条校验:xh 严格递增、no 严格递减、typeName 恒非空,三项全真 | | string(0) 行为 | 双轨一致:string(0) 长度为 1、值就是 0;String(0,'0') 同理;等值比较返回 true(即不是空串) | | 0 值统计两写法对照 | 同一批 Application 结果:字符串比较 117 条、数值比较 117 条,逐条一致 | | 日志名白名单校验 | Application → true、system → true、NoSuchLogXYZ → false、空串 → false,双轨一致 | | 读数浮动观察 | 同一二进制、同一命令、同一轨道:Application 在一小时内读到 10035 / 10037 / 10000 / 10002 四个值;Security 在 10005 ~ 10009 之间浮动;System 始终 5878(全量) | | 完整性判据与 PowerShell 对照 | System:判据 COMPLETE / 真实 5878;Application:判据 TRUNCATED / 真实 26546;Security:真实 28340 |
未实测项(明确划出边界)
- ★「约 1 万条」这个上限的确切触发机制未定论。本轮只能确定它是容量级而非精确条数级:同一份二进制、同一条命令,Application 的读数在 30 分钟内从 10035 变到 10000,Security 在 10005 ~ 10009 之间浮动,而 System(5878 条)始终读到全量。是「按字节容量」「按内部缓冲」还是「按扫描预算」,本轮没有拆到组件内部,不作结论。因此本文只给了不依赖该数字的完整性判据。
- ★filter_id 超过 32767 时是否存在「部分可用」的写法未实测。本文只验证了 32767 / 32768 / 40000 三个点全部为 0 条,没有穷举。
- ★Security 日志在非管理员上下文下的可读性未实测。本机全部读数都在管理员上下文取得。
- ★自定义日志(非 Application / System / Security / Setup)未实测。本机没有构造自定义日志来做验证。
- ★未测试远程主机日志。三个签名都没有主机名参数,本文结论只适用于本机。
- ★未测试日志写入 / 清除。该对象根本没有写方法,本轮也没有绕道 Win32 API 去验证。
- ★Get 对数值字段返回字符串的完整行为未穷举。本文只验证了 no / eventID / type 三个字段取成字符串的行为,以及「真值为 0 时写回 '0'」这一条。其它数值字段(含负数、超大值)未逐一验证。
静态检查
- 编码检查:0 错(全部 GBK + CRLF + 无 BOM,首行 $PBExportHeader$ 齐全)。
- 帮助库核对:0 错误、0 提醒。
- 本文所有代码均在 PB10 与 PB12.5 双轨编译通过;运行级结论均来自实机读数。
十三、写在最后
uo_eventlog 是一个「看起来特别简单、用起来特别容易错」的组件。它只有 1 个方法名,你 10 分钟就能把它调通;但它的三个坑没有一个是会报错的:
- 日志名写错——不报错,给你另一本日志的数据;
- 事件 ID 超过 32767——不报错,给你一个空集;
- 日志太长——不报错,给你最近那 1 万条,还长得特别像全量。
三条的共同点是:你会得到一份格式完全正确、内容完全错误的数据。 这比抛异常难查一百倍。
所以我给三条建议:
第一,名字必须先过白名单。 别把日志名当成一个「随便传的字符串」,它是这个组件里唯一一个错了还照样返回 true 的参数。
第二,判断完整性一律用 ArrCount() = Number(首条 no)。 这个判据不依赖任何版本常量,是本文里最应该被抄进项目工具类的一行代码。
第三,写过滤条件前先看一眼事件 ID 的数值。 大于 32767 的,这个组件帮不了你——得换路径。
如果你手上也在用这个对象,欢迎在回帖里说说你遇到过哪些「安静给你错数据」的场景。本文把能测的边界尽量钉死了,剩下没定论的那条(1 万条上限的确切机制)我留在第十二节里,等你来补。 |