summaryrefslogtreecommitdiff
path: root/src/buildtool/logging/log_sink_file.hpp
blob: c6f0126c759b061eddf1a18873fe6eca55078555 (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Copyright 2022 Huawei Cloud Computing Technology 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.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef INCLUDED_SRC_BUILDTOOL_LOGGING_LOG_SINK_FILE_HPP
#define INCLUDED_SRC_BUILDTOOL_LOGGING_LOG_SINK_FILE_HPP

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

#ifdef __unix__
#include <sys/time.h>
#else
#error "Non-unix is not supported yet"
#endif

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

/// \brief Thread-safe map of mutexes.
template <class TKey>
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(TKey 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(TKey const& key) noexcept -> std::mutex& {
        std::lock_guard lock(mutex_);
        return map_[key];
    }

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

class LogSinkFile final : public ILogSink {
  public:
    enum class Mode : std::uint8_t {
        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
        FileMutexes().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);
        const auto* cont_prefix = "  ";

        {
            std::lock_guard lock{FileMutexes().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_;

    [[nodiscard]] static auto FileMutexes() noexcept -> MutexMap<std::string>& {
        static MutexMap<std::string> instance{};
        return instance;
    }
};

#endif  // INCLUDED_SRC_BUILDTOOL_LOGGING_LOG_SINK_FILE_HPP