From f831636b304b8d817b2e14e3873c130b1a48f328 Mon Sep 17 00:00:00 2001 From: SonglinLyu <111941624+SonglinLyu@users.noreply.github.com> Date: Fri, 19 Jan 2024 21:52:37 -0800 Subject: [PATCH] change audit log file format into json (#349) * change audit log file into json format * pass unit test * fix it error * fix merge error * rerun * rerun * update docs for log * fix pr comment * fix pr comment * fix coding style * fix pr comment * fix permission mode * add prefix for audit log file * add audit log readability test * fix coding style * fix audit log ut * define audit prefix * fix audit log ut * test asan bug * fix asan bug * fix ut asan * fix asan ut * fix ut asan * rerun ut asan * return asan ut --------- Co-authored-by: Shipeng Qi --- .../5.ecosystem-tools/4.log.md | 37 +++++- .../5.ecosystem-tools/4.log.md | 37 +++++- include/tools/lgraph_log.h | 116 +++++++++++++++--- procedures/demo/log_demo.cpp | 31 +++++ src/core/audit_logger.h | 91 ++++++++------ test/test_audit_logger.cpp | 29 +++++ 6 files changed, 279 insertions(+), 62 deletions(-) create mode 100644 procedures/demo/log_demo.cpp diff --git a/docs/en-US/source/5.developer-manual/5.ecosystem-tools/4.log.md b/docs/en-US/source/5.developer-manual/5.ecosystem-tools/4.log.md index b05ab1d2b2..df1fd8dfb1 100644 --- a/docs/en-US/source/5.developer-manual/5.ecosystem-tools/4.log.md +++ b/docs/en-US/source/5.developer-manual/5.ecosystem-tools/4.log.md @@ -11,11 +11,11 @@ TuGraph keeps two types of logs: server logs and audit logs. Server logs record ### 2.1.Server Log Configuration Items The output directory of server logs can be specified through the `log_dir` configuration item. The level of log can be specified through the `verbose` configuration item. -The `log_dir` configuration item is empty by default. If `log_dir` is empty, then all logs will be write to the console, the maximum size of a single log file is 256MB. +The `log_dir` configuration item is empty by default. If `log_dir` configuration item is empty, then all logs will be write to the console(nothing will be written to the console if the `log_dir` configuration item is empty under daemon mode); if `log_dir` configuration item is configured specifically, log files will bew generated under that path. The maximum size of a single log file is 256MB. The `verbose` configuration item controls the level of log, and is divided into three levels of `0, 1, 2`. Ther verbosity of log record grows as the number grows. The default level is `1`. When the level is set to `2`, the server will print logs in `DEBUG` level and above. When the level is set to `1`, the server will print logs in `INFO` level and above. When the level is set to `0`, the server will print log in `ERROR` level and above. -### 2.3.Example of Server Log Output Macro Usage +### 2.2.Example of Server Log Output Macro Usage If the developer wants to add logs to the code during development, they can refer to the following example: @@ -36,8 +36,39 @@ void LogExample() { You can also refer to the log macro usage in test/test_lgraph_log.cpp. +### 2.3.Procedure log + +Developers can use the log function to output debug information that they need to the log for looking up and assisting development during the development of procedures. Debug information will be output to the same log file as the server log (if `log_dir` configuration item is not specified, it will also be output to the console) + +#### 2.3.1.Cpp procedure +Please use the log macro provided in 2.2 to output debug information, and avoid using output methods such as cout or printf. For specific usage, please refer to the following sample code (see `procedures/demo/log_demo.cpp` for details) + +``` +#include +#include "lgraph/lgraph.h" +#include "tools/lgraph_log.h" // add log dependency +using namespace lgraph_api; + +void LogExample() { + LOG_DEBUG() << "This is a debug level log message."; + LOG_INFO() << "This is a info level log message."; + LOG_WARN() << "This is a warning level log message."; + LOG_ERROR() << "This is a error level log message."; +} + +extern "C" bool Process(GraphDB& db, const std::string& request, std::string& response) { + response = "TuGraph log demo"; + LogExample(); + return true; +} +``` +After inserting the above sample code into the database as a procedure and running it, you can see the corresponding log entries in the log file. + +#### 2.3.1.python procedure +Please use Python's built-in print to output debug information. The debug information will be merged into a WARN-level log entry and output to the log file after the procedure is executed. + ## 3.Audit log Audit logs record each request and response, as well as the user who sent the request and when the request received. Audit logging can only be turned on or off. The results can be queried using the TuGraph visualization tool and the REST API. -To enable the Audit Log, you need to set the `enable_audit_log` parameter to `true` in the configuration file. For the configuration file and parameter descriptions, see:[Tugraph Running/Service configuration](../2.running/2.tugraph-running.md/#4service-configuration) \ No newline at end of file +To enable the Audit Log, you need to set the `enable_audit_log` parameter to `true` in the configuration file. For the configuration file and parameter descriptions, see:[Tugraph Running/Service configuration](../2.running/2.tugraph-running.md/#4service-configuration) diff --git a/docs/zh-CN/source/5.developer-manual/5.ecosystem-tools/4.log.md b/docs/zh-CN/source/5.developer-manual/5.ecosystem-tools/4.log.md index 71b875ddb2..f2cde8edca 100644 --- a/docs/zh-CN/source/5.developer-manual/5.ecosystem-tools/4.log.md +++ b/docs/zh-CN/source/5.developer-manual/5.ecosystem-tools/4.log.md @@ -12,11 +12,11 @@ TuGraph 保留两种类型的日志:服务器日志和审计日志。服务器 服务器日志的输出位置可以通过`log_dir`配置指定。服务器日志详细程度可通过`verbose`配置项指定。 -`log_dir`配置项默认为空。若`log_dir`为空,则所有日志会输出到控制台;若手动指定`log_dir`,则日志文件会生成在对应的路径下面,单个日志文件最大大小为256MB。 +`log_dir`配置项默认为空。若`log_dir`配置项为空,则所有日志会输出到控制台(daemon模式下若log_dir配置项为空则不会向console输出任何日志);若手动指定`log_dir`配置项,则日志文件会生成在对应的路径下面。单个日志文件最大大小为256MB。 `verbose`配置项控制日志的详细程度,从粗到细分为`0, 1, 2`三个等级,默认等级为`1`。等级为`2`时,日志记录最详细,服务器将打印`DEBUG`及以上等级的全部日志信息;等级为`1`时,服务器将仅打印`INFO`等级及以上的主要事件的日志;等级为`0`时,服务器将仅打印`ERROR`等级及以上的错误日志。 -### 2.3.服务器日志输出宏使用示例 +### 2.2.服务器日志输出宏使用示例 如果开发者在开发过程中希望在代码中添加日志,可以参考如下示例 @@ -36,8 +36,39 @@ void LogExample() { ``` 更多用法可以参考test/test_lgraph_log.cpp中的日志宏的使用方法 +### 2.3.存储过程日志 + +用户在存储过程的编写过程中可以使用日志功能将所需的调试信息输出到日志中进行查看,辅助开发。调试信息会输出到与服务器日志相同的日志文件中(如未指定`log_dir`则同样输出至console) + +#### 2.3.1.cpp存储过程 +请使用2.2中提供的log宏输出调试信息,避免使用cout或者printf等输出方式。具体使用方式可参考如下示例代码(详见`procedures/demo/log_demo.cpp`) + +``` +#include +#include "lgraph/lgraph.h" +#include "tools/lgraph_log.h" // add log dependency +using namespace lgraph_api; + +void LogExample() { + LOG_DEBUG() << "This is a debug level log message."; + LOG_INFO() << "This is a info level log message."; + LOG_WARN() << "This is a warning level log message."; + LOG_ERROR() << "This is a error level log message."; +} + +extern "C" bool Process(GraphDB& db, const std::string& request, std::string& response) { + response = "TuGraph log demo"; + LogExample(); + return true; +} +``` +将以上示例代码作为存储过程插入数据库并运行后,可以在日志文件中看到相应的日志条目。 + +#### 2.3.1.python存储过程 +请使用python自带的print输出调试信息,调试信息会在存储过程运行结束后合并为一条WARN等级的日志条目输出至日志文件中。 + ## 3.审计日志 审核日志记录每个请求和响应,以及发送请求的用户以及收到请求的时间。审核日志只能是打开或关闭状态。可以使用 TuGraph 可视化工具和 REST API 查询结果。 -开启审计日志需要在配置文件中将`enable_audit_log`参数设置为`true`。配置文件和配置参数说明详见:[数据库运行/服务配置](../2.running/2.tugraph-running.md/#4服务配置)。 \ No newline at end of file +开启审计日志需要在配置文件中将`enable_audit_log`参数设置为`true`。配置文件和配置参数说明详见:[数据库运行/服务配置](../2.running/2.tugraph-running.md/#4服务配置)。 diff --git a/include/tools/lgraph_log.h b/include/tools/lgraph_log.h index 1837f88815..180d1e918f 100644 --- a/include/tools/lgraph_log.h +++ b/include/tools/lgraph_log.h @@ -27,6 +27,7 @@ #include #include #include +#include "tools/json.hpp" namespace lgraph_log { @@ -38,11 +39,41 @@ namespace expr = boost::log::expressions; namespace keywords = boost::log::keywords; using boost::shared_ptr; +using json = nlohmann::json; typedef sinks::synchronous_sink< sinks::text_file_backend > file_sink; typedef sinks::synchronous_sink< sinks::text_ostream_backend > stream_sink; typedef sinks::synchronous_sink< sinks::text_ostream_backend > ut_sink; +// Define log macro +#define LGRAPH_LOG(LEVEL) BOOST_LOG_SEV(::lgraph_log::debug_logger::get(), \ + ::lgraph_log::severity_level::LEVEL) \ + << ::lgraph_log::logging::add_value("Line", __LINE__) \ + << ::lgraph_log::logging::add_value("File", __FILE__) \ + +#define LOG_DEBUG() LGRAPH_LOG(DEBUG) +#define LOG_INFO() LGRAPH_LOG(INFO) +#define LOG_WARN() LGRAPH_LOG(WARNING) +#define LOG_ERROR() LGRAPH_LOG(ERROR) + +#define LOG_FATAL() lgraph_log::FatalLogger(__FILE__, __LINE__) + +#define FMA_UT_LOG(LEVEL) BOOST_LOG_SEV(::lgraph_log::debug_logger::get(), \ + LEVEL) \ + << ::lgraph_log::logging::add_value("Line", __LINE__) \ + << ::lgraph_log::logging::add_value("File", __FILE__) \ + +#define AUDIT_LOG() BOOST_LOG(::lgraph_log::audit_logger::get()) + +#define AUDIT_PREFIX "audit_" + +// LogType includes the following type: debug, audit +// debug type for general log +// audit type for audit log +#define DEBUG_TYPE "debug" +#define AUDIT_TYPE "audit" +BOOST_LOG_ATTRIBUTE_KEYWORD(log_type_attr, "LogType", std::string) + enum severity_level { TRACE, DEBUG, @@ -128,7 +159,8 @@ class LoggerManager { keywords::enable_final_rotation = false, keywords::auto_flush = true, keywords::rotation_size = rotation_size_)); - file_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_); + file_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_ && + log_type_attr == DEBUG_TYPE); file_sink_->set_formatter(&formatter); logging::core::get()->add_sink(file_sink_); @@ -143,7 +175,8 @@ class LoggerManager { boost::shared_ptr< std::ostream >(console_stream_)); } stream_sink_->locked_backend()->auto_flush(true); - stream_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_); + stream_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_ && + log_type_attr == DEBUG_TYPE); stream_sink_->set_formatter(&formatter); logging::core::get()->add_sink(stream_sink_); @@ -166,9 +199,11 @@ class LoggerManager { void SetLevel(severity_level level) { level_ = level; if (!log_dir_.empty()) { - file_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_); + file_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_ && + log_type_attr == DEBUG_TYPE); } else { - stream_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_); + stream_sink_->set_filter(expr::attr< severity_level >("Severity") >= level_ && + log_type_attr == DEBUG_TYPE); } } @@ -237,6 +272,9 @@ class LoggerManager { BOOST_LOG_INLINE_GLOBAL_LOGGER_INIT(debug_logger, src::severity_logger_mt< severity_level >) { src::severity_logger_mt< severity_level > lg; + attrs::constant< std::string > debug_type(DEBUG_TYPE); + lg.add_attribute("LogType", debug_type); + // Init empty console log first if not inited if (!LoggerManager::GetInstance().IsInited()) { boost::shared_ptr< stream_sink > empty_sink = @@ -249,16 +287,62 @@ BOOST_LOG_INLINE_GLOBAL_LOGGER_INIT(debug_logger, src::severity_logger_mt< sever return lg; }; -// Define log macro -#define LGRAPH_LOG(LEVEL) BOOST_LOG_SEV(::lgraph_log::debug_logger::get(), \ - ::lgraph_log::severity_level::LEVEL) \ - << ::lgraph_log::logging::add_value("Line", __LINE__) \ - << ::lgraph_log::logging::add_value("File", __FILE__) \ +BOOST_LOG_INLINE_GLOBAL_LOGGER_INIT(audit_logger, src::logger_mt) { + src::logger_mt lg; + attrs::constant< std::string > audit_type(AUDIT_TYPE); + lg.add_attribute("LogType", audit_type); + return lg; +}; -#define LOG_DEBUG() LGRAPH_LOG(DEBUG) -#define LOG_INFO() LGRAPH_LOG(INFO) -#define LOG_WARN() LGRAPH_LOG(WARNING) -#define LOG_ERROR() LGRAPH_LOG(ERROR) +class AuditLogger { + private: + std::string log_dir_; + size_t rotation_size_ = 256*1024*1024; + boost::shared_ptr< file_sink > audit_sink_; + bool global_inited_ = false; + + public: + void Init(std::string log_dir = "logs/", size_t rotation_size = 256*1024*1024) { + // Set up log directory + log_dir_ = log_dir; + if (log_dir_.back() != '/') { + log_dir_ += '/'; + } + rotation_size_ = rotation_size; + + // Set up sink for audit log + audit_sink_ = boost::shared_ptr< file_sink > (new file_sink( + keywords::file_name = log_dir_ + AUDIT_PREFIX + "%Y%m%d_%H%M%S.log", + keywords::open_mode = std::ios_base::out | std::ios_base::app, + keywords::enable_final_rotation = false, + keywords::auto_flush = true, + keywords::rotation_size = rotation_size_)); + audit_sink_->locked_backend()->set_file_collector(sinks::file::make_collector( + keywords::target = log_dir_)); + audit_sink_->locked_backend()->scan_for_files(); + audit_sink_->set_filter(log_type_attr == AUDIT_TYPE); + + // Add sinks to log core + logging::core::get()->add_sink(audit_sink_); + + global_inited_ = true; + } + + // write a json record to log file. + void WriteLog(json log_msg) { + AUDIT_LOG() << log_msg.dump(); + } + + // remove sink from log core + void Close() { + logging::core::get()->remove_sink(audit_sink_); + } + + static AuditLogger& GetInstance() { + static AuditLogger instance; + return instance; + } +}; class FatalLogger { public: @@ -282,11 +366,5 @@ class FatalLogger { std::string file_; int line_; }; -#define LOG_FATAL() lgraph_log::FatalLogger(__FILE__, __LINE__) - -#define FMA_UT_LOG(LEVEL) BOOST_LOG_SEV(::lgraph_log::debug_logger::get(), \ - LEVEL) \ - << ::lgraph_log::logging::add_value("Line", __LINE__) \ - << ::lgraph_log::logging::add_value("File", __FILE__) \ } // namespace lgraph_log diff --git a/procedures/demo/log_demo.cpp b/procedures/demo/log_demo.cpp new file mode 100644 index 0000000000..a4d6fdb989 --- /dev/null +++ b/procedures/demo/log_demo.cpp @@ -0,0 +1,31 @@ +/** + * Copyright 2022 AntGroup CO., Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + */ + +#include +#include "lgraph/lgraph.h" +#include "tools/lgraph_log.h" // add log dependency +using namespace lgraph_api; + +void LogExample() { + LOG_DEBUG() << "This is a debug level log message."; + LOG_INFO() << "This is a info level log message."; + LOG_WARN() << "This is a warning level log message."; + LOG_ERROR() << "This is a error level log message."; +} + +extern "C" bool Process(GraphDB& db, const std::string& request, std::string& response) { + response = "TuGraph log demo"; + LogExample(); + return true; +} diff --git a/src/core/audit_logger.h b/src/core/audit_logger.h index 35e2799004..05435a507c 100644 --- a/src/core/audit_logger.h +++ b/src/core/audit_logger.h @@ -92,8 +92,10 @@ class AuditLogger { public: // take string of local time, then convert it to second (int64_t) of local time - static inline bool FilePathToTime(int64_t& t, const std::string& s) { - if (s.size() != 15) return false; + static inline bool FilePathToTime(int64_t& t, const std::string& path) { + if (path.size() != 25) return false; + if (path.find(AUDIT_PREFIX) == 0) return false; + const std::string s = path.substr(6, 15); #ifdef _WIN32 lgraph_api::DateTime::YMDHMSF ymdhms; @@ -169,6 +171,26 @@ class AuditLogger { } } + inline bool ParseAuditLog(std::string path, std::string& log_line, lgraph::LogMessage& msg) { + if (log_line == "") { + LOG_DEBUG() << "Error parsing audit log from string " << path; + return false; + } + lgraph_log::json log_msg = lgraph_log::json::parse(log_line); + msg.set_index(log_msg["index"]); + msg.set_time(log_msg["time"]); + msg.set_begin_end(log_msg["is_end_log"]); + msg.set_success(log_msg["success"]); + msg.set_content(log_msg["content"]); + msg.set_read_write(log_msg["read_write"]); + if (!log_msg["is_end_log"]) { + msg.set_user(log_msg["user"]); + msg.set_graph(log_msg["graph"]); + msg.set_type(log_msg["type"]); + } + return true; + } + static bool IsEnabled() { return enabled_.load(std::memory_order_acquire); } static void SetEnable(bool enable) { enabled_.store(enable, std::memory_order_release); } @@ -200,11 +222,9 @@ class AuditLogger { sort(files.begin(), files.end()); index_ = GetLogIdx(); LOG_DEBUG() << "VertexIndex of audit log : " << index_; - if (!SetLogFileName()) - throw std::runtime_error("Failed to get audit log file name from int64_t."); - LOG_DEBUG() << "Open audit log file : " << file_name_; - file_.Open(file_name_, fma_common::OutputFmaStream::DEFAULT_BLOCK_SIZE, std::ofstream::app); - if (!file_.Good()) throw std::runtime_error("Failed to open audit log file for writing."); + + // init lgraph_log AuditLogger + lgraph_log::AuditLogger::GetInstance().Init(dir_); // thread clear the expired AuditLog in certain time, execute each hour if (expire_second_ > 0) { @@ -266,8 +286,10 @@ class AuditLogger { } lgraph::LogMessage msg; - while (input.Good()) { - if (!ReadNextAuditLog(input, msg)) break; // broken log entry + fma_common::StreamLineReader reader(input); + auto lines = reader.ReadAllLines(); + for (auto& line : lines) { + if (!ParseAuditLog(input.Path(), line, msg)) break; // broken log entry int64_t idx = msg.index(); if (!msg.begin_end()) { // begin if (begin_idx < idx) begin_idx = idx; @@ -287,16 +309,6 @@ class AuditLogger { AutoWriteLock lock(lock_); int64_t log_time = lgraph_api::DateTime::LocalNow().MicroSecondsSinceEpoch(); - if ((file_.Size() >= file_size_limit_) && (last_log_time_ < log_time)) { - file_.Close(); - if (!SetLogFileName(log_time)) - throw std::runtime_error("Failed to get audit log file name from int64_t."); - LOG_DEBUG() << "Open a new audit log file : " << file_name_; - file_.Open(file_name_, fma_common::OutputFmaStream::DEFAULT_BLOCK_SIZE, - std::ofstream::app); - if (!file_.Good()) - throw std::runtime_error("Failed to open audit log file for writing."); - } last_log_time_ = log_time; int64_t index; @@ -305,20 +317,21 @@ class AuditLogger { index = idx; else index = ++index_; - msg.set_index(index); - msg.set_time(log_time); - msg.set_begin_end(is_end_log); - msg.set_success(success); - msg.set_content(content); - msg.set_read_write(read_write); + + lgraph_log::json log_msg; + log_msg["index"] = index; + log_msg["time"] = log_time; + log_msg["is_end_log"] = is_end_log; + log_msg["success"] = success; + log_msg["content"] = content; + log_msg["read_write"] = read_write; if (!is_end_log) { - msg.set_user(user); - msg.set_graph(graph); - msg.set_type(type); + log_msg["user"] = user; + log_msg["graph"] = graph; + log_msg["type"] = type; } - int len = static_cast(msg.ByteSizeLong()); - file_.Write((char*)(&len), sizeof(len)); - file_.Write(msg.SerializeAsString().c_str(), len); + lgraph_log::AuditLogger::GetInstance().WriteLog(log_msg); + return index; } @@ -405,9 +418,10 @@ class AuditLogger { lgraph::LogMessage msg; LOG_DEBUG() << "Search audit log " << f; - - while (input.Good()) { - if (!ReadNextAuditLog(input, msg)) break; // broken log entry + fma_common::StreamLineReader reader(input); + auto lines = reader.ReadAllLines(); + for (auto& line : lines) { + if (!ParseAuditLog(input.Path(), line, msg)) break; // broken log entry if ((user.length() != 0) && (user != msg.user())) // check user continue; auto msg_time = msg.time(); @@ -443,6 +457,7 @@ class AuditLogger { } log->second.end_time = lgraph_api::DateTime(msg_time).ToString(); log->second.success = msg.success(); + if (log->second.success) log->second.content = log->second.content + " Successful"; else @@ -497,9 +512,10 @@ class AuditLogger { lgraph::LogMessage msg; LOG_DEBUG() << "Search audit log " << f; - - while (input.Good()) { - if (!ReadNextAuditLog(input, msg)) break; // broken log entry + fma_common::StreamLineReader reader(input); + auto lines = reader.ReadAllLines(); + for (auto& line : lines) { + if (!ParseAuditLog(input.Path(), line, msg)) break; // broken log entry if ((user.length() != 0) && (user != msg.user())) // check user continue; auto msg_time = msg.time(); @@ -587,6 +603,7 @@ class AuditLogger { void Close() { AutoWriteLock lock(lock_); file_.Close(); + lgraph_log::AuditLogger::GetInstance().Close(); } }; diff --git a/test/test_audit_logger.cpp b/test/test_audit_logger.cpp index ba76747743..28c55efdba 100644 --- a/test/test_audit_logger.cpp +++ b/test/test_audit_logger.cpp @@ -57,7 +57,11 @@ TEST_F(TestAuditLogger, AuditLogger) { logger.WriteLog(true, "user1", "graph1", LogApiType::Cypher, true, true, "", log_id); UT_EXPECT_EQ(log_id, 1); fma_common::SleepS(1); + + // test log parsing // logs = logger.GetLog(time_t(), end); + UT_LOG() << "begin time: " << beg; + UT_LOG() << "end time: " << end; logs = logger.GetLog(beg, end); UT_EXPECT_EQ(logs.size(), 1); auto &log = logs[0]; @@ -73,6 +77,31 @@ TEST_F(TestAuditLogger, AuditLogger) { idx = logger.GetLogIdx(); UT_EXPECT_EQ(idx, 1); UT_LOG() << "logger Idx:" << idx; + + // test log entry's human readability. + auto &log_0 = logs[0]; + std::string line; + std::vector lines; + std::vector log_files; + for (const auto & entry : std::filesystem::directory_iterator("./audit")) { + log_files.push_back(entry.path().generic_string()); + } + UT_EXPECT_EQ(log_files.size(), 1); + std::ifstream log_file(log_files[0]); + UT_EXPECT_TRUE(log_file.is_open()); + while (getline(log_file, line)) { + lines.push_back(line); + } + UT_EXPECT_EQ(lines.size(), 2); + lgraph_log::json log_msg_0 = lgraph_log::json::parse(lines[0]); + lgraph_log::json log_msg_1 = lgraph_log::json::parse(lines[1]); + std::string log_content_0 = log_msg_0["content"]; + UT_EXPECT_EQ(log_0.user, log_msg_0["user"]); + UT_EXPECT_EQ(log_0.graph, log_msg_0["graph"]); + UT_EXPECT_EQ(log_0.content.substr(0, log_content_0.length()), log_content_0); + UT_EXPECT_EQ("", log_msg_1["content"]); + log_file.close(); + std::string log_info(2048, 'a'); int size = 4500; for (int i = 0; i < size; i++) {