使用递归下降文法解析Cognos TM1数据库日志

同事希望了解TM1中不同对象的执行信息,如开始时间和结束时间,时长,成功与否,错误信息等,以便了解执行快慢,分析错误原因。注意到tm1日志中包含了类似这些信息,所以希望通过分析执行日志来实现这个需求。查看了一下TM1日志的构成,借鉴常见程序日志的记录过程,参考python cookbook中递归下降文法,最后使用python来分析。

TM1的日志类似如下

 大致来说每个日志记录行是这样的,

<语句>::= 成分1 + 成分2 + LOG_LEVEL + yyyy-mm-dd hh24:mi:ss.f3 + 日志来源对象 + 描述简短信息 (可能存在换行)

程序中为了匹配一个对象开始执行和结束执行, 尝试分析后发现, 实际是通过PART1和PART2构成了对象执行中的标识。

同时考虑到日志分析时,可能某些对象正在执行之中, 需要在下一次分析中完整记录这些正在执行对象的分析结果,所以引入了savepoint,表示这个时间点之前启动的对象都已经执行完成,类似数据库之中的checkpoint.

具体程序如下

# -*- coding:utf-8 -*-

# 保存分析结果到oracle数据库中,用于琪雯和志勇使用
import cx_Oracle

# 修改为使用logging模块来记录日志
import logging

# 用于打印异常函数栈
import traceback

import datetime
# 计算进程消耗cpu
import time

# 使用哪个正则匹配
import re

# 使用命名元组来操作token
import collections

# 为自定义解码错误处理方法而导入
import codecs

# 使用2分搜索查找ti记录位置
import bisect

# 用于保存启动和执行完成的描述信息,描述信息包含行号和具体信息,使用json.dumps(data,separators=(',',':') )
# json.loads(strJson)来复原
import json

# ===========================================================================
# 设计说明
#
# 目的是分析tm1server.log日志,获取执行ti的时间长短,执行结果,失败原因等
# 考虑日志在不断追加之中,且日志可能被其他人修改, 每天早上执行1次,记录分析程序开始时间,结束时间,消耗cpu,
# 分析开始位置所对应时间,结束位置对应时间,本次分析的保存点对应时间(此时间之前的ti都已经执行完成)
# 对于每个ti执行,记录开始时间,结束时间,时长,执行结果,失败原因

# 程序大致过程如下:
# 1. 按照T_tm1slog中analysis_id排序,查找最后一次执行时的save_point和analysis_id
# 2. 如果为空, 则文件从开始分析,新的分析编号为1,
#    否则analysis_id加1作为新的分析编号,同时文件找到此时间,然后读入下一行,作为开始分析
# 3. 假设不同ti存在并发执行的情况,开始进行ti扫描.假设输入为具备行缓冲的文件,使用基于stream的操作
#    1. 扫描符合ti的行,获取ti名称,判断是开始执行还是执行完成或退出,操作时间,按照不同情况,记录对应的各种数据
#    2. 查找字典结构中此ti名称的分析,
#    3. 如果当前行为开始执行,则在此ti名称的分析list中增加一行,假设同一ti操作日志输出会按照时间排序,且忽略不同线程操作时,由于缓冲导致的
#       输出时间顺序不同情况
#    4. 如果是执行返回,则检查此ti名称的分析list是否为空,若空,则提示错误, 否则list排序中查找前次完成操作的位置,然后在之后的未完成操作
#       中记录完成时间和返回信息,若没有未完成的操作,提示错误
# 4. 从本次操作的数据构造保存到数据库中的数据,并保存
#

# 解析中发现tm1server.log文件存在下面现象:
# 1. 文件中存在乱码,文件编码为utf-8,但存在非utf-8的数据
# 2. 存在单独的0x0d,如47533行,在}之后存在0x0d, 随后才是) 导致增加了1行
#    在以下位置或其附近出现语法错误: '})', 字符位置 215
#

# 对于扫描性能,暂时不考虑,每次分析完整文件
# 2022.12.19

# 由于tm1s数据库服务存在重新启动和异常退出问题,所以仅匹配同次启动的情况
# 需要从服务器启动后开始匹配,如果服务器存在shutdown情况,则此前没有匹配成功的全部不再分析
# 服务器启动标志是:
# TM1.Server   thread TM1UpdateLoggerThread starts with id:
# 服务关闭标志是
# TM1.Server   thread TM1UpdateLoggerThread quits with id:
# 2022.12.20

# 1. TI_UNKNOWN1是这个ti的提示信息,查找对应的ti分析记录,补充到描述结构中
# 2. TI_UNKNOWN2实际是异常退出,可以作为返回代码2来处理
# 3. TI_UNKNOWN3应该是ti的提示信息,但发现仅出现1次,Cube数据导出_B0703损益表, 且应该是代码错误,所以暂时按照原有方式
# 2022.12.21

# 1. 增加ti_process_type,引入执行对象类型TM1.Chore,TM1.Process等,parent_begin_time,记录父对象的启动时间,以便后续使用层级来搜索启动关系
#    增加以便查找父对象实例化之后的实际记录,在目前情况下,实际无法区分同时存在多个实例化的情况
# 2. 增加TM1.Chore结构的处理,以便获取完整启动父子关系
# 3. 分析时,当ti启动时,记录父对象的类型,父对象的名称和实例化时间,同时在父对象的child_nodes中记录子的名称,实例化时间,通过双向方式
#    以便TM1.Chore结束时,将其所属的所有子对象关闭,如果子对象没有对应的结束,则此时,使用单独的状态标识这种现象
# 2022.12.21

# 1. ti层级关系判断时,需要增加用户
# 2022.12.25

# 1. 对日志中显示2次的原因
#    例如 317142行的Process A00系统_更新年度预算实际数 executed by chore A00系统_更新年度预算实际数
#    Process "A02年度预算_B0601、F0502部门费用预算" run from process "A00系统_更新年度预算实际数" by user "R*A00系统_更新年度预算实际数"
# ===========================================================================

# ===========================================================================
# 常量及变量说明
# 日志记录对象
logger=None

# 批量执行控制
BATCH_SIZE=100

# 全面预算 数据库的连接字符串 C1_MIDDLEDATA
BGDB_CONN_STR="C1_MIDDLEDATA/mypasswd@bgdb"

# ===========================================================================
# 正则解析

# 查看tm1sserver.log,发现存在简短描述信息换行的情况,猜测实际文法构成是
# <语句列表>::= <语句>|<语句列表><语句>
# <语句>::= 成分1 + 成分2 + LOG_LEVEL + yyyy-mm-dd hh24:mi:ss.f3 + 日志来源对象 + 描述简短信息 (可能存在换行)
# 若语句不符合上述文法,则需要提示
# 其中
# 成分1  从行开始的数值部分,直到分隔符   用途不知  从数据来看,可能是对应执行线程的某种id,
# 成分2  [] 或[]中间包含数值, 直到分隔符  用途不知 从数据看, 应该是实例id, 如果存在,
# LOG_LEVEL : INFO | ERROR | DEBUG | WARN, 其他信息则需要提示, 正常来说,应该是这样的
# CURR_SOURCE 存在 TM1.Mdx.Interface,TM1.Server等不同情况,所以使用 (?:\.\w+)* 非捕获版本
# 日志来源对象,为下面的一种
# TM1.API
# TM1.Blob
# TM1.Chore
# TM1.Client
# TM1.ClientMessage
# TM1.Cube
# TM1.Cube.Dependency
# TM1.Dimension
# TM1.Lock
# TM1.Mdx.Interface
# TM1.Process
# TM1.Server
# TM1.Server.Config
# TM1.Server.Memory
# TM1.Server.Network
# TM1.SQLAPI
# TM1.Subset
# TM1.TILogOutput
# TM1.Transaction
# TM1.User.Thread
LOG_STATEMENT=r'(?P<PART1>^\d+)' + r'\s+'+ \
              r'\[(?P<PART2>\d*)\]' + r'\s+'+ \
              r'(?P<LOG_LEVEL>INFO|ERROR|DEBUG|WARN)' + r'\s+'+ \
              r'(?P<CURR_TIMESTAMP>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})' + r'\s+'+ \
              r'(?P<CURR_SOURCE>TM1\.\w+(?:\.\w+)*)' + r'\s+' + \
              r'(?P<CURR_DESC>.+?)'+r'$'
# 获取日志语句对应的 编译后正则表达式对象
log_statement = re.compile(LOG_STATEMENT)

# 空行
WS=r'^\s*$'
ws=re.compile(WS)

# 需要从服务器启动后开始匹配,如果服务器存在shutdown情况,则此前没有匹配成功的全部不再分析
# 服务器启动标志是:
# TM1.Server   thread TM1UpdateLoggerThread starts with id:
# 服务关闭标志是
# TM1.Server   thread TM1UpdateLoggerThread quits with id:

SERVER_START=r'thread TM1UpdateLoggerThread starts with id:'
server_start=re.compile(SERVER_START)

SERVER_SHUTDOWN=r'thread TM1UpdateLoggerThread quits with id:'
server_shutdown=re.compile(SERVER_SHUTDOWN)

# TM1.Chore 部分的匹配
# chore的启动,仅1种方式
# 140134112556800   []   INFO   2022-12-05 05:30:01.991   TM1.Chore   Chore "A00系统_更新年度预算实际数" executed by scheduler
CHORE_START=r'^Chore'+r'\s+'+ \
            r'(?P<QUOTATION_MARKS>["\'])?(?P<CHORE_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))'+r'\s+'+ \
            r'executed by scheduler'
chore_start=re.compile(CHORE_START)

# chore的执行完成,实际包含2个步骤
# 140133978339072   []   INFO   2022-12-05 05:34:28.160   TM1.Chore   Chore "A00系统_更新年度预算实际数0330" time = 7466.71 seconds
# 140133978339072   []   INFO   2022-12-05 05:34:41.848   TM1.Chore   Chore "A00系统_更新年度预算实际数0330" finished executing
# 猜测这2者之间的间隔为执行完成后,释放其所使用的各种对象,内存等,所以消耗了13秒
# 使用chore_finish1,chore_finish2来结束它所启动的所有ti,
# 其中chore_finish1设置状态为1,chore_finish2设置状态为0,区分看看是否存在1的情况

CHORE_FINISH1=r'^Chore'+r'\s+'+ \
              r'(?P<QUOTATION_MARKS>["\'])?(?P<CHORE_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))'+r'\s+'+ \
              r'time'+r'\s*=\s*'+ r'(?P<DURATION>\d+\.?\d+)'+r'\s*'+r'seconds'
chore_finish1=re.compile(CHORE_FINISH1)

CHORE_FINISH2=r'^Chore'+r'\s+'+ \
              r'(?P<QUOTATION_MARKS>["\'])?(?P<CHORE_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))'+r'\s+'+ \
              r'finished executing'
chore_finish2=re.compile(CHORE_FINISH2)

# 以下这几种方式可以忽略处理
# TM1.Chore   start wait for next runtime
# TM1.Chore   executing next runtime
# TM1.Chore   Registering chore: A06维度元素导出_组织末级  Start time: 2021/07/28 04:10:00  Frequency: 01:00:00:00
# TM1.Chore   Deactivating chore: A00系统_更新年度预算实际数0330
CHORE_IGNORE1=r'start wait for next runtime'
chore_ignore1=re.compile(CHORE_IGNORE1)

CHORE_IGNORE2=r'executing next runtime'
chore_ignore2=re.compile(CHORE_IGNORE2)

CHORE_IGNORE3=r'Registering chore:'+r'\s+'+r'.*'
chore_ignore3=re.compile(CHORE_IGNORE3)

CHORE_IGNORE4=r'Deactivating chore:'+r'\s+'+r'.*'
chore_ignore4=re.compile(CHORE_IGNORE4)

# ti的启动 存在类似下面3种说明的方式
# Process "A01维度_物料" run from process "A00系统_维度更新_每天0530" by user "R*A00系统_维度更新_每天0530"
# Process A00系统_更新年度预算实际数 executed by chore A00系统_更新年度预算实际数
# Process "生产模块_ti送数_B0205计算分交易类型生产量" executed by user "8037932"
TI_START1=r'^(进程|Process)'+r'\s+'+ \
          r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))'+r'\s+'+ \
          r'run from process'+r'\s+' + \
          r'(?P<QUOTATION_MARKS2>["\'])?(?P<RUN_FROM_PROCESS_NAME>[^"\']+?)(?(QUOTATION_MARKS2)(?P=QUOTATION_MARKS2))'+r'\s+'+ \
          r'by user'+r'\s+' + \
          r'(?P<QUOTATION_MARKS3>["\'])?(?P<EXECUTED_BY_USER>[^"\']+?)(?(QUOTATION_MARKS3)(?P=QUOTATION_MARKS3))'
ti_start1=re.compile(TI_START1)

TI_START2=r'^(进程|Process)'+r'\s+'+ \
          r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))'+r'\s+'+ \
          r'executed by chore'+r'\s+' + \
          r'(?P<QUOTATION_MARKS2>["\'])?(?P<RUN_FROM_PROCESS_NAME>[^"\']+?)(?(QUOTATION_MARKS2)(?P=QUOTATION_MARKS2))'+r'$'
ti_start2=re.compile(TI_START2)

TI_START3=r'^(进程|Process)'+r'\s+'+ \
          r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))'+r'\s+'+ \
          r'executed by user'+r'\s+' + \
          r'(?P<QUOTATION_MARKS2>["\'])?(?P<EXECUTED_BY_USER>[^"\']+?)(?(QUOTATION_MARKS2)(?P=QUOTATION_MARKS2))'+r'$'
ti_start3=re.compile(TI_START3)

# 正常执行完成
# 进程 "A01维度_半成品":  已正常完成执行,耗时 67.95 秒
TI_FINISH1=r'^(进程|Process)'+r'\s+'+ \
           r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))(:|:)'+r'\s+'+ \
           r'.*'+ r'已正常完成执行'+r'.*'+ \
           r'耗时\s*(?P<DURATION>\d+\.?\d+)\s*秒'
ti_finish1=re.compile(TI_FINISH1)

# 执行完成,但存在错误,其中 已正常完成执行.请在以下文件中检查新消息 这种方式实际仍然是有问题,所以归为一类
# 进程 "A00系统_更新经营分析实际数":  已完成执行,但存在错误.错误文件:<tm1processerror_20221214235040_34120320_a00系统_更新经营分析实际数.log>
# 进程 "P050_Change_ElementName":  已正常完成执行.请在以下文件中检查新消息:<tm1processerror_20220323140015_94976000_p050_change_elementname.log>
TI_FINISH2=r'^(进程|Process)'+r'\s+'+ \
           r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))(:|:)'+r'\s+'+ \
           r'.*'+ r'(已完成执行|已正常完成执行)'+r'.*'+ \
           r'<'+r'(?P<MESSAGE_FILE>[^<>]+?)'+r'>'
ti_finish2=re.compile(TI_FINISH2)

# 执行终止
# 进程 "A08经营分析_BA0304材料采购成本分析_采购入库": :执行已中止.错误文件:<tm1processerror_20221214234251_31720320_a08经营分析_ba0304材料采购成本分析_采购入库.log>
# 进程 "A06维度元素导出_客户(层级)": :执行已中止.错误文件:<tm1processerror_20220325163559_26905088_a06维度元素导出_客户(层级).log> : 执行 SQL 查询出错:“insert into T_C1_DIMENSION_CUSTOMER (c1,c2,c3) values ( 'B822','HARRY'S.INC-B822','N-B822')”
TI_TERMINATE=r'^(进程|Process)'+r'\s+'+ \
             r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))(:|:)'+r'\s+'+ \
             r'.*'+r'执行已中止.*错误文件.*'+ \
             r'<'+r'(?P<MESSAGE_FILE>[^<>]+?)'+r'>'
ti_terminate=re.compile(TI_TERMINATE)

# 执行取消
# 进程 "A02年度预算_F0603损益预实_内外部客户营收_按钮": :ProcessQuit 函数已将执行中止.
# 这里 允许 '或" 来分隔 TI_NAME 或没有分隔符
TI_QUIT=r'^(进程|Process)'+r'\s+'+ \
        r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))(:|:)'+r'\s+'+ \
        r'.*'+r'ProcessQuit'+r'.*'+r'$'
ti_quit=re.compile(TI_QUIT)

# 其他方式 此时主要是获取 程序的名称,如果这个名称没有启动,则将其认为是启动,含义是启动时,就遇到了错误,所以这个启动和终止是一个提示信息
# A00系统_更新年度预算实际数0330:不存在这样的进程名称:“A02年度预算_B0407人员成本占比_清数”
# 进程 "A00系统_更新年度预算实际数0330": :无法初始化进程 : A00系统_更新年度预算实际数0330:不存在这样的进程名称:“A02年度预算_B0407人员成本占比_清数”
# 在进程“Cube数据导出_B0703损益表”中找不到参数“”
TI_UNKNOWN1=r'(?P<TI_NAME>[^"\'\s]+?)(:|:)'+ r'\s*' +\
            r'不存在这样的进程名称'
ti_unknown1=re.compile(TI_UNKNOWN1)

TI_UNKNOWN2=r'^(进程|Process)'+r'\s+'+ \
            r'(?P<QUOTATION_MARKS>["\'])?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)(?P=QUOTATION_MARKS))(:|:)'+r'\s+'
ti_unknown2=re.compile(TI_UNKNOWN2)

TI_UNKNOWN3=r'.*'+r'(进程|Process)'+r'\s*'+ \
            r'(?P<QUOTATION_MARKS>“)?(?P<TI_NAME>[^"\']+?)(?(QUOTATION_MARKS)”)'
ti_unknown3=re.compile(TI_UNKNOWN3)

# 忽略,此方式没有程序名称,而且是固定的,且是DEBUG级别或者是前面没有解析出 名称的 其他未知方式
# ExecuteProcedures: Cancel request received.
# 139372829603584   [9846]   DEBUG   2022-11-24 13:49:26.455   TM1.Process   ExecuteProcedures: Cancel request received.
TI_IGNORE=r'ExecuteProcedures: Cancel request received.'
ti_ignore=re.compile(TI_IGNORE)

# token类型
Token = collections.namedtuple('Token', ['type', 'value'])

# 用于分析过程的描述,不包含执行开始,结束,消耗cpu这3个统计信息
ANALYSIS_INFO=collections.namedtuple('ANALYSIS_INFO',
                                     ['analysis_id',
                                      'db_started_line','db_started_time','db_shutdowned_line','db_shutdowned_time',
                                      'begin_position_line','begin_position_time','begin_position_desc',
                                      'end_position_line','end_position_time','end_position_desc',
                                      'save_point_line','save_point_time','save_point_desc',
                                      'description'
                                     ]
                                    )

# ===========================================================================
# 函数
def generate_tokens():
    '''
    产生LOG_STATEMENT的token.由于语句可能包含多行的情况,此时考虑token阶段不处理文法
    所以将后续的多行均作为unknown,在文法阶段,将其合并到前一个语句的信息描述部分
    '''
    # tm1server.log文件存在编码无法解析为utf-8的情况,此时使用replace方式,将其作为?处理
    # codecs.register_error()
    # Error Handlers
    def unicode_decode_err_handle(exc):
        '''
        由于文件中包含无法解析为utf-8的编码,而使用replace,backslashreplace方式,取代结果中都包含了\n,导致行无法和文件中行对应
        不便于核对文件数据,所以使用替代方式,使用codecs.register_error注册,然后调用这个替代方式处理
        exc是UnicodeDecodeError异常,为UnicodeError的子类,具有下面属性
        encoding 引发错误的编码名称.
        reason   描述特定编解码器错误的字符串.
        object   编解码器试图要编码或解码的对象.
        start    object 中无效数据的开始位置索引.
        end      object 中无效数据的末尾位置索引(不含).
        '''
        if not isinstance(exc, UnicodeDecodeError):
            raise TypeError("don't know how to handle %r" % exc)

        sk=str(exc.object[exc.start:exc.end])
        newpos = exc.end + 1
        return (sk, newpos)

    # 设置unicode处理方式为前面替代方法
    codecs.register_error("unicode_decode_err_handle", unicode_decode_err_handle)
    with open("tm1server.log","rt",encoding='utf-8',errors='unicode_decode_err_handle') as f1:
        i=0
        for line in f1:
            i+=1
            if  ws.match(line) :
                # 忽略空行
                continue
            if  log_statement.match(line) :
                match=log_statement.match(line)
                # 参考https://cx-oracle.readthedocs.io/en/latest/user_guide/sql_execution.html?highlight=TIMESTAMP%20WITH%20TIME%20ZONE#id7
                # The timestamps returned are naive timestamps without any time zone information present.
                # TIMESTAMP WITH TIME ZONE中实际没有包含时区信息,所以数据库中列类型仅仅是TIMESTAMP
                tok=Token('LOG_STATEMENT',
                          {'LINE':i,                                                                           # 便于核对
                           'PART1':int(match.group('PART1')),                                                  # 这个应该是和线程某种编号有关对象id
                           'PART2':int(match.group('PART2')) if match.group('PART2')!='' else None,            # 这个应该是和实例化有关的编号 
                           'CURR_TIMESTAMP':datetime.datetime.fromisoformat(match.group('CURR_TIMESTAMP')),    # 使用本地时区时间戳
                           'CURR_SOURCE':match.group('CURR_SOURCE'),                                           # 日志来源对象
                           'CURR_DESC':match.group('CURR_DESC')                                                # 描述信息
                          }
                         )
            else:
                tok=Token('UNKNOWN',
                          {'LINE':i,                                       # 便于核对
                           'CURR_DESC':line                                # 将整行作为描述信息
                          }
                         )
                logger.debug("UNKNOWN TOKEN %d , %s"%(i,line))
            yield tok

class TM1_ServerLog(object):
    '''
    解析tm1server日志文法,提取句子,使用产生器方式来迭代提取
    参考 python cookbook 中P78示例
    '''
    def parse(self,last_info):
        '''
        对tm1server.log文件进行解析,获取主要信息
        '''
        self.tokens = generate_tokens()
        self.tok = None # Last symbol consumed
        self.nexttok = None # Next symbol tokenized
        self._advance() # Load first lookahead token
        self.context(last_info)

    def _advance(self):
        '''
        读取token
        '''
        self.tok, self.nexttok = self.nexttok, next(self.tokens, None)

    def _accept(self, toktype):
        '''
        判断是否为对应类型的token,如果符合要求,则读入下一个token
        '''
        if self.nexttok and self.nexttok.type == toktype:
            self._advance()
            return True
        else:
            return False

    def _expect(self, toktype):
        '''
        判断是否为预期的token,否则产生语法异常
        '''
        if not self._accept(toktype):
            raise SyntaxError('Expected ' + toktype+', current token is '+str(self.tok))

    # Grammar rules follow
    def context(self,last_info):
        '''
        按照下面文法解析trace文件
        <语句列表>::= <语句>|<语句列表><语句>
        考虑为了尽可能的得到执行结构的输出,防止日志输出方式和自己理解的不一致,对于异常方式,均输出提示.方便理解日志,修改代码
        因此这里是全部日志文件解析,后续利用last_savepoint过滤需要的分析结果
        '''
        # 初始化分析TM1.Process的变量
        self.ti_list={}
        self.chore_list={}
        #数据库服务启动说明
        self.started_db={}
        self.started_times=0

        # 当开始阶段没有匹配到语句,则继续读取token
        while not self._accept('LOG_STATEMENT'):
            # 当读取到文件尾时,实际表示文件中不包含需要处理的信息,或文件实际为空
            # 直接返回
            if  self.tok is None:
                return
            # 继续读取下一个token来判断是否进入了语句
            self._advance()

        while True:
            # 解析出语句信息
            curr_value=self.statement()

            if curr_value['CURR_SOURCE']=='TM1.Server':
                # 此时判断是否匹配启动
                self.process_server_data(curr_value,last_info)

            if curr_value['CURR_SOURCE']=='TM1.Process':
                # 此时是需要处理的ti
                self.process_ti_data(curr_value,last_info)

            if curr_value['CURR_SOURCE']=='TM1.Chore':
                # 此时处理chore
                self.process_chore_data(curr_value,last_info)

            # 其他来源的暂时不处理,如果后续处理,这里增加即可

            # 读入后续的语句
            if  self._accept('LOG_STATEMENT'):
                continue

            if  self.nexttok is None:
                # 此时trace文件匹配到尾部
                # 将self.ti_list增加到前面的启动分析中
                # 为防止 TM1.Server关闭后,已经将分析数据追加到self.started_db[self.started_times],所以这里需要判断是否为空
                if len(self.ti_list.keys())>0 or len(self.chore_list.keys())>0:
                    if self.started_times in self.started_db:
                        db_started_line,db_started_time,*ignore_items=self.started_db[self.started_times]
                    else:
                        # 此时表示人为删除了文件,已经没有遇到数据库启动标识
                        db_started_line,db_started_time=None,None
                    
                    self.started_db[self.started_times]=(db_started_line,db_started_time,None,None,self.ti_list,self.chore_list)
                    # 清空ti分析数据,chore分析数据
                    self.ti_list={}
                    self.chore_list={}
                    self.started_times+=1
                break

            # 此时说明遇到了非 LOG_STATEMENT,也不是结尾的情况
            raise SyntaxError('遇到 ' + self.nexttok.type+' 不符合文法要求')

        #这里无返回值,需要外部过程调用,按照上次的保存点来过滤需要的结果
        return

    def statement(self):
        '''
        按照下面文法解析
        语句 ::= LOG_STATEMENT [ UNKNOWN ]
        这里解析后,实际是返回语句的值
        '''
        curr_value=self.tok.value

        # 判断是否包含多行描述信息,这里保持原始文件中多行描述信息的换行方式
        while True:
            if self._accept('UNKNOWN'):
                curr_value['CURR_DESC']=curr_value['CURR_DESC']+'\n'+self.tok.value['CURR_DESC']
                continue
            else:
                break
        return curr_value

    def process_server_data(self,curr_value,last_info):
        '''
        解析是否匹配启动和suhtdown,目的是仅分析同次启动的数据,
        增加TM1.Chore数据,仍然遵守TM1.Process的处理原则,以便后续使用层级来搜索启动关系,从而区分同次
        '''
        # 当前所使用的变量初始化
        line_number=curr_value['LINE']
        curr_timestamp=curr_value['CURR_TIMESTAMP']
        curr_desc=curr_value['CURR_DESC']

        match=server_start.match(curr_desc)
        if match:
            # 此时开始新的数据
            # 需要检查看self.ti_list是否有数据,如果有,则需要其对应到前一次的数据中
            # 为防止 TM1.Server关闭后,已经将分析数据追加到self.started_db[self.started_times],所以这里需要判断是否为空 
            if  len(self.ti_list.keys())>0 or len(self.chore_list.keys())>0 :
                # 此时需要将待处理的ti数据,保存到前次对应的启动数据
                # 如果self.started_times为0,则表示原来没有找到过,此时启动时间未知,结束时间未知
                # 例如是异常终止
                if self.started_times in self.started_db:
                    db_started_line,db_started_time,*ignore_items=self.started_db[self.started_times]
                else:
                    db_started_line,db_started_time=None,None
                self.started_db[self.started_times]=(db_started_line,db_started_time,None,None,self.ti_list,self.chore_list)
            # 恢复新的ti数据分析
            self.ti_list={}
            self.chore_list={}
            self.started_times+=1
            self.started_db[self.started_times]=(line_number,curr_timestamp,None,None,self.ti_list,self.chore_list)

            return

        match=server_shutdown.match(curr_desc)
        if match:
            # 此时填写结束时间
            db_started_line,db_started_time,*ignore_items=self.started_db[self.started_times]
            self.started_db[self.started_times]=(db_started_line,db_started_time,line_number,curr_timestamp,self.ti_list,self.chore_list)
            # 恢复新的ti数据分析
            self.ti_list={}
            self.chore_list={}
        return

    def process_chore_data(self,curr_value,last_info):
        '''
        解析是否匹配启动和suhtdown,目的是仅分析同次启动的数据,
        增加TM1.Chore处理,仍然遵守TM1.Process的处理原则,以便后续使用层级来搜索启动关系,从而区分同次
        '''
        def parse_start(curr_desc):
            # 检查是否符合启动匹配
            match=chore_start.match(curr_desc)
            if  match:
                chore_name=match.group('CHORE_NAME')
                return True,chore_name

            # 此时不匹配启动
            return False,None

        def parse_finish(curr_desc):
            # 检查是否为 执行完成 匹配
            match=chore_finish1.match(curr_desc)
            if  match:
                chore_name=match.group('CHORE_NAME')
                duration=float(match.group('DURATION'))
                return_code=1
                return True,chore_name,duration,return_code

            match=chore_finish2.match(curr_desc)
            if  match:
                chore_name=match.group('CHORE_NAME')
                duration=None
                return_code=0
                return True,chore_name,duration,return_code

            # 此时不匹配完成
            return False,None,None,None

        def parse_ignore(curr_desc):
            # 检查是否为 执行完成 匹配
            if  (   chore_ignore1.match(curr_desc)
                 or chore_ignore2.match(curr_desc)
                 or chore_ignore3.match(curr_desc)
                 or chore_ignore4.match(curr_desc)
                ):
                # 此时是 正常执行完成
                return True

            # 此时不匹配忽略
            return False

        def add_start(line_number,curr_timestamp,description,chore_name):
            # 添加chore对象的实例,由于chore实际是一种计划任务,为对象启动父子关系中的最高层级,实际是根节点
            # 所以是简单添加
            if chore_name not in self.chore_list:
                # 表示这个记录是 这个chore的初次记录
                # 最后一个为list对象,以便在chore完成时,用于子对象搜索,关闭子对象
                # 倒数第2个为return_code状态
                self.chore_list[chore_name]=[(line_number,chore_name,
                                              curr_timestamp,None,None,{line_number:description},
                                              None,[],
                                              part1,part2
                                             )]
            else:
                # 在chore_list中按照启动时间排序的位置,插入当前记录
                # 遵循的原则是如果没有开始时间,则结束时间早的一定排在待插入记录之前
                # 如果没有开始时间,但结束时间在此启动时间之后,则对于待插入的数据,可以插入在前,也可以插入在后
                # 这里简单追加list,然后执行原地排序
                self.chore_list[chore_name].append((line_number,chore_name,
                                                    curr_timestamp,None,None,{line_number:description},
                                                    None,[],
                                                    part1,part2
                                                   ))
                # 如果begin_time 开始时间存在,则使用开始时间, 否则使用end_time结束时间
                self.chore_list[chore_name].sort(key=lambda r: r[2] if r[2] else r[3])

            return

        def add_finish(line_number,curr_timestamp,description,chore_name,duration,return_code):
            # 关闭chore对象,查找对应的实例,设置状态,同时依据子对象结构,关闭ti_list中的子对象
            # 此时添加chore结束 的记录
            if chore_name not in self.chore_list:
                # 此时日志文件不完整,或曾经被删除过,导致启动的记录存在缺失
                # 没有子对象,不关闭子对象
                self.chore_list[chore_name]=[(line_number,chore_name,
                                              None,curr_timestamp,duration,{line_number:description},
                                              return_code,[],
                                              part1,part2
                                             )]
            else:
               # 查找可能匹配的同次chore执行的记录,找到后,则填充那个执行日志的结束时间,状态等,
               # duration可以用于辅助判断是否为同次chore执行
               # 查找机制是查开始时间是last_savepoint之后,在curr_timestamp时间戳之前的日志且没有结束的,
               # 此外如果是正常结束,时长已知的,同时校核一下时长,若时长不正确,则继续搜索下一个
               # 若没有找到,则表示解析又问题,提示的同时,新增加一个日志记录

               # 由于python 3.9中bisect.bisect_left没有包含key参数,所以前面使用其他方式来操作
               time_list=[r[2] if r[2] else r[3] for r in self.chore_list[chore_name]]

               # 暂时从头开始查找,后续视情况再改善效率
               position_begin=0
               position_end=bisect.bisect_right(time_list,curr_timestamp)

               for i in range(position_begin,position_end):
                   # 逐个查找过滤,要求数据是
                   (temp_line_number,temp_chore_name,
                    temp_begin_time,temp_end_time,temp_ignore_duration,temp_description,
                    temp_return_code,temp_child_nodes,
                    temp_part1,temp_part2
                   )=self.chore_list[chore_name][i]
                   # 考虑return_code状态为0和1的情况
                   # 1匹配到有时长的完成,其后继续释放资源,进入状态0,0表示匹配到最终完成,
                   # 如果temp_return_code为0, 则要么完成匹配了,或者是异常的情况,无开始时间,但状态为0,那个对象一定关闭了
                   if  (   temp_return_code==0 
                        or temp_begin_time is None 
                        or temp_part1!=part1 and temp_part2!=part2 
                       ) :
                       continue

                   temp_duration=(curr_timestamp-temp_begin_time).days*86400+ \
                                 (curr_timestamp-temp_begin_time).seconds+ \
                                 (curr_timestamp-temp_begin_time).microseconds*1.0/1000/1000
                   
                   # 现在使用part1,part2来判断,取消原来判断方法
                   # 2022.12.25
                   
                   # 当执行时间很短时,不能简单使用%来判断所以这里使用1秒以下的,和1秒以上的
                   # duration is None,表示本次操作是最终释放资源
                   # if  (   duration is None
                   #      or duration>=1 and temp_duration>=0.9*duration and temp_duration<1.1*duration
                   #      or duration<1 and temp_duration<1
                   #     ):
                   
                   # 采用找到的第一个
                   # 为了便于核对,保留启动时和行号和描述,追加当前行号和描述
                   # 后续使用json来保存description数据
                   temp_description[line_number]=description
                   self.chore_list[chore_name][i]=(temp_line_number,chore_name,
                                                   temp_begin_time,curr_timestamp,temp_duration,temp_description,
                                                   return_code,temp_child_nodes,
                                                   temp_part1,temp_part2
                                                  )
                   # 对所有子节点对象关闭实例,并设置结束状态为4
                   self.close_child_nodes(temp_child_nodes,4)
                   return

               # 此时没有找到匹配
               # 没有子对象,不关闭子对象
               logger.warning("没有找到对应的启动记录,当前操作完成日志为 {} {} {}".format(line_number,curr_timestamp,description))
               self.chore_list[chore_name].append((line_number,chore_name,
                                                   None,curr_timestamp,duration,{line_number:description},
                                                   return_code,[]
                                                  ))
              
            return

        # 当前所使用的变量初始化
        line_number=curr_value['LINE']
        curr_timestamp=curr_value['CURR_TIMESTAMP']
        description=curr_value['CURR_DESC']
        part1=curr_value['PART1']
        part2=curr_value['PART2']

        # 检查是否为启动 匹配
        status,chore_name=parse_start(description)
        if  status:
            # 此时添加到chore_list中
            # 此时已解析 chore_name
            add_start(line_number,curr_timestamp,description,chore_name)
            return

        # 检查是否为完成 匹配
        status,chore_name,duration,finish_code=parse_finish(description)
        if  status:
            # 此时添加到chore_list中
            # 此时已解析 chore_name,duration
            add_finish(line_number,curr_timestamp,description,chore_name,duration,finish_code)
            return

        # 检查是否为可忽略的匹配
        status=parse_ignore(description)
        if  status:
            # 这里均为忽略
            return

        # 这种情况表示是未知的情况,警告输出
        logger.warning("UNKNOWN TM1.Chore %d , %s"%(line_number,description))
        return

    def process_ti_data(self,curr_value,last_info):
        '''
        处理ti的数据.主要是对描述信息解析,将处理的结果保存到ti_list数据结构中
        ti_list结构是 {"ti名称":[按照启动时间排序,同ti名称的不同次执行过程列表],[]...}
        ti_name :用于对应志勇他们程序中名称
        状态: 启动状态,终止状态,还是其他
        启动状态时:解析出 启动程序,启动用户
        终止状态时:判断出终止状态.是异常终止,还是带警告的终止,或正常结束.同时获取涉及的详细信息记录文件,文件的名称通过解析简短描述
                    中<> 的内容来获取.通过日志文件发现,每次描述信息中<>仅出现1次

        填充ti数据,大致处理过程
        1. 判断是开始执行还是执行完成或退出,操作时间,按照不同情况,记录对应的各种数据
        2. 查找字典结构中此ti名称的分析,
        3. 如果当前行是开始执行,则在此ti名称的分析list中按照启动时间顺序增加一行.
           同一次ti操作日志输出会按照时间排序,但不同次,或不同ti执行程序会由于不同线程线程操作,或者写入机制导致输出时间顺序不同情况
        4. 如果是执行返回,则按照下面原理查找这个对应的同次执行.
           1. 同次执行 日志文件中开始位置一定在结束位置之前,
           2. 不同次的 同执行程序,执行顺序与记录顺序可能不同.
              但结束时间在某次执行开始时间前的,则在list位置中在前
        5. 如果处理分析有违背此分析原则的,则提示,以便后续查找原因

        '''
        def parse_start(curr_desc):
            # 检查是否符合3种启动匹配
            match=ti_start1.match(curr_desc)
            if  match:
                ti_name=match.group('TI_NAME')
                run_from_process_type='TM1.Process'
                run_from_process_name=match.group('RUN_FROM_PROCESS_NAME')
                executed_by_user=match.group('EXECUTED_BY_USER')
                return True,ti_name,run_from_process_type,run_from_process_name,executed_by_user

            match=ti_start2.match(curr_desc)
            if  match:
                ti_name=match.group('TI_NAME')
                run_from_process_type='TM1.Chore'
                run_from_process_name=match.group('RUN_FROM_PROCESS_NAME')
                executed_by_user=None
                return True,ti_name,run_from_process_type,run_from_process_name,executed_by_user

            match=ti_start3.match(curr_desc)
            if  match:
                ti_name=match.group('TI_NAME')
                run_from_process_type=None
                run_from_process_name=None
                executed_by_user=match.group('EXECUTED_BY_USER')
                return True,ti_name,run_from_process_type,run_from_process_name,executed_by_user

            # 此时不匹配启动
            return False,None,None,None,None

        def parse_finish(curr_desc):
            # 包含正常执行完成,
            # 检查是否为 执行完成 匹配
            match=ti_finish1.match(curr_desc)
            if  match:
                # 此时是 正常执行完成
                ti_name=match.group('TI_NAME')
                duration=float(match.group('DURATION'))
                return_code=0
                message_file=None
                return True,ti_name,duration,return_code,message_file

            match=ti_finish2.match(curr_desc)
            if  match:
                # 此时是 带警告的执行完成
                ti_name=match.group('TI_NAME')
                duration=None
                return_code=1
                message_file=match.group('MESSAGE_FILE')
                return True,ti_name,duration,return_code,message_file

            # 检查是否为 执行终止 匹配
            match=ti_terminate.match(curr_desc)
            if  match:
                # 此时是 执行终止
                ti_name=match.group('TI_NAME')
                duration=None
                return_code=2
                message_file=match.group('MESSAGE_FILE')
                return True,ti_name,duration,return_code,message_file

            # 检查是否为 取消执行 匹配
            match=ti_quit.match(curr_desc)
            if  match:
                # 此时是 取消执行
                ti_name=match.group('TI_NAME')
                duration=None
                return_code=3
                message_file=None
                return True,ti_name,duration,return_code,message_file

            # 此时不匹配完成
            return False,None,None,None,None

        def parse_unknown(curr_desc):
            # 处理方法与完成的有所区别,主要是某些情况下,启动和这个退出合并为1次出现
            # 由于ti_unknown1需要将信息增加到最近启动结构中,ti_unknown2需要视为结束,ti_unknown3维持原有方法不变
            # 所以增加返回参数,用于区别对待
            match=ti_unknown1.match(curr_desc)
            if  match:
                ti_name=match.group('TI_NAME')
                return True,ti_name,1

            match=ti_unknown2.match(curr_desc)
            if  match:
                ti_name=match.group('TI_NAME')
                return True,ti_name,2

            match=ti_unknown3.match(curr_desc)
            if  match:
                ti_name=match.group('TI_NAME')
                return True,ti_name,3

            return False,None,None

        def add_start(line_number,curr_timestamp,description,ti_name,run_from_process_type,run_from_process_name,executed_by_user):
            # 判断文件在上次分析结束前的记录是否被修改过,如果修改过,则需要判断是否简单的删除上次分析后可以删除的部分,
            # 如果是,则可能仅分析后面新产生的日志,从而提高分析性能.否则基本无法提高分析性能

            # 判断是否存在上次保存点之前启动,但日志记录很晚的现象.
            # 正常不应该存在此现象,但如果这个程序线程被阻塞或类似原因,造成日志无法及时写入日志文件,则可能存在此现象

            # 考虑启动,执行完成的描述信息都需要保存,所以使用json来保存,以便后续复原
            if (    last_info
                and last_info.save_point_line
                and line_number>last_info.save_point_line
                and last_info.save_point_time
                and curr_timestamp<last_info.save_point_time
               ):
                logger.warning("启动时间早于上次保存点,但启动记录位置晚于上次的保存点 {} {} {}".format(line_number,curr_timestamp,description))

            # 依据run_from_process_type,run_from_process_name 查找父对象的实例启动时间,同时在父对象实例中注册自身实例名称和启动时间
            parent_begin_time=self.register_nodes(run_from_process_type,run_from_process_name,ti_name,curr_timestamp,part1,part2)
            
            # 此时添加ti启动的记录
            # 最后一列增加子节点对象列表,实现父子关系搜索
            if ti_name not in self.ti_list:
                # 表示这个记录是 这个ti的初次记录
                self.ti_list[ti_name]=[(line_number,ti_name,
                                        run_from_process_type,run_from_process_name,parent_begin_time,executed_by_user,
                                        curr_timestamp,None,None,{line_number:description},
                                        None,None,[],
                                        part1,part2
                                       )]
            else:
                # 在ti_list中按照启动时间排序的位置,插入当前记录
                # 遵循的原则是如果没有开始时间,则结束时间早的一定排在待插入记录之前
                # 如果没有开始时间,但结束时间在此启动时间之后,则对于待插入的数据,可以插入在前,也可以插入在后
                # 这里简单追加list,然后执行原地排序
                self.ti_list[ti_name].append((line_number,ti_name,
                                              run_from_process_type,run_from_process_name,parent_begin_time,executed_by_user,
                                              curr_timestamp,None,None,{line_number:description},
                                              None,None,[],
                                              part1,part2
                                             ))
                # 如果begin_time 开始时间存在,则使用开始时间, 否则使用end_time结束时间
                self.ti_list[ti_name].sort(key=lambda r: r[6] if r[6] else r[7])
            
            return

        def add_finish(line_number,curr_timestamp,description,ti_name,duration,return_code,message_file):
            # 此时添加ti结束 的记录
            if ti_name not in self.ti_list:
                # 此时日志文件不完整,或曾经被删除过,导致启动的记录存在缺失
                self.ti_list[ti_name]=[(line_number,ti_name,
                                        None,None,None,None,
                                        None,curr_timestamp,duration,{line_number:description},
                                        return_code,message_file,[],
                                        part1,part2
                                       )]
            else:
               # 查找可能匹配的同次ti执行的记录,找到后,则填充那个执行日志的结束时间,状态等,
               # duration可以用于辅助判断是否为同次ti执行
               # 查找机制是查开始时间是last_savepoint之后,在curr_timestamp时间戳之前的日志且没有结束的,
               # 此外如果是正常结束,时长已知的,同时校核一下时长,若时长不正确,则继续搜索下一个
               # 若没有找到,则表示解析有问题,提示的同时,新增加一个日志记录

               # 由于python 3.9中bisect.bisect_left没有包含key参数,所以前面使用其他方式来操作
               time_list=[r[6] if r[6] else r[7] for r in self.ti_list[ti_name]]

               # 暂时从头开始查找,后续视情况再改善效率
               position_begin=0
               position_end=bisect.bisect_right(time_list,curr_timestamp)

               for i in range(position_begin,position_end):
                   # 逐个查找过滤,要求数据是
                   (temp_line_number,temp_ti_name,
                    temp_run_from_process_type,temp_run_from_process_name,temp_executed_by_user,temp_parent_begin_time,
                    temp_begin_time,temp_end_time,temp_ignore_duration,temp_description,
                    temp_return_code,temp_ignore_message,temp_child_nodes,
                    temp_part1,temp_part2
                   )=self.ti_list[ti_name][i]
                   # 如果这个数据结束时间已经有了,或 开始时间为空 说明这个是一个完成记录
                   # 如果temp_return_code为-1,或其他非None的值,都表示这个数据已经记录完整了
                   # 相同的对象,part1,part2是相同的
                   if  temp_return_code is not None or temp_part1!=part1 and temp_part2!=part2:
                       continue
                   # 由于Process "A02年度预算_B0601、F0502部门费用预算" run from process "A00系统_更新年度预算实际数" by user "R*A00系统_更新年度预算实际数"
                   # 存在显示2次,导致跨天匹配的问题,所以进行天的比对
                   # 2022.12.21
                   temp_duration=(curr_timestamp-temp_begin_time).days*86400+ \
                                 (curr_timestamp-temp_begin_time).seconds+ \
                                 (curr_timestamp-temp_begin_time).microseconds*1.0/1000/1000
                   # 当执行时间很短时,不能简单使用%来判断所以这里使用1秒以下的,和1秒以上的
                   # 使用part1,part2来判断同一对象,所以这里取消时长的判断
                   # duration is None表示非正常执行完成情况
                   # if  (   duration is None
                   #      or duration>=1 and temp_duration>=0.9*duration and temp_duration<1.1*duration
                   #      or duration<1 and temp_duration<1
                   #     ):
                   
                   # 此时不是正常完成的,没有时长,认为是找到的第一个
                   # 或者时长匹配
                   # 为了便于核对,保留启动时和行号和描述,追加当前行号和描述

                   # str(temp_line_number)+':'+temp_description+'\n'+ str(line_number)+':'+description
                   # 后续使用json来保存description数据
                   temp_description[line_number]=description
                   self.ti_list[ti_name][i]=(temp_line_number,ti_name,
                                             temp_run_from_process_type,temp_run_from_process_name,temp_executed_by_user,temp_parent_begin_time,
                                             temp_begin_time,curr_timestamp,temp_duration,temp_description,
                                             return_code,message_file,temp_child_nodes,
                                             temp_part1,temp_part2
                                            )
                   # 由于不确定ti执行时,是否等待子对象完成,所以不处理关闭子节点对象,仅在chore对象关闭时,关闭子节点
                   # self.close_child_nodes(temp_child_nodes,4)
                   return

               # 此时没有找到匹配
               logger.warning("没有找到对应的启动记录,当前操作完成日志为 {} {} {}".format(line_number,curr_timestamp,description))
               self.ti_list[ti_name].append((line_number,ti_name,
                                             None,None,None,None,
                                             None,curr_timestamp,duration,{line_number:description},
                                             return_code,message_file,[],
                                             part1,part2
                                            ))
            return

        def add_extendinfo(line_number,curr_timestamp,description,ti_name):
            # 此时查到最可能的ti,将额外信息追加到描述中
            # 由于是额外信息,可以暂时不实现
            # 2022.12.21
            pass
            return

        def add_unknown(line_number,curr_timestamp,description,ti_name):
            # 此时添加 不是 启动和结束,但可以解析出ti_name的 记录
            # 此时return_code为-1,表示未知的方式,这个暂时单独一行,后续分析后,视情况修改代码,决定如何处理
            # 将当前时间戳,作为此事件发生的时间,填写到begin_time中
            return_code=-1
            if ti_name not in self.ti_list:
                self.ti_list[ti_name]=[(line_number,ti_name,
                                        None,None,None,None,
                                        curr_timestamp,curr_timestamp,None,{line_number:description},
                                        return_code,None,[],
                                        part1,part2
                                       )]
            else:
                self.ti_list[ti_name].append((line_number,ti_name,
                                              None,None,None,None,
                                              curr_timestamp,curr_timestamp,None,{line_number:description},
                                              return_code,None,[],
                                              part1,part2
                                             ))
                # 如果begin_time 开始时间存在,则使用开始时间, 否则使用end_time结束时间
                self.ti_list[ti_name].sort(key=lambda r: r[6] if r[6] else r[7])
            return

        # 当前所使用的变量初始化
        line_number=curr_value['LINE']
        curr_timestamp=curr_value['CURR_TIMESTAMP']
        description=curr_value['CURR_DESC']
        part1=curr_value['PART1']
        part2=curr_value['PART2']

        # 检查是否为启动 匹配
        status,ti_name,run_from_process_type,run_from_process_name,executed_by_user=parse_start(description)
        if  status:
            # 此时添加到ti_list中
            # 此时已解析 ti_name,run_from_process_type,run_from_process_name,executed_by_user
            add_start(line_number,curr_timestamp,description,ti_name,run_from_process_type,run_from_process_name,executed_by_user)
            return

        # 检查是否为完成 匹配 ,包含 正常执行完成,带警告的执行完成, 异常终止, 取消执行
        status,ti_name,duration,return_code,message_file=parse_finish(description)
        if  status:
            # 此时添加到ti_list中
            # 此时已解析 ti_name,duration,return_code,message_file
            add_finish(line_number,curr_timestamp,description,ti_name,duration,return_code,message_file)
            return

        # 检查是否为 可以匹配出ti_name的情况
        status,ti_name,unknown_code=parse_unknown(description)
        if  status:
            # 状态1 ,需要将信息补充到ti的描述信息中
            if  unknown_code==1:
                add_extendinfo(line_number,curr_timestamp,description,ti_name)
                return
            # 状态2, 按照退出方式来处理,等同于 2--执行已中止
            if  unknown_code==2:
                add_finish(line_number,curr_timestamp,description,ti_name,None,2,None)
                return
            # 状态3,维持原来处理方式不变
            # 此时添加到ti_list中,处理方法与完成的有所区别,主要是某些情况下,启动和这个退出合并为1次出现
            if  unknown_code==3:
                add_unknown(line_number,curr_timestamp,description,ti_name)
                return

        # 检查是否为可忽略的情况
        match=ti_ignore.match(description)
        if  match :
            # 此时表示直接忽略
            return

        # 这种情况表示是未知的情况,警告输出
        logger.warning("UNKNOWN TM1.Process %d , %s"%(line_number,description))
        return

    def get_ti_list(self,last_info):
        '''
        假设savepoint之前的内容可能会删除,所以这里仅取此后启动的,但为了对比分析,如何判断此前启动的
        这里意味着需要判断上次分析后,文件是否仅仅追加,而没有删除,通过比对上次的位置和描述信息是否一致来判断
        如果有不一致的情况,则在本次分析的描述中记录

        如果以前没有分析过,则按照行号排序后,行号最小的,对应数据作为本次开始,
        逐个检查状态,直到状态为None的数据,此时这个数据是启动状态,这个时间是savepoint,保留其对应的数据为本次savepoint
        继续检查,直到数据尾部,此时数据为本次分析结束点
        如果前次分析存在,且文件仅追加,或符合要求,不产生歧义,
        则排序后,向后查找,直至找到上次的保存点, 随后以此时间为本次开始,
        随后重复前面步骤,找到savepoint,和结束点
        若记录存在可能的歧义,则仍然按照这个方式来处理,但在分析概述中说明即可
        '''
        def search_positions(position_time,position_desc):
            # 查找符合时间戳和描述内容的日志位置
            # 数据不多,暂时顺序检查
            for temp_i in range(len(all_ti_lists)):
                temp_line,temp_time,temp_desc=get_line_info(temp_i)
                if temp_time==position_time and temp_desc==position_desc:
                    return temp_i

            return None

        def get_line_info(i):
            # 获取这个分析位置的信息
            (temp_times,
             temp_line_number,temp_ti_process_type,temp_ti_name,
             temp_run_from_process_type,temp_run_from_process_name,temp_parent_begin_time,temp_executed_by_user,
             temp_begin_time,temp_end_time,temp_duration,temp_description,
             temp_return_code,temp_message_file
            )=all_ti_lists[i]
            # 获取这个位置的行号
            position_line=temp_line_number
            # 获取这个位置的时间戳
            # 由于可能存在仅有结束时间,没有启动时间的日志,所以这种现象时,获取这个位置的结束时间
            if temp_begin_time:
                  position_time=temp_begin_time
            else:
                position_time=temp_end_time

            # 保存描述信息时,需要分离出原始行号对应数据,以便于核对
            position_desc=temp_description[temp_line_number]

            return position_line,position_time,position_desc

        # 将分析的数据转为list
        all_ti_lists=[]

        # 增加数据库启动过程记录,便于后续核对
        analysis_description=""
        for i in self.started_db:
            (db_started_line,db_started_time,db_shutdowned_line,db_shutdowned_time,
             ti_list,chore_list
            )=self.started_db[i]
            analysis_description=analysis_description+ \
                                 "启动行号: {} 启动时间: {}, 关闭行号: {} 关闭时间: {}\n".format(db_started_line,db_started_time,
                                      db_shutdowned_line,db_shutdowned_time)
            for curr_ti_list in ti_list.values():
                for (curr_line_number,curr_ti_name,
                     curr_run_from_process_type,curr_run_from_process_name,curr_parent_begin_time,curr_executed_by_user,
                     curr_begin_time,curr_end_time,curr_duration,curr_description,
                     curr_return_code,curr_message_file,ignore_child_nodes,
                     curr_part1,curr_part2
                    ) in curr_ti_list:
                    # 增加ti_process_type列,引入执行对象类型TM1.Chore,TM1.Process等,以便后续使用层级来搜索启动关系
                    all_ti_lists.append((i,
                                         curr_line_number,"TM1.Process",curr_ti_name,
                                         curr_run_from_process_type,curr_run_from_process_name,curr_parent_begin_time,curr_executed_by_user,
                                         curr_begin_time,curr_end_time,curr_duration,curr_description,
                                         curr_return_code,curr_message_file
                                        ))

            # 这里处理TM1.Chore对象
            for curr_chore_list in chore_list.values():
                for (curr_line_number,curr_chore_name,
                     curr_begin_time,curr_end_time,curr_duration,curr_description,
                     curr_return_code,ignore_child_nodes,
                     curr_part1,curr_part2
                    ) in curr_chore_list:
                    all_ti_lists.append((i,
                                         curr_line_number,"TM1.Chore",curr_chore_name,
                                         None,None,None,None,
                                         curr_begin_time,curr_end_time,curr_duration,curr_description,
                                         curr_return_code,None
                                        ))
            
        # 按照行位置排序
        all_ti_lists.sort(key=lambda r: r[1] )

        # 假设文件仅仅追加,或者文件从头删除到上次保存点之前的某位置
        # 查找本次开始的位置
        # 如果上次保存点未找到,或文件删除了部分内容,予以描述
        if last_info is None:
            # 此时是首次分析
            i,curr_analysis_id=0,1
        else:
            curr_analysis_id=last_info.analysis_id+1
            # 查找从上次分析的savepoint的位置
            # 并判断自上次分析后,是否删除,且可能删除的行数
            i=search_positions(last_info.save_point_time,last_info.save_point_desc)
            if i is None:
                i=0
                analysis_description=analysis_description+'上次的保存点未找到,重新开始分析文件'
            else:
                # 检查此位置对应数据
                if all_ti_lists[i][1]!=last_info.save_point_line:
                    analysis_description=analysis_description + \
                        '上次的保存点找到,但文件删除了 {} 行'.format(last_info.save_point_line-all_ti_lists[i][1])

        # 获取本次开始分析位置的信息
        curr_begin_position_line,curr_begin_position_time,curr_begin_position_desc=get_line_info(i)

        # 查找savepoint,这里查询最后一次数据库启动的分析
        imax_times=max(self.started_db.keys())
        (db_started_line,db_started_time,db_shutdowned_line,db_shutdowned_time,
         *ignore_items
        )=self.started_db[imax_times]

        for m in range(i,len(all_ti_lists)):
            # 要求是最后一次数据库启动的,且状态为空的数据为savepoint
            # 将TM1.Chore作为保存点的要素,无返回值或今天凌晨后启动的chore
            if  (    all_ti_lists[m][0]==imax_times 
                 and all_ti_lists[m][-2] is None 
                 and all_ti_lists[m][2]=="TM1.Chore"
                  or all_ti_lists[m][8].date()>=datetime.datetime.now().date()
                 and all_ti_lists[m][2]=="TM1.Chore"
                ):
                break

        curr_save_point_line,curr_save_point_time,curr_save_point_desc=get_line_info(m)

        curr_end_position_line,curr_end_position_time,curr_end_position_desc=get_line_info(-1)

        # 获取本次分析的详细数据
        result=all_ti_lists[i:]

        return ANALYSIS_INFO(curr_analysis_id,
                             db_started_line,db_started_time,db_shutdowned_line,db_shutdowned_time,
                             curr_begin_position_line,curr_begin_position_time,curr_begin_position_desc,
                             curr_end_position_line,curr_end_position_time,curr_end_position_desc,
                             curr_save_point_line,curr_save_point_time,curr_save_point_desc,
                             analysis_description
                            ),result

    def close_child_nodes(self,child_nodes,close_code):
        '''
        关闭子节点对象ti实例,并且设置关闭的状态.通过递归调用实现所有子节点对象关闭
        由于假设ti关闭时,其子ti不一定关闭,但chore(将其理解为调度任务,类似cron)关闭时,所有的子ti一定都关闭
        '''
        if child_nodes is None or len(child_nodes)==0:
            return
        for (ti_name,begin_time) in child_nodes:
            # 查询这个ti在self.ti_list中的实例位置
            # 若其存在子节点,则递归调用
            for i in range(len(self.ti_list[ti_name])):
                (curr_line_number,curr_ti_name,
                 curr_run_from_process_type,curr_run_from_process_name,curr_parent_begin_time,curr_executed_by_user,
                 curr_begin_time,curr_end_time,curr_duration,curr_description,
                 ignore_return_code,curr_message_file,curr_child_nodes,
                 curr_part1,curr_part2
                )=self.ti_list[ti_name][i]
                # 简单使用顺序扫描,检查是否是这个时间启动的对象实例
                if  begin_time==curr_begin_time :
                    if  ignore_return_code is None:
                        # 若这个子对象没有关闭,则设置为特殊的状态来关闭
                        # 已关闭则不处理
                        self.ti_list[ti_name][i]=(curr_line_number,curr_ti_name,
                            curr_run_from_process_type,curr_run_from_process_name,curr_parent_begin_time,curr_executed_by_user,
                            curr_begin_time,curr_end_time,curr_duration,curr_description,
                            close_code,curr_message_file,curr_child_nodes,
                            curr_part1,curr_part2
                            )
                    self.close_child_nodes(curr_child_nodes,close_code)
                    break
        return

    def register_nodes(self,parent_process_type,parent_process_name,ti_name,begin_time,part1,part2):
        '''
        子ti向父对象注册
        '''
        if  parent_process_type is None or parent_process_name is None:
            return None
            
        if parent_process_type=='TM1.Chore':
            # 父对象是TM1.Chore
            for i in range(len(self.chore_list[parent_process_name])):
                (curr_line_number,curr_chore_name,
                 curr_begin_time,curr_end_time,curr_duration,curr_description,
                 curr_return_code,curr_child_nodes,
                 curr_part1,curr_part2
                )=self.chore_list[parent_process_name][i]
                if  (    curr_return_code is None and curr_begin_time<=begin_time 
                     and curr_part1==part1 and curr_part2==part2
                    ) :
                    # 这个状态表示未结束,且启动时间早于这个子对象的启动时间
                    curr_child_nodes.append((ti_name,begin_time))
                    self.chore_list[parent_process_name][i]=(curr_line_number,curr_chore_name,
                            curr_begin_time,curr_end_time,curr_duration,curr_description,
                            curr_return_code,curr_child_nodes,
                            curr_part1,curr_part2
                            )
                    return curr_begin_time
            logger.warning("父节点未找到 {} {}".format(ti_name,begin_time))
            return None
        else:
            # 父对象是TM1.Process
            for i in range(len(self.ti_list[parent_process_name])):
                (curr_line_number,curr_ti_name,
                 curr_run_from_process_type,curr_run_from_process_name,curr_parent_begin_time,curr_executed_by_user,
                 curr_begin_time,curr_end_time,curr_duration,curr_description,
                 curr_return_code,curr_message_file,curr_child_nodes,
                 curr_part1,curr_part2
                )=self.ti_list[parent_process_name][i]
                if  (    curr_return_code is None and curr_begin_time<=begin_time 
                     and curr_part1==part1 and curr_part2==part2
                    ):
                    # 这个状态表示未结束,且启动时间早于这个子对象的启动时间
                    curr_child_nodes.append((ti_name,begin_time))
                    self.ti_list[parent_process_name][i]=(curr_line_number,curr_ti_name,
                         curr_run_from_process_type,curr_run_from_process_name,curr_parent_begin_time,curr_executed_by_user,
                         curr_begin_time,curr_end_time,curr_duration,curr_description,
                         curr_return_code,curr_message_file,curr_child_nodes,
                         curr_part1,curr_part2
                        )
                    return curr_begin_time
            logger.warning("父节点未找到 {} {}".format(ti_name,begin_time))
            return None
            
def get_last_analysis():
    '''
    获取前一次的分析的analysis_id, savepoint等信息,使用命名元组返回
    1. 按照analysis_id排序,查找最后一次分析的编号和savepoint时间
    2. 如果是首次分析,则返回 None
    '''

    try:
        conn=cx_Oracle.connect(BGDB_CONN_STR)
        cur=conn.cursor()
        # 使用命名元组的_fields属性来获取需要查询的列名
        # 这里不包含分析开始,分析结束,分析消耗cpu 这3个统计信息
        sql='select '+','.join(ANALYSIS_INFO._fields)+' from T_tm1slog' + chr(10) + \
            ' order by analysis_id desc'

        cur.execute(sql)

        # 这里仅提取1条数据,实际是返回最大的
        res=cur.fetchone()
        cur.close()
    except :
        stack = traceback.format_exc()
        logger.error( 'exception stack: %s' % stack)
    finally:
        conn.close()

    if res:
        return ANALYSIS_INFO(*res)
    else:
        return None

def fill_ti_data(curr_info,analysis_begin,analysis_end,cpu_time,ti_list):
    '''
    将分析后的数据保存到T_tm1slog,t_tm1slog_ti中,这里需要针对ti错误信息文件,返回每个错误的详细信息,而在ti文法解析阶段,
    不读取文件内容,仅到保存到数据库的阶段,才读取文件内容
    Args:
        curr_info       :本次分析信息,包含编号,时间戳,行等记录信息,数据类型为ANALYSIS_INFO
        analysis_begin  :本次分析,进程开始分析时,对应时刻
        analysis_end    :本次分析,进程结束分析时,对应时刻
        cpu_time        :进程消耗的cpu时间,用于查看性能情况,这3个为分析进程统计信息
        ti_list         :需要保存的ti数据
    Returns:
    Raises:
       无抛出异常,仅在遇到异常时,终端中提示
    '''
    # 如果没有需要保存的数据,直接返回
    if curr_info is None and ti_list is None:
        return

    try:
        conn=cx_Oracle.connect(BGDB_CONN_STR)
        conn.begin()

        cur=conn.cursor()
        cur.arraysize=BATCH_SIZE
        cur.bindarraysize=BATCH_SIZE

        # 插入本次分析的概述信息
        # 获取表中列的数据类型
        sql='select * from T_tm1slog where 1=0'
        cur.execute(sql)

        #获取每列的变量类型
        db_types = (d[1] for d in cur.description)
        # cur.description 描述了各返回列的描述,包含名称,类型等
        # oracle不支持?作为绑定变量,所以这里使用位置绑定变量
        col_parameters=",".join(":"+str(i) for i in range(len(cur.description)))

        # 构成插入语句
        sql="insert into T_tm1slog " +chr(10)+ \
            " values("+col_parameters+")"

        cur.prepare(sql)
        # 设置每列的数据类型
        cur.setinputsizes(*db_types)
        # 这里仅插入1条分析概述数据
        cur.execute(sql,(*curr_info,analysis_begin,analysis_end,cpu_time))

        # 插入ti数据
        sql='select * from t_tm1slog_ti where 1=0'
        cur.execute(sql)
        db_types = (d[1] for d in cur.description)
        col_parameters=",".join(":"+str(i) for i in range(len(cur.description)))

        sql="insert into t_tm1slog_ti "+chr(10)+ \
            " values("+col_parameters+")"
        cur.prepare(sql)
        # 设置每列的数据类型
        cur.setinputsizes(*db_types)

        # 由于cx_oracle没有cur.executemany类似hana中设置batch size,所以需要使用循环来判断
        # cx_oracle中cur.executemany行为是1次执行客户端中不判断插入的数据行数和cur.arraysize的关系
        data_arr=[]
        # 检查是否为空
        if  ti_list :
            for (i_start_times,
                 line_number,ti_process_type,ti_name,
                 run_from_process_type,run_from_process_name,parent_begin_time,executed_by_user,
                 begin_time,end_time,duration,description,
                 return_code,message_file
                ) in ti_list:
                # 这里需要针对ti错误信息文件,返回每个错误的详细信息
                if  message_file:
                    try:
                        with open(message_file,'rt',encoding='utf-8') as f:
                            return_message=f.read()
                    except :
                        logger.warning("%s 文件不存在"%message_file)
                else:
                    return_message=None

                # 填充数组
                # 描述信息description实际包含了行号和实际描述组成的字典,使用json.dumps来操作
                # 由于包含中午,所以使用ensure_ascii=False
                data_arr.append((curr_info.analysis_id,
                                 line_number,ti_process_type,ti_name,
                                 run_from_process_type,run_from_process_name,parent_begin_time,executed_by_user,
                                 begin_time,end_time,duration,
                                 json.dumps(description,separators=(',',':'),ensure_ascii=False),
                                 return_code,return_message
                                ))
                if  len(data_arr) ==BATCH_SIZE:
                    cur.executemany(sql,data_arr)
                    data_arr=[]
            if  data_arr:
                cur.executemany(sql,data_arr)
        conn.commit()
    except :
        stack = traceback.format_exc()
        logger.error('exception stack: %s' % stack)
        conn.rollback()
    finally:
        conn.close()

# 处理函数
def process_tm1server_log():
    '''
    处理tm1 server log大致过程
    1. 从数据库中查找上次的savepoint和analysis_id
    2. 建立TM1_ServerLog, 并从此savepoint之后开始过滤ti TM1.Process 的日志
    3. 返回本次处理的begin_position,end_position,savepoint,以及ti_list
    4. 保存数据到数据库中
    由于tm1server.log存在并发写入,由不同线程操作,同时分析程序和tm1s数据库日志操作也存在同时并发的情况
    处理时,不考虑日志被截断(或类似情况),仅假设为追加操作.不考虑使用文件加锁方式,但需要考虑文件内容正在写入时,读出的情况
    '''
    # 获取前一次的分析的analysis_id, savepoint,若为初次分析,则返回0,None
    last_info=get_last_analysis()

    # 获取进程执行的统计信息
    analysis_begin=datetime.datetime.now()
    cpu_begin=time.process_time()

    # 考虑为了尽可能的得到执行结构的输出,防止日志输出方式和自己理解的不一致,对于异常方式,均输出提示.方便理解日志,修改代码
    # 因此这里是全部日志文件解析,后续利用last_savepoint过滤需要的分析结果
    obj_serverlog = TM1_ServerLog()
    obj_serverlog.parse(last_info)
    # 获取本次处理的begin_position,end_position,savepoint,以及ti_list
    curr_info,ti_list=obj_serverlog.get_ti_list(last_info)

    # 获取进程执行的统计信息
    analysis_end=datetime.datetime.now()
    cpu_time=time.process_time()-cpu_begin

    # 将数据保存到数据库中
    fill_ti_data(curr_info,analysis_begin,analysis_end,cpu_time,ti_list)

# ===========================================================================
# main函数
if __name__ == '__main__':
    logging.basicConfig(level = logging.INFO,format = "%(asctime)s.%(msecs)d pid %(process)d %(filename)s line %(lineno)d %(funcName)s %(message)s",datefmt="%Y-%m-%d %H:%M:%S")
    logger = logging.getLogger(__name__)

    logger.info("begin...")
    # 处理tm1 server 日志
    process_tm1server_log()
    logger.info("finished...")
    

为了将数据保存到数据库中,便于分析, 建立如下的表结构

--用于琪雯她们查询预算中ti执行结果
--详细使用说明参考tm1slog.py
--万起胜
--2022.12.12

--=======================================================================================================================================
drop table t_tm1slog_ti purge;
drop table T_tm1slog purge;

create table T_tm1slog(
  analysis_id                 integer                    ,  --分析编号
  db_started_line             integer                    ,  --C1 数据库启动行号
  db_started_time             TIMESTAMP(3)               ,  --数据库启动的时间戳
  db_shutdowned_line          integer                    ,  --C1 数据库关闭行号
  db_shutdowned_time          TIMESTAMP(3)               ,  --C1 数据库关闭时间戳
  begin_position_line         integer                    ,  --分析开始位置对应行号
  begin_position_time         TIMESTAMP(3)               ,  --分析开始位置对应时间,主要用于核对分析结果,同时显示和保存点之间的差异
  begin_position_desc         varchar2(2000)             ,  --分析开始行所对应描述内容,用于对比文件是否变动
  end_position_line           integer                    ,  --分析结束位置对应行号
  end_position_time           TIMESTAMP(3)               ,  --分析结束位置对应时间,
  end_position_desc           varchar2(2000)             ,  --分析结束行所对应描述内容,用于对比文件是否变动
  save_point_line             integer                    ,  --save_point对应的行号
  save_point_time             TIMESTAMP(3)               ,  --从本次开始截止这个时间,这之间发起的ti均执行完成,完成时间可以在此save_point之后
  save_point_desc             varchar2(2000)             ,  --save_point对应行所对应描述内容,用于对比文件是否变动
  description                 clob                       ,  --上次分析后,如果文件曾经有删除,则说明一下
  analysis_begin              date                       ,  --本次分析开始时间
  analysis_end                date                       ,  --本次分析结束时间
  cpu_time                    number                        --cpu时长,以秒为单位
);

alter table T_tm1slog add constraint pk_T_tm1slog primary key(analysis_id);

comment on table T_tm1slog is '分析概述';
comment on column T_tm1slog.analysis_id         is '分析编号';
comment on column T_tm1slog.db_started_line     is 'C1 数据库启动行号';
comment on column T_tm1slog.db_started_time     is '数据库启动的时间戳';
comment on column T_tm1slog.db_shutdowned_line  is 'C1 数据库关闭行号';
comment on column T_tm1slog.db_shutdowned_time  is 'C1 数据库关闭时间戳';
comment on column T_tm1slog.begin_position_line is '分析开始位置对应行号';
comment on column T_tm1slog.begin_position_time is '分析开始位置对应时间,主要用于核对分析结果,同时显示和保存点之间的差异';
comment on column T_tm1slog.begin_position_desc is '分析开始行所对应描述内容,用于对比文件是否变动';
comment on column T_tm1slog.end_position_line   is '分析结束位置对应行号';
comment on column T_tm1slog.end_position_time   is '分析结束位置对应时间,';
comment on column T_tm1slog.end_position_desc   is '分析结束行所对应描述内容,用于对比文件是否变动';
comment on column T_tm1slog.save_point_line     is 'save_point对应的行号';
comment on column T_tm1slog.save_point_time     is '从本次开始截止这个时间,这之间发起的ti均执行完成,完成时间可以在此save_point之后';
comment on column T_tm1slog.save_point_desc     is 'save_point对应行所对应描述内容,用于对比文件是否变动';
comment on column T_tm1slog.description         is '上次分析后,如果文件曾经有删除,则说明一下';
comment on column T_tm1slog.analysis_begin      is '本次分析开始时间';
comment on column T_tm1slog.analysis_end        is '本次分析结束时间';
comment on column T_tm1slog.cpu_time            is 'cpu时长,以秒为单位';

create table t_tm1slog_ti(
  analysis_id                 integer                    ,  --分析编号
  line_number                 integer                    ,  --日志中行号,用于简化校核,对应启动时间的行号,如果没有启动,则为结束时间的行号
  ti_process_type             varchar2(50)               ,  --引入执行对象类型TM1.Chore,TM1.Process等,以便后续使用层级来搜索启动关系
  ti_name                     varchar2(500)              ,  --ti名称
  run_from_process_type       varchar2(50)               ,  --启动程序类型,例如是TM1.Chore,TM1.Process
  run_from_process_name       varchar2(500)              ,  --启动程序名称,可能使用executed by chore或run from process启动当前程序
  parent_begin_time           TIMESTAMP(3)               ,  --父对象的实例启动时间,用于层级搜索时,获取父,不考虑同时刻存在多个实例情况,此时实际无法确定父
  executed_by_user            varchar2(500)              ,  --启动用户
  begin_time                  TIMESTAMP(3)               ,  --ti开始执行时间
  end_time                    TIMESTAMP(3)               ,  --ti执行完成时间,null表示未处理完成
  duration                    number                     ,  --cpu时长,以秒为单位,主要是便于查询执行时间长的ti
  description                 varchar2(2000)             ,  --简短描述,便于分析当前可能的描述情况
  return_code                 integer                    ,  --执行结果,0--已正常完成执行,1--已完成执行,但存在错误,2--执行已中止,3--执行取消,4--祖先chore关闭,-1--表示未知的方式,用于排除执行时错误 null则表示未处理完成
  return_message              clob                          --返回错误信息,正常完成执行时,无错误信息
);

alter table t_tm1slog_ti add constraint pk_t_tm1slog_ti primary key(analysis_id,line_number);
alter table t_tm1slog_ti add constraint fk_t_tm1slog_ti
  foreign key (analysis_id) references T_tm1slog(analysis_id);

create index idx1_t_tm1slog_ti on t_tm1slog_ti(ti_name);
create index idx2_t_tm1slog_ti on t_tm1slog_ti(begin_time);

comment on table t_tm1slog_ti is 'ti分析日志';
comment on column T_tm1slog_ti.analysis_id      is '分析编号';
comment on column T_tm1slog_ti.line_number      is '日志中行号,用于简化校核';
comment on column T_tm1slog_ti.ti_process_type  is '引入执行对象类型TM1.Chore,TM1.Process等,以便后续使用层级来搜索启动关系';
comment on column T_tm1slog_ti.ti_name          is 'ti名称';
comment on column T_tm1slog_ti.run_from_process_type is '启动程序类型,例如是TM1.Chore,TM1.Process';
comment on column T_tm1slog_ti.run_from_process_name is '启动程序名称,可能使用executed by chore或run from process启动当前程序';
comment on column T_tm1slog_ti.parent_begin_time is '父对象的实例启动时间,用于层级搜索时,获取父,不考虑同时刻存在多个实例情况,此时实际无法确定父';
comment on column T_tm1slog_ti.executed_by_user is '启动用户';
comment on column T_tm1slog_ti.begin_time       is 'ti开始执行时间';
comment on column T_tm1slog_ti.end_time         is 'ti执行完成时间';
comment on column T_tm1slog_ti.duration         is 'cpu时长,以秒为单位,主要是便于查询执行时间长的ti';
comment on column T_tm1slog_ti.description      is '简短描述,便于分析当前可能的描述情况';
comment on column T_tm1slog_ti.return_code      is '执行结果,0--已正常完成执行,1--已完成执行,但存在错误,2--执行已中止';
comment on column T_tm1slog_ti.return_message   is '返回错误信息,正常完成执行时,无错误信息';

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值