wazuh 默认安装到 /var/ossec目录下。
我基于manager端进行分析,和agent一样。默认启动ossec-logcollector进程去搜集日志:比如 snort日志、auditd日志、syslog日志等。
入口函数代码在src/logcollector/main.c中。
int main(int argc, char **argv)
{
/* 初始化处理: 命令行解析、权限限制、 消息队列哈希表、加载配置*/
...
/* 初始化消息队列, 用来将数据通过队列顺序处理, 后面的w_create_input_threads和w_create_output_threads创建线程组来处理日志队列 */
w_msg_hash_queues_init();
/* 这里会初始化logsk,这个会涉及到后面创建out thread的数量 */
if (LogCollectorConfig(cfg) < 0) {
merror_exit(CONFIG_ERROR, cfg);
}
...
/* 默认在ossec.conf中没有定义socket配置,所以走下面的代码,logsk只有两个元素并且name为空。
* 后面在w_msg_hash_queues_add_entry函数中往msg_queues_table中写入一个元素,只有一个out thread
*/
if (logsk == NULL) {
os_calloc(2, sizeof(logsocket), logsk);
logsk[0].name = NULL;
logsk[0].location = NULL;
logsk[0].mode = 0;
logsk[0].prefix = NULL;
logsk[1].name = NULL;
logsk[1].location = NULL;
logsk[1].mode = 0;
logsk[1].prefix = NULL;
}
/* 主要处理入口*/
LogCollectorStart();
}
LogCollectorConfig的实现:
/* Read the config file (the localfiles) */
int LogCollectorConfig(const char *cfgfile)
{
int modules = 0;
logreader_config log_config;
modules |= CLOCALFILE;
modules |= CSOCKET;
log_config.config = NULL;
log_config.globs = NULL;
log_config.socket_list = NULL;
log_config.agent_cfg = 0;
accept_remote = getDefine_Int("logcollector", "remote_commands", 0, 1);
log_config.accept_remote = accept_remote;
/* 这里读取internal_options.conf和local_internal_options.conf的配置信息 */
loop_timeout = getDefine_Int("logcollector", "loop_timeout", 1, 120);
open_file_attempts = getDefine_Int("logcollector", "open_attempts", 0, 998);
vcheck_files = getDefine_Int("logcollector", "vcheck_files", 0, 1024);
maximum_lines = getDefine_Int("logcollector", "max_lines", 0, 1000000);
maximum_files = getDefine_Int("logcollector", "max_files", 1, 100000);
sock_fail_time = getDefine_Int("logcollector", "sock_fail_time", 1, 3600);
sample_log_length = getDefine_Int("logcollector", "sample_log_length", 1, 4096);
force_reload = getDefine_Int("logcollector", "force_reload", 0, 1);
reload_interval = getDefine_Int("logcollector", "reload_interval", 1, 86400);
reload_delay = getDefine_Int("logcollector", "reload_delay", 0, 30000);
free_excluded_files_interval = getDefine_Int("logcollector", "exclude_files_interval", 1, 172800);
// 读取配置文件ossec.conf的入口,ReadConfig函数通过调用read_main_elements=>Read_Socket最终往log_config.socket_list添加元素
if (ReadConfig(modules, cfgfile, &log_config, NULL) < 0) {
return (OS_INVALID);
}
#ifdef CLIENT
modules |= CAGENT_CONFIG;
log_config.agent_cfg = 1;
ReadConfig(modules, AGENTCONFIG, &log_config, NULL);
log_config.agent_cfg = 0;
#endif
logff = log_config.config;
globs = log_config.globs;
logsk = log_config.socket_list;//将读取到的socket的配置赋值给logsk,这里可能影响out thread的创建
return (1);
}
src/logcollector/logcollector.c为主要处理逻辑:
void LogCollectorStart()
{
...
/* 后面很多地方会用到这个变量 */
logreader *current;
...
/* 设置*/
set_sockets();
...
for (i = 0;; i++) {
/* 更新 current信息,为后面做铺垫*/
if (f_control = update_current(¤t, &i, &j), f_control) {
if (f_control == NEXT_IT) {
continue;
} else {
break;
}
...
/*绝大部分read函数在这个函数中进行设置 :read_snortfull、read_audit、read_syslog*/
set_read(current, i, j);
}
}
/* 创建发送数据的线程*/
w_create_output_threads();
/* 创建读取数据的线程 */
w_create_input_threads();
...
}
set_read 实现:
/* wazuh支持的日志非常多*/
void set_read(logreader *current, int i, int j) {
minfo("in set_read func logformat : %s.", current->logformat);
int tg;
current->command = NULL;
current->ign = 0;
/* Initialize the files */
if (current->ffile) {
/* Day must be zero for all files to be initialized */
_cday = 0;
if (update_fname(i, j)) {
handle_file(i, j, 1, 1);
} else {
merror_exit(PARSE_ERROR, current->ffile);
}
} else {
handle_file(i, j, 1, 1);
}
tg = 0;
if (current->target) {
while (current->target[tg]) {
mdebug1("Socket target for '%s' -> %s", current->file, current->target[tg]);
tg++;
}
}
/* Get the log type */
if (strcmp("snort-full", current->logformat) == 0) {
current->read = read_snortfull;/* snort日志的读取方式*/
}
#ifndef WIN32
if (strcmp("ossecalert", current->logformat) == 0) {
current->read = read_ossecalert;
}
#endif
else if (strcmp("nmapg", current->logformat) == 0) {
current->read = read_nmapg;
} else if (strcmp("json", current->logformat) == 0) {
current->read = read_json;//wazuh与suricata联动的时候使用的就是json格式的日志
} else if (strcmp("mysql_log", current->logformat) == 0) {
current->read = read_mysql_log;
} else if (strcmp("mssql_log", current->logformat) == 0) {
current->read = read_mssql_log;
} else if (strcmp("postgresql_log", current->logformat) == 0) {
current->read = read_postgresql_log;
} else if (strcmp("djb-multilog", current->logformat) == 0) {
if (!init_djbmultilog(current)) {
merror(INV_MULTILOG, current->file);
if (current->fp) {
fclose(current->fp);
current->fp = NULL;
}
current->file = NULL;
}
current->read = read_djbmultilog;
} else if (strncmp(current->logformat, "multi-line:", 11) == 0) {
current->read = read_multiline;
} else if (strcmp("audit", current->logformat) == 0) {
current->read = read_audit;/* audit日志的读取方式*/
} else {
#ifdef WIN32
if (current->filter_binary) {
/* If the file is empty, set it to UCS-2 LE */
if (FileSizeWin(current->file) == 0) {
current->ucs2 = UCS2_LE;
current->read = read_ucs2_le;
mdebug2("File '%s' is empty. Setting encoding to UCS-2 LE.",current->file);
return;
}
}
if(current->ucs2 == UCS2_LE){
mdebug1("File '%s' is UCS-2 LE",current->file);
current->read = read_ucs2_le;
return;
}
if(current->ucs2 == UCS2_BE){
mdebug1("File '%s' is UCS-2 BE",current->file);
current->read = read_ucs2_be;
return;
}
#endif
/* 设置 syslog的读取方式*/
current->read = read_syslog;
}
}
设置好读取方式,后面启动线程进行读取、发送;
先来看看如何读取auditd日志的:
/* type=SYSCALL msg=audit(1572939835.214:135): arch=c000003e syscall=54 success=yes exit=0 a0=4 a1=0 a2=40 a3=234a100 items=0 ppid=130689 pid=130690 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=10 comm="iptables" exe="/sbin/xtables-multi" key=(null)
这是auditd的系统调用日志, 基本就是按照这个去解析的
*/
void *read_audit(logreader *lf, int *rc, int drop_it) {
char *cache[MAX_CACHE];
char header[MAX_HEADER] = { '\0' };
int icache = 0;
char buffer[OS_MAXSTR];
char *id;
char *p;
size_t z;
int64_t offset = 0;
int64_t rbytes = 0;
int lines = 0;
*rc = 0;
for (offset = w_ftell(lf->fp); fgets(buffer, OS_MAXSTR, lf->fp) && (!maximum_lines || lines < maximum_lines) && offset >= 0; offset += rbytes) {
rbytes = w_ftell(lf->fp) - offset;
/* Flow control */
if (rbytes <= 0) {
break;
}
lines++;
if (buffer[rbytes - 1] == '\n') {
buffer[rbytes - 1] = '\0';
if ((int64_t)strlen(buffer) != rbytes - 1)
{
mdebug2("Line in '%s' contains some zero-bytes (valid=" FTELL_TT " / total=" FTELL_TT "). Dropping line.", lf->file, FTELL_INT64 strlen(buffer), FTELL_INT64 rbytes - 1);
continue;
}
} else {
if (rbytes == OS_MAXSTR - 1) {
// Message too large, discard line
for (offset += rbytes; fgets(buffer, OS_MAXSTR, lf->fp); offset += rbytes) {
rbytes = w_ftell(lf->fp) - offset;
/* Flow control */
if (rbytes <= 0) {
break;
}
if (buffer[rbytes - 1] == '\n') {
break;
}
}
} else if (feof(lf->fp)) {
mdebug2("Message not complete. Trying again: '%s'", buffer);
if (fseek(lf->fp, offset, SEEK_SET) < 0) {
merror(FSEEK_ERROR, lf->file, errno, strerror(errno));
break;
}
}
break;
}
// Extract header: "type=\.* msg=audit(\d+.\d+:\d+):"
if (strncmp(buffer, "type=", 5) || !((id = strstr(buffer + 5, "msg=audit(")) && (p = strstr(id += 10, "): ")))) {
merror("Discarding audit message because of invalid syntax.");
break;
}
z = p - id;
if (strncmp(id, header, z)) {
// Current message belongs to another event: send cached messages
if (icache > 0)
audit_send_msg(cache, icache, lf->file, drop_it, lf->log_target);
// Store current event
*cache = strdup(buffer);
icache = 1;
strncpy(header, id, z < MAX_HEADER ? z : MAX_HEADER - 1);
} else {
// The header is the same: store
if (icache == MAX_CACHE)
merror("Discarding audit message because cache is full.");
else
cache[icache++] = strdup(buffer);
}
}
if (icache > 0)
audit_send_msg(cache, icache, lf->file, drop_it, lf->log_target);
mdebug2("Read %d lines from %s", lines, lf->file);
return NULL;
}
下面看看在读取线程中如何触发读取的:
/* 默认读取一个线程,可通过配置文件配置input_threads参数进行设置多线程处理 */
void w_create_input_threads(){
int i;
N_INPUT_THREADS = getDefine_Int("logcollector", "input_threads", N_MIN_INPUT_THREADS, 128);
#ifdef WIN32
w_mutex_init(&win_el_mutex, &win_el_mutex_attr);
w_mutexattr_destroy(&win_el_mutex_attr);
#endif
for(i = 0; i < N_INPUT_THREADS; i++) {
#ifndef WIN32
w_create_thread(w_input_thread,NULL);
#else
if (CreateThread(NULL,
0,
(LPTHREAD_START_ROUTINE)w_input_thread,
NULL,
0,
NULL) == NULL) {
merror(THREAD_ERROR);
}
#endif
}
}
实现:
void * w_input_thread(__attribute__((unused)) void * t_id){
...
/* Daemon loop */
while (1) {
...
/* Check which file is available */
for (i = 0, j = -1;; i++) {
...
/* Finally, send to the function pointer to read it */
/* 读取地方*/
current->read(current, &r, 0);
/* Check for error */
if (!ferror(current->fp)) {
...
}
/* If ferror is set */
else {
merror(FREAD_ERROR, current->file, errno, strerror(errno));
#ifndef WIN32
if (fseek(current->fp, 0, SEEK_END) < 0)
#else
if (1)
#endif
{
...
/* 在这里进行处理日志 调用w_msg_hash_queues_push 将数据放入队列中*/
current->read(current, &r, 1);
#endif
}
/* Increase the error count */
current->ign++;
...
}
}
}
}
return NULL;
}
看看日志进入队列后怎么处理的:
void w_create_output_threads(){
unsigned int i;
const OSHashNode *curr_node;
/* 循环遍历哈希表,创建于哈希表元素一样多的out thread*/
for(i = 0; i <= msg_queues_table->rows; i++){
if(msg_queues_table->table[i]){
curr_node = msg_queues_table->table[i];
/* Create one thread per valid hash entry */
if(curr_node->key){
#ifndef WIN32
/* 使用哈希表的key (例如agent)作为参数传入进去,每个线程负责一种key的队列来处理*/
w_create_thread(w_output_thread, curr_node->key);
#else
...
endif
}
}
}
}
/* 该线程只负责处理queue_name的队列*/
void * w_output_thread(void * args){
char *queue_name = args;
w_message_t *message;
w_msg_queue_t *msg_queue;
if (msg_queue = OSHash_Get(msg_queues_table, queue_name), !msg_queue) {
mwarn("Could not found the '%s'.", queue_name);
return NULL;
}
while(1)
{
/* 将消息从队列中取出来,发送给logr_queue, 默认是/var/ossec/queue/ossec/queue 就是unix socket, 如果在agent端就是发给ossec-agentd进程 */
message = w_msg_queue_pop(msg_queue);
if (SendMSGtoSCK(logr_queue, message->buffer, message->file, message->queue_mq, message->log_target) < 0) {
merror(QUEUE_SEND);
/* 如果发送失败 创建 /var/ossec/queue/ossec/queue unix socket */
if ((logr_queue = StartMQ(DEFAULTQPATH, WRITE)) < 0) {
merror_exit(QUEUE_FATAL, DEFAULTQPATH);
}
}
free(message->file);
free(message->buffer);
free(message);
}
return NULL;
}
yes
yes
no
no
no
smtp.example.wazuh.com
[email protected]
[email protected]
12
alerts.log
日志会放入到writer_queue、writer_queue_log中,ossec-analysisd在启动的时候会创建,一系列队列和线程池。
#ifndef TESTRULE
__attribute__((noreturn))
void OS_ReadMSG(int m_queue)
#else
__attribute__((noreturn))
void OS_ReadMSG_analysisd(int m_queue)
#endif
{
Eventinfo *lf = NULL;
int i;
/* 初始化日志系统 */
OS_InitLog();
...
/* 初始化前面说的队列*/
w_init_queues();
/* Queue stats */
w_get_initial_queues_size();
...
/* Create message handler thread */
w_create_thread(ad_input_main, &m_queue);
/* 创建写归档日志的线程, 从writer_queue队列中获取日志 */
w_create_thread(w_writer_thread,NULL);
/* 创建写日志线程 ,从writer_queue_log队列中取日志*/
w_create_thread(w_writer_log_thread,NULL);
/* Create statistical log writer thread */
w_create_thread(w_writer_log_statistical_thread,NULL);
/* Create firewall log writer thread */
w_create_thread(w_writer_log_firewall_thread,NULL);
/* Create FTS log writer thread */
w_create_thread(w_writer_log_fts_thread,NULL);
/* Create log rotation thread */
w_create_thread(w_log_rotate_thread,NULL);
/* Create decode syscheck threads */
for(i = 0; i < num_decode_syscheck_threads;i++){
w_create_thread(w_decode_syscheck_thread,NULL);
}
/* Create decode syscollector threads */
for(i = 0; i < num_decode_syscollector_threads;i++){
w_create_thread(w_decode_syscollector_thread,NULL);
}
/* Create decode hostinfo threads */
for(i = 0; i < num_decode_hostinfo_threads;i++){
w_create_thread(w_decode_hostinfo_thread,NULL);
}
/* Create decode rootcheck threads */
for(i = 0; i < num_decode_rootcheck_threads;i++){
w_create_thread(w_decode_rootcheck_thread,NULL);
}
/* Create decode Security Configuration Assessment threads */
for(i = 0; i < num_decode_sca_threads;i++){
w_create_thread(w_decode_sca_thread,NULL);
}
/* 创建decoder线程池,用于处理agent转发过来的日志数据,放入decode_queue_event_output队列中 */
for(i = 0; i < num_decode_event_threads;i++){
w_create_thread(w_decode_event_thread,NULL);
}
/* 创建规则匹配线程池,decoder线程池处理好(DecodeEvent函数来做)的数据:从
* decode_queue_event_output拿出数据, 循环遍历初始时从ruleset/rules目录下读取处理规则,
* 来判定是否匹配成功,如果成功,是否有alert需求, alert level是否大于等于ossec.conf中的
* log_alert_level, 如果满足,生成alert log, 放入writer_queue_log队列中。后面由
* w_writer_log_thread来处理, 其中会调用OS_Log 往logs/alerts/alerts.log中写日志
*/
for(i = 0; i < num_rule_matching_threads;i++){
w_create_thread(w_process_event_thread,(void *) (intptr_t)i);
}
/* Create decode winevt threads */
for(i = 0; i < num_decode_winevt_threads;i++){
w_create_thread(w_decode_winevt_thread,NULL);
}
/* Create State thread */
w_create_thread(w_analysisd_state_main,NULL);
mdebug1("Startup completed. Waiting for new messages..");
while (1) {
sleep(1);
}
}
/* 写日志线程, 将writer_queue_log的数据拿出来, 根据配置信息,决定写入日志*/
void * w_writer_log_thread(__attribute__((unused)) void * args ){
Eventinfo *lf;
while(1){
/* Receive message from queue */
if (lf = queue_pop_ex(writer_queue_log), lf) {
w_mutex_lock(&writer_threads_mutex);
w_inc_alerts_written();
if (Config.custom_alert_output) {
__crt_ftell = ftell(_aflog);
OS_CustomLog(lf, Config.custom_alert_output_format);
} else if (Config.alerts_log) {
__crt_ftell = ftell(_aflog);
OS_Log(lf);//这里写入 alerts.log中
} else if(Config.jsonout_output){
__crt_ftell = ftell(_jflog);
}
/* Log to json file */
if (Config.jsonout_output) {
jsonout_output_event(lf);
}
...
w_mutex_unlock(&writer_threads_mutex);
Free_Eventinfo(lf);
}
}
}
/* 从decode_queue_event_input队列中,将数据拿出来, 根据ruleset/decoders/下的decoder规则decode数据,
* 并存入decode_queue_event_output中
*/
void * w_decode_event_thread(__attribute__((unused)) void * args){
...
while(1){
/* Receive message from queue */
if (msg = queue_pop_ex(decode_queue_event_input), msg) {
...
if (msg[0] == CISCAT_MQ) {
if (!DecodeCiscat(lf, &sock)) {
w_free_event_info(lf);
free(msg);
continue;
}
} else {
DecodeEvent(lf, &decoder_match);
}
...
if (queue_push_ex_block(decode_queue_event_output,lf) < 0) {
Free_Eventinfo(lf);
}
// 更新统计计数
w_inc_decoded_events();
}
}
}
/* 该线程从decode_queue_event_output拿出decode好的数据,然后,循环遍历规则,检测是否是恶意操作
* (匹配的细节在OS_CheckIfRuleMatch函数中,并忽略level为0的规则, 如果匹配成功,立即终止循环),
* 如果检测到了,将产生日志,并放入 writer_queue_log中,如果配置logall 会放入writer_queue队列中,
* 如果配置active response了,会产生响应,所谓的联动
*/
void * w_process_event_thread(__attribute__((unused)) void * id){
...
while(1) {
RuleNode *rulenode_pt;
lf_logall = NULL;
/* Extract decoded event from the queue */
if (lf = queue_pop_ex(decode_queue_event_output), !lf) {
continue;
}
...
/* 从这里开始遍历规则 */
rulenode_pt = OS_GetFirstRule();
if (!rulenode_pt) {
merror_exit("Rules in an inconsistent state. Exiting.");
}
do {
if (lf->decoder_info->type == OSSEC_ALERT) {
if (!lf->generated_rule) {
goto next_it;
}
/* Process the alert */
t_currently_rule = lf->generated_rule;
}
/* Categories must match */
else if (rulenode_pt->ruleinfo->category !=
lf->decoder_info->type) {
continue;
}
/* 如果没有匹配上,继续下一条规则 */
else if ((t_currently_rule = OS_CheckIfRuleMatch(lf, rulenode_pt, &rule_match))
== NULL) {
continue;
}
/* Ignore level 0 */
if (t_currently_rule->level == 0) {
break;
}
/* Check ignore time */
if (t_currently_rule->ignore_time) {
if (t_currently_rule->time_ignored == 0) {
t_currently_rule->time_ignored = lf->generate_time;
}
/* If the current time - the time the rule was ignored
* is less than the time it should be ignored,
* alert about the parent one instead
*/
else if ((lf->generate_time - t_currently_rule->time_ignored)
< t_currently_rule->ignore_time) {
if (t_currently_rule->prev_rule) {
t_currently_rule = (RuleInfo*)t_currently_rule->prev_rule;
w_FreeArray(lf->last_events);
} else {
break;
}
} else {
t_currently_rule->time_ignored = lf->generate_time;
}
}
/* Pointer to the rule that generated it */
lf->generated_rule = t_currently_rule;
/* Check if we should ignore it */
if (t_currently_rule->ckignore && IGnore(lf, t_id)) {
/* Ignore rule */
lf->generated_rule = NULL;
break;
}
/* Check if we need to add to ignore list */
if (t_currently_rule->ignore) {
AddtoIGnore(lf, t_id);
}
/* Log the alert if configured to */
if (t_currently_rule->alert_opts & DO_LOGALERT) {
lf->comment = ParseRuleComment(lf);
os_calloc(1, sizeof(Eventinfo), lf_cpy);
w_copy_event_for_log(lf,lf_cpy);
//将日志入队
if (queue_push_ex_block(writer_queue_log, lf_cpy) < 0) {
Free_Eventinfo(lf_cpy);
}
}
/* Execute an active response */
if (t_currently_rule->ar) {
int do_ar;
active_response **rule_ar;
rule_ar = t_currently_rule->ar;
while (*rule_ar) {
do_ar = 1;
if ((*rule_ar)->ar_cmd->expect & USERNAME) {
if (!lf->dstuser ||
!OS_PRegex(lf->dstuser, "^[a-zA-Z._0-9@?-]*$")) {
if (lf->dstuser) {
mwarn(CRAFTED_USER, lf->dstuser);
}
do_ar = 0;
}
}
if ((*rule_ar)->ar_cmd->expect & SRCIP) {
if (!lf->srcip ||
!OS_PRegex(lf->srcip, "^[a-zA-Z.:_0-9-]*$")) {
if (lf->srcip) {
mwarn(CRAFTED_IP, lf->srcip);
}
do_ar = 0;
}
}
if ((*rule_ar)->ar_cmd->expect & FILENAME) {
if (!lf->filename) {
do_ar = 0;
}
}
if (do_ar && execdq >= 0) {
OS_Exec(execdq, arq, lf, *rule_ar);
}
rule_ar++;
}
}
/* Copy the structure to the state memory of if_matched_sid */
if (t_currently_rule->sid_prev_matched) {
OSListNode *node;
w_mutex_lock(&t_currently_rule->mutex);
if (node = OSList_AddData(t_currently_rule->sid_prev_matched, lf), !node) {
merror("Unable to add data to sig list.");
} else {
lf->sid_node_to_delete = node;
}
w_mutex_unlock(&t_currently_rule->mutex);
}
/* Group list */
else if (t_currently_rule->group_prev_matched) {
unsigned int j = 0;
OSListNode *node;
w_mutex_lock(&t_currently_rule->mutex);
os_calloc(t_currently_rule->group_prev_matched_sz, sizeof(OSListNode *), lf->group_node_to_delete);
while (j < t_currently_rule->group_prev_matched_sz) {
if (node = OSList_AddData(t_currently_rule->group_prev_matched[j], lf), !node) {
merror("Unable to add data to grp list.");
} else {
lf->group_node_to_delete[j] = node;
}
j++;
}
w_mutex_unlock(&t_currently_rule->mutex);
}
lf->queue_added = 1;
os_calloc(1, sizeof(Eventinfo), lf_logall);
w_copy_event_for_log(lf, lf_logall);
w_free_event_info(lf);
OS_AddEvent(lf, last_events_list);
break;// 到这里来,说明匹配成功,不再匹配后面的规则
} while ((rulenode_pt = rulenode_pt->next) != NULL);
w_inc_processed_events();//更新统计计数
// 将日志入队
if (Config.logall || Config.logall_json){
if (!lf_logall) {
os_calloc(1, sizeof(Eventinfo), lf_logall);
w_copy_event_for_log(lf, lf_logall);
}
result = queue_push_ex(writer_queue, lf_logall);
if (result < 0) {
if(!reported_writer){
reported_writer = 1;
mwarn("Archive writer queue is full. %d", t_id);
}
Free_Eventinfo(lf_logall);
}
} else if (lf_logall) {
Free_Eventinfo(lf_logall);
}
next_it:
if (!lf->queue_added) {
w_free_event_info(lf);
}
}
}
到这里来,整个日志的流程基本就介绍完了。