trainings/CreatingReliableSoftwareCpp/Presentation/logger.md

6.5 KiB

logger


source_loaction

Since C++20 we got a nice functionality to perform easy and efficient logging.

void log(const std::string_view message,
         const std::source_location location = 
               std::source_location::current())
{
    std::cout << std::format("{}({}:{}) '{}':{}\n",
                             location.file_name(),
                             location.line(),
                             location.column(),
                             location.function_name(),
                             message);
}
 
template <typename T> void fun(T x)
{
    log(x);
}
 
int main(int, char*[])
{
    log("Hello world!");
    fun("Hello C++20!");
}
prog.cc(24:8) 'int main(int, char**)': Hello world!
prog.cc(19:8) 'void fun(T) [with T = const char*]': Hello C++20!

It's easy to rebuild a little this solution and create own logger:

enum LogType {INF, WRN, ERR};

struct Logger {
    Logger(LogType logType, std::source_location location = std::source_location::current()): 
        logType_{logType},
        location_{location} {}

    Logger& operator<<(std::string_view message) {
        file_ << std::format("[{}]({}): {}({}:{}) `{}`: {}\n",
                             toString(logType_), 
                             std::chrono::system_clock::now(),
                             location_.file_name(),
                             location_.line(),
                             location_.column(),
                             location_.function_name(),
                             message);
        file_ << std::flush;

        return *this;
    }

private:
    std::string_view toString(LogType logType) {
        switch (logType) {
        case INF: return "I";
        case WRN: return "W";
        case ERR: return "E";
        default: return "UNKNOWN!";
        }
    }

    LogType logType_;
    std::source_location location_;
    static std::ofstream file_;
};

std::ofstream Logger::file_("log.txt");

Ok now is time to log sth

int main() {
    Logger(INF) << "Hello!" << "And Hi" << "And Dzien dobry!";
    Logger(WRN) << "Ojej!";
    Logger(ERR) << "Critical!!!" << "UPS!" << "Bad bad!";
}

And in file log.txt we got:

[I](2024-03-17 15:29:34.248747710): prog.cc(55:15) `int main()`: Hello!
[I](2024-03-17 15:29:34.248817593): prog.cc(55:15) `int main()`: And Hi
[I](2024-03-17 15:29:34.248832174): prog.cc(55:15) `int main()`: And Dzien dobry!
[W](2024-03-17 15:29:34.248843859): prog.cc(56:15) `int main()`: Ojej!
[E](2024-03-17 15:29:34.248855275): prog.cc(57:15) `int main()`: Critical!!!
[E](2024-03-17 15:29:34.248866476): prog.cc(57:15) `int main()`: UPS!
[E](2024-03-17 15:29:34.248877615): prog.cc(57:15) `int main()`: Bad bad!

Second version

We can also write a whole output when the logger will be destroyed, this allows to log only one line.

Logger::Logger(LogType logType, std::source_location location)
    : logType_{logType},
      location_{location} {
        file_ << std::format("[{}]({}): {}({}:{}) `{}`:",
                             toString(logType_), 
                             std::chrono::system_clock::now(),
                             location_.file_name(),
                             location_.line(),
                             location_.column(),
                             location_.function_name());
}

Logger::~Logger() {
    file_ << std::endl; // flush and new line
}

Logger& Logger::operator<<(const std::string& str) {
    file_ << " " << str;
    return *this;
}

int main() {
    Logger(INF) << "Hello!" << "And Hi" << "And Dzien dobry!";
    Logger(WRN) << "Ojej!";
    Logger(ERR) << "Critical!!!" << "UPS!" << "Bad bad!";
}

And in file log.txt we got:

[I](2024-03-17 15:29:34.248747710): prog.cc(55:15) `int main()`: Hello! And Hi And Dzien dobry!
[W](2024-03-17 15:29:34.248843859): prog.cc(56:15) `int main()`: Ojej!
[E](2024-03-17 15:29:34.248855275): prog.cc(57:15) `int main()`: Critical!!! UPS! Bad bad!

Ok but this is not thread safe!

Now we need to make the code a thread-safe and put the id of the thread. We need to lock a mutex before writing output to the file and keep it until we destroy a logger. Ofc we risk that someone will create a logger instance and don't destroy them in the following line, but we assume that we use it correctly. We can add a special function that wraps this for use (the most common is macro).

Logger::Logger(LogType logType, std::source_location location): logType_{logType}, location_{location} {
    formatted_ = std::format("[{}][th:{}]({}): {}({}:{}) `{}`:",
                             toString(logType_), 
                             std::this_thread::get_id(),
                             std::chrono::system_clock::now(),
                             location_.file_name(),
                             location_.line(),
                             location_.column(),
                             location_.function_name());
}

Logger& Logger::operator<<(const std::string& str) {
    formatted_ = std::format("{} {}", std::move(formatted_), str);
    return *this;
}

Logger::~Logger() {
    std::lock_guard lg(loggMutex_);
    loggFile_ << formatted_ << std::endl;
}

#define LOG_INF Logger(INF)
#define LOG_ERR Logger(ERR)
#define LOG_WRN Logger(WRN)

int main() {
    LOG_INF << "This" << "is" << "an" << "info";
    LOG_WRN << "This" << "is" << "a" << "warning";
    LOG_ERR << "This" << "is" << "an" << "error";
}
[I][th:1](2024-03-17 15:53:04.743470912): prog.cc(86:5) `int main()`: This is an info
[W][th:1](2024-03-17 15:53:04.743541096): prog.cc(87:5) `int main()`: This is a warning
[E][th:1](2024-03-17 15:53:04.743558245): prog.cc(88:5) `int main()`: This is an error

Cleanup

At the beginning of the program, we should also clear all logged data, this is easy to achieve:

// Always clear file when program start
std::ofstream Logger::loggFile_("log.txt", std::ofstream::trunc);