Bicameral Engine
GPU-resident engine for a magic-research open-world game
Loading...
Searching...
No Matches
log_sinks.cpp
Go to the documentation of this file.
1// log_sinks.cpp — ログの出力先 3 種の実装(コンソール・OutputDebugString・ファイル)。
2//
3// どのシンクも Logger の鍵の中から呼ばれる(Logger::Write)ので、シンク自身は鍵を持たない。
4// core はプリコンパイルヘッダを持たないので、windows.h はここで直接 include する。
5#include "core/log_sinks.h"
6
7#include <windows.h>
8
9#include <algorithm>
10#include <cstdio>
11#include <ctime>
12#include <fstream>
13#include <system_error>
14#include <vector>
15
16#include "core/aliases.h"
17#include "core/hresult.h"
18#include "core/paths.h"
19#include "core/unicode.h"
20
21namespace bicameral {
22 namespace {
23
24 constexpr size_t MAX_LOG_FILES = 20;
25 constexpr std::wstring_view LOG_FILE_PREFIX = L"bicameral-";
26 constexpr std::wstring_view LOG_FILE_EXTENSION = L".log";
27
28 // --- コンソール ---
29
30 // VT のエスケープシーケンスで色を付ける(Windows 10 以降のコンソールが対応)
31 std::string_view ColorOf(Level level) {
32 switch (level) {
33 case Level::Trace:
34 case Level::Debug: return "\x1b[90m"; // 灰
35 case Level::Warning: return "\x1b[33m"; // 黄
36 case Level::Error:
37 case Level::Fatal: return "\x1b[31m"; // 赤
38 default: return "";
39 }
40 }
41
42 class ConsoleSink final : public LogSink {
43 public:
45 // 日本語をそのまま出すため、コンソールの出力コードページを UTF-8 にする(終わったら戻す)
46 m_previousCodePage = GetConsoleOutputCP();
47 if (m_previousCodePage != 0 && m_previousCodePage != CP_UTF8)
48 SetConsoleOutputCP(CP_UTF8);
49
50 // リダイレクトされている(ランナー・パイプ)ときは GetConsoleMode が失敗する → 色を付けない
51 m_output = GetStdHandle(STD_OUTPUT_HANDLE);
52 if (!GetConsoleMode(m_output, &m_previousMode))
53 return;
54
55 m_useColor = SetConsoleMode(m_output, m_previousMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
56 }
57
58 ~ConsoleSink() override {
59 std::fflush(stdout);
60 if (m_useColor)
61 SetConsoleMode(m_output, m_previousMode);
62
63 if (m_previousCodePage != 0 && m_previousCodePage != CP_UTF8)
64 SetConsoleOutputCP(m_previousCodePage);
65 }
66
67 void Write(const LogRecord& record) override {
68 const std::string line = FormatLogLine(record, record.level >= Level::Warning);
69 const std::string_view color = m_useColor ? ColorOf(record.level) : std::string_view{};
70 if (color.empty())
71 std::fprintf(stdout, "%s\n", line.c_str());
72 else
73 std::fprintf(stdout, "%.*s%s\x1b[0m\n", static_cast<int>(color.size()), color.data(), line.c_str());
74
75 // 落ちる直前の行を失わないよう毎行書き出す(フレームごとの大量ログはまだ無い。増えたら見直す)
76 std::fflush(stdout);
77 }
78
79 void Flush() override { std::fflush(stdout); }
80
81 private:
82 HANDLE m_output = nullptr;
83 DWORD m_previousMode = 0;
85 bool m_useColor = false;
86 };
87
88 // --- デバッガ ---
89
90 class DebuggerSink final : public LogSink {
91 public:
92 void Write(const LogRecord& record) override {
93 std::string line;
94 if (record.level >= Level::Warning) {
95 // VS の出力ウィンドウは "フルパス(行): " で始まる行をダブルクリックでその場所へ飛べる
96 line = std::format("{}({}): {} {} | {}\n", record.location.file_name(), record.location.line(),
97 LevelName(record.level), ChannelName(record.channel), record.message);
98 } else
99 line = FormatLogLine(record, false) + '\n';
100
101 OutputDebugStringW(ToWide(line).c_str());
102 }
103 };
104
105 // --- ファイル ---
106
107 class FileSink final : public LogSink {
108 public:
109 explicit FileSink(std::ofstream stream) : m_stream(std::move(stream)) {}
110 // 破棄のときの書き出しは std::ofstream のデストラクタに任せる(デストラクタから例外を出さないため)
111
112 void Write(const LogRecord& record) override { m_stream << FormatLogLine(record, true) << '\n'; }
113 void Flush() override { m_stream.flush(); }
114
115 private:
116 std::ofstream m_stream;
117 };
118
119 // std::error_code の説明を UTF-8 で。MSVC の system_category().message() は ANSI コードページで返すので使わない
120 std::string DescribeErrorCode(const std::error_code& error) {
121 if (error.category() != std::system_category())
122 return error.message();
123 return DescribeHresult(HRESULT_FROM_WIN32(static_cast<DWORD>(error.value())));
124 }
125
126 std::tm LocalNow() {
127 const std::time_t now = std::time(nullptr);
128 std::tm local{};
129 localtime_s(&local, &now);
130
131 return local;
132 }
133
134 bool IsLogFileName(const fs::path& path) {
135 const std::wstring name = path.filename().wstring();
136 return name.starts_with(LOG_FILE_PREFIX) && name.ends_with(LOG_FILE_EXTENSION);
137 }
138
139 // 新しいファイルを 1 つ足しても MAX_LOG_FILES 個に収まるように、古いものから消す。
140 // 名前に日時が入っているので、名前順 = 古い順。消せなくても続ける(ログが無いよりはまし)
141 void RemoveOldLogFiles(const fs::path& directory) {
142 std::error_code error;
143 std::vector<fs::path> files;
144 for (const auto& entry : fs::directory_iterator(directory, error)) {
145 if (entry.is_regular_file(error) && IsLogFileName(entry.path()))
146 files.push_back(entry.path());
147 }
148
149 if (files.size() < MAX_LOG_FILES)
150 return;
151
152 rng::sort(files);
153 const size_t removeCount = files.size() - (MAX_LOG_FILES - 1);
154 for (size_t i = 0; i < removeCount; ++i)
155 fs::remove(files[i], error);
156 }
157
158 } // namespace
159
160 std::unique_ptr<LogSink> CreateConsoleSink() {
161 return std::make_unique<ConsoleSink>();
162 }
163
164 std::unique_ptr<LogSink> CreateDebuggerSink() {
165 return std::make_unique<DebuggerSink>();
166 }
167
168 std::expected<fs::path, std::string> OpenLogFile(Logger& logger, const fs::path& directory) {
169 std::error_code error;
170 fs::create_directories(directory, error);
171 if (error) {
172 return std::unexpected(
173 std::format("ログのフォルダを作れない: {}: {}", ToUtf8(directory.wstring()), DescribeErrorCode(error)));
174 }
175
176 RemoveOldLogFiles(directory);
177
178 // --- ファイルを開く ---
179 const std::tm now = LocalNow();
180
181 const std::wstring fileName = std::format(L"{}{:04}{:02}{:02}-{:02}{:02}{:02}{}", LOG_FILE_PREFIX,
182 now.tm_year + 1900, now.tm_mon + 1, now.tm_mday, now.tm_hour,
183 now.tm_min, now.tm_sec, LOG_FILE_EXTENSION);
184
185 fs::path path = directory / fileName; // const にしない(return で move させる)
186 std::ofstream stream(path, std::ios::binary | std::ios::app); // 同じ秒に 2 回起動したら追記になる
187 if (!stream)
188 return std::unexpected(std::format("ログのファイルを開けない: {}", ToUtf8(path.wstring())));
189
190 stream << std::format("# Bicameral Engine log {:04}-{:02}-{:02} {:02}:{:02}:{:02}\n", now.tm_year + 1900,
191 now.tm_mon + 1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec);
192 stream << "# [経過秒] 重大度 チャンネル | 本文 (ファイル:行)\n";
193 logger.AddSink(std::make_unique<FileSink>(std::move(stream)));
194
195 return path;
196 }
197
199 return ExecutableDirectory() / L"logs";
200 }
201
202} // namespace bicameral
void AddSink(std::unique_ptr< LogSink > sink)
Definition log.cpp:103
void Write(const LogRecord &record) override
Definition log_sinks.cpp:67
void Write(const LogRecord &record) override
Definition log_sinks.cpp:92
void Write(const LogRecord &record) override
std::string_view ColorOf(Level level)
Definition log_sinks.cpp:31
constexpr std::wstring_view LOG_FILE_EXTENSION
Definition log_sinks.cpp:26
constexpr std::wstring_view LOG_FILE_PREFIX
Definition log_sinks.cpp:25
void RemoveOldLogFiles(const fs::path &directory)
std::string DescribeErrorCode(const std::error_code &error)
std::expected< fs::path, std::string > OpenLogFile(Logger &logger, const fs::path &directory)
std::string_view ChannelName(Channel channel)
Definition log.cpp:35
fs::path DefaultLogDirectory()
fs::path ExecutableDirectory()
Definition paths.cpp:12
std::string FormatLogLine(const LogRecord &record, bool withLocation)
Definition log.cpp:79
std::string DescribeHresult(HResult result)
Definition hresult.cpp:88
std::string ToUtf8(std::wstring_view text)
Definition unicode.cpp:10
std::string_view LevelName(Level level)
Definition log.cpp:43
Level
Definition log.h:53
std::unique_ptr< LogSink > CreateConsoleSink()
std::unique_ptr< LogSink > CreateDebuggerSink()
std::wstring ToWide(std::string_view text)
Definition unicode.cpp:22
std::string_view message
Definition log.h:73
std::source_location location
Definition log.h:74
Channel channel
Definition log.h:71