summaryrefslogtreecommitdiff
path: root/src/buildtool/logging/log_sink_file.hpp
blob: 2ca1b75bf1157dfdf0d6ccbe6f085ec4feb88ea5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#ifndef INCLUDED_SRC_BUILDTOOL_LOGGING_LOG_SINK_FILE_HPP
#define INCLUDED_SRC_BUILDTOOL_LOGGING_LOG_SINK_FILE_HPP

#include <cstdio>
#include <filesystem>
#include <functional>
#include <iterator>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <unordered_map>

#ifdef __unix__
#include <sys/time.h>
#endif

#include "fmt/chrono.h"
#include "fmt/core.h"
#include "gsl-lite/gsl-lite.hpp"
#include "src/buildtool/logging/log_sink.hpp"
#include "src/buildtool/logging/logger.hpp"

/// \brief Thread-safe map of mutexes.
template <class T_Key>
class MutexMap {
  public:
    /// \brief Create mutex for key and run callback if successfully created.
    /// Callback is executed while the internal map is still held exclusively.
    void Create(T_Key const& key, std::function<void()> const& callback) {
        std::lock_guard lock(mutex_);
        if (not map_.contains(key)) {
            [[maybe_unused]] auto& mutex = map_[key];
            callback();
        }
    }
    /// \brief Get mutex for key, creates mutex if key does not exist.
    [[nodiscard]] auto Get(T_Key const& key) noexcept -> std::mutex& {
        std::lock_guard lock(mutex_);
        return map_[key];
    }

  private:
    std::mutex mutex_{};
    std::unordered_map<T_Key, std::mutex> map_{};
};

class LogSinkFile final : public ILogSink {
  public:
    enum class Mode {
        Append,    ///< Append if log file already exists.
        Overwrite  ///< Overwrite log file with each new program instantiation.
    };

    static auto CreateFactory(std::filesystem::path const& file_path,
                              Mode file_mode = Mode::Append) -> LogSinkFactory {
        return
            [=] { return std::make_shared<LogSinkFile>(file_path, file_mode); };
    }

    LogSinkFile(std::filesystem::path const& file_path, Mode file_mode)
        : file_path_{std::filesystem::weakly_canonical(file_path).string()} {
        // create file mutex for canonical path
        file_mutexes_.Create(file_path_, [&] {
            if (file_mode == Mode::Overwrite) {
                // clear file contents
                if (gsl::owner<FILE*> file =
                        std::fopen(file_path_.c_str(), "w")) {
                    std::fclose(file);
                }
            }
        });
    }
    ~LogSinkFile() noexcept final = default;
    LogSinkFile(LogSinkFile const&) noexcept = delete;
    LogSinkFile(LogSinkFile&&) noexcept = delete;
    auto operator=(LogSinkFile const&) noexcept -> LogSinkFile& = delete;
    auto operator=(LogSinkFile&&) noexcept -> LogSinkFile& = delete;

    /// \brief Thread-safe emitting of log messages to file.
    /// Race-conditions for file writes are resolved via a separate mutexes for
    /// every canonical file path shared across all instances of this class.
    void Emit(Logger const* logger,
              LogLevel level,
              std::string const& msg) const noexcept final {
#ifdef __unix__  // support nanoseconds for timestamp
        timespec ts{};
        clock_gettime(CLOCK_REALTIME, &ts);
        auto timestamp = fmt::format(
            "{:%Y-%m-%d %H:%M:%S}.{}", fmt::localtime(ts.tv_sec), ts.tv_nsec);
#else
        auto timestamp = fmt::format(
            "{:%Y-%m-%d %H:%M:%S}", fmt::localtime(std::time(nullptr));
#endif

        std::ostringstream id{};
        id << "thread:" << std::this_thread::get_id();
        auto thread = id.str();

        auto prefix = fmt::format(
            "{}, [{}] {}", thread, timestamp, LogLevelToString(level));

        if (logger != nullptr) {
            // append logger name
            prefix = fmt::format("{} ({})", prefix, logger->Name());
        }
        prefix = fmt::format("{}:", prefix);
        auto cont_prefix = std::string(prefix.size(), ' ');

        {
            std::lock_guard lock{file_mutexes_.Get(file_path_)};
            if (gsl::owner<FILE*> file = std::fopen(file_path_.c_str(), "a")) {
                using it = std::istream_iterator<ILogSink::Line>;
                std::istringstream iss{msg};
                for_each(it{iss}, it{}, [&](auto const& line) {
                    fmt::print(file, "{} {}\n", prefix, line);
                    prefix = cont_prefix;
                });
                std::fclose(file);
            }
        }
    }

  private:
    std::string file_path_{};
    static inline MutexMap<std::string> file_mutexes_{};
};

#endif  // INCLUDED_SRC_BUILDTOOL_LOGGING_LOG_SINK_FILE_HPP