singleton.h
#pragma once

#include <iostream>

namespace sky {
template <typename T>
class singleton {
public:
    singleton(const singleton&) = delete;

    singleton& operator=(const singleton&) = delete;

protected:
    singleton() noexcept = default;

    virtual ~singleton() = default;

public:
    static T& instance() noexcept(std::is_nothrow_constructible<T>::value) {
        static T ins{};
        return ins;
    }
};
}  // namespace sky
comm_func.h
#pragma once

#include <cstdint>
#include <ctime>
#include <string>
#include <string_view>
#include <vector>

namespace sky::comm_func {
bool create_dir(std::string_view dir);

std::string get_current_exe_dir();

std::string current_date_to_string(std::string_view fmt = std::string_view{"%Y-%m-%d %H:%M:%S"});

tm* get_cur_tm();

int32_t get_tid();
}  // namespace sky::comm_func
comm_func.cpp
#include "comm_func.h"

#include <chrono>
#include <filesystem>
#include <iostream>
#include <thread>

#include "logger.h"

namespace sky {
bool comm_func::create_dir(std::string_view dir) {
    std::error_code ec;
    std::filesystem::create_directories(std::filesystem::path(dir.data()), ec);
    if (ec) {
        LOG_ERROR << "comm_func::create_dir dir:" << dir << " error:" << ec.message();
        return false;
    }

    return true;
}

std::string comm_func::get_current_exe_dir() {
    std::error_code ec;
    auto path = std::filesystem::current_path(ec);
    if (ec) {
        LOG_ERROR << "comm_func::get_current_exe_dir error:" << ec.message();
        return {};
    }

    return path.string();
}

std::string comm_func::current_date_to_string(std::string_view fmt) {
    time_t rawtime;
    struct tm* time_info;
    char buffer[80];

    time(&rawtime);
    time_info = localtime(&rawtime);

    strftime(buffer, sizeof(buffer), fmt.data(), time_info);
    return {buffer};
}

tm* comm_func::get_cur_tm() {
    time_t t = time(nullptr);
    return localtime(&t);
}

int32_t comm_func::get_tid() {
#if _WIN32
    return GetCurrentThreadId();
#else
    return gettid();
#endif
}
}
logger.h
#pragma once

#include <atomic>
#include <chrono>
#include <cstring>
#include <fstream>
#include <mutex>
#include <sstream>
#include <string>
#include <string_view>

#include "comm_func.h"
#include "singleton.h"

#define LOG_FMT(level, fmt)                                                                            \
    if (sky::logger::instance().check_level(level))                                                    \
    sky::log_event() << sky::comm_func::current_date_to_string(std::string_view{"%y-%m-%d %H:%M:%S "}) \
                     << sky::comm_func::get_tid() << fmt

#define LOG_DEBUG LOG_FMT(sky::logger::log_level_type::llt_debug, " [DEBUG] ")
#define LOG_INFO LOG_FMT(sky::logger::log_level_type::llt_info, " [INFO] ")
#define LOG_WARN LOG_FMT(sky::logger::log_level_type::llt_warn, " [WARN] ")
#define LOG_ERROR LOG_FMT(sky::logger::log_level_type::llt_error, " [ERROR] ")

namespace sky {
class logger final : public singleton<logger> {
    friend class singleton<logger>;

public:
    enum log_level_type {
        llt_debug = 0,
        llt_info,
        llt_warn,
        llt_error,
        llt_off,
    };

private:
    logger();

public:
    ~logger() {
        std::lock_guard lock(mutex_);
        close_file();
    }

    bool start(std::string_view file_name = std::string_view{"default"}, uint16_t level = log_level_type::llt_debug,
               std::string_view dir = std::string_view{"logs"});

    void set_log_level(log_level_type level);

    bool check_level(log_level_type level) {
        return level_ <= level;
    }

    void write(const std::stringstream &log);

private:
    void close_file();

    void daily_scrolling();

private:
    std::ofstream file_{};
    std::string name_{};
    std::mutex mutex_{};
    std::atomic_uint16_t level_{};
    int32_t yday_{};
};

class log_event final {
public:
    log_event() = default;

    ~log_event() {
        log_ << "\n";
        logger::instance().write(log_);
    }

    template <typename T>
    log_event &operator<<(T const &v) {
        log_ << v;
        return *this;
    }

private:
    std::stringstream log_{};
};
}  // namespace sky
logger.cpp
#include "logger.h"

#include <iostream>

#include "define.h"

namespace sky {
logger::logger()
    : level_(log_level_type::llt_debug) {
}

bool logger::start(std::string_view file_name, uint16_t level, std::string_view dir) {
    std::lock_guard lock(mutex_);
    level_ = level;
    std::string path = comm_func::get_current_exe_dir();
    path.append(SLASH_STR).append(dir.data());
    if (!comm_func::create_dir(path)) {
        LOG_ERROR << "logger::start create dir error";
        return false;
    }

    name_ = path.append(SLASH_STR).append(file_name).append("_");
    daily_scrolling();
    return true;
}

void logger::set_log_level(log_level_type level) {
    level_ = level;
    if (level_ == log_level_type::llt_off) {
        std::lock_guard lock(mutex_);
        yday_ = 0;
        close_file();
    }
}

void logger::close_file() {
    if (file_.is_open()) {
        file_.flush();
        file_.close();
    }
}

void logger::daily_scrolling() {
    auto cur_yday = comm_func::get_cur_tm()->tm_yday;
    if (yday_ == cur_yday || name_.empty()) {
        return;
    }

    close_file();
    yday_ = cur_yday;
    std::string log_file_name = name_;
    log_file_name.append(comm_func::current_date_to_string("%Y_%m_%d.log"));
    file_.open(log_file_name, std::ios::app);
    if (file_.fail()) {
        std::cerr << "logger::check_file_name file:" << log_file_name << " open failed." << std::endl;
        return;
    }
}

void logger::write(const std::stringstream &log) {
    std::lock_guard lock(mutex_);
    daily_scrolling();
    if (!file_.fail()) {
        file_ << log.str();
        file_.flush();
    }
    std::cout << log.str();
}
}  // namespace sky