RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
Logger.h
Go to the documentation of this file.
1// Logger.h (replace the previous version)
2#pragma once
3#include <mutex>
4#include <unordered_map>
5#include <deque>
6#include <vector>
7#include <string>
8#include <functional>
9#include <algorithm>
10#include <memory>
11#include <ostream>
12#include <chrono>
13#include <cctype>
14
15class Logger
16{
17public:
18 enum class Level : uint8_t
19 {
20 Trace,
21 Debug,
22 Info,
23 Warn,
24 Error,
26 };
27
28 struct LogEntry
29 {
30 std::string text;
32 uint64_t tsMs; // epoch ms (render formats once)
33 // cached formatted time (optional; filled lazily)
34 mutable std::string tsStr;
35 };
36
37 using Sink = std::function<void(const std::string& channel, const LogEntry& e)>;
38
39 static Logger& instance()
40 {
41 static Logger L;
42 return L;
43 }
44
45 // --- Public API ----------------------------------------------------------
46
47 // Simple: default to Info (with auto-detect)
48 void log(const std::string& channel, const std::string& line)
49 {
50 log(channel, Level::Info, line);
51 }
52
53 // Structured: you can set level explicitly if you want
54 void log(const std::string& channel, Level lvl, const std::string& line)
55 {
56 LogEntry e{line, lvl, nowMs_(), {}};
57 // auto-detect if level was Info but looks like warn/error
58 if (lvl == Level::Info)
59 e.level = autoDetect_(line);
60 commit_(channel, std::move(e));
61 }
62
63 // Snapshot of entries for a channel
64 std::vector<LogEntry> getChannel(const std::string& channel)
65 {
66 std::lock_guard<std::mutex> lk(m_);
67 auto it = channels_.find(channel);
68 if (it == channels_.end())
69 return {};
70 return {it->second.begin(), it->second.end()};
71 }
72
73 std::vector<std::string> channels() const
74 {
75 std::lock_guard<std::mutex> lk(m_);
76 std::vector<std::string> names;
77 names.reserve(channels_.size());
78 for (auto& kv : channels_)
79 names.push_back(kv.first);
80 return names;
81 }
82
83 void clearChannel(const std::string& channel)
84 {
85 std::lock_guard<std::mutex> lk(m_);
86 channels_[channel].clear();
87 }
88
89 void setChannelCapacity(size_t cap)
90 {
91 std::lock_guard<std::mutex> lk(m_);
92 capacity_ = cap;
93 }
94
95 // Sinks now receive structured LogEntry
96 int addSink(const Sink& s)
97 {
98 std::lock_guard<std::mutex> lk(m_);
99 int id = ++nextSinkId_;
100 sinks_.emplace_back(id, s);
101 return id;
102 }
103 void removeSink(int id)
104 {
105 std::lock_guard<std::mutex> lk(m_);
106 sinks_.erase(std::remove_if(sinks_.begin(), sinks_.end(),
107 [id](auto& p)
108 { return p.first == id; }),
109 sinks_.end());
110 }
112 {
113 std::lock_guard<std::mutex> lk(m_);
114 sinks_.clear();
115 }
116
117 // --- std::cout / std::cerr capture (unchanged externally) ---------------
119 {
120 std::lock_guard<std::mutex> lk(m_);
121 if (!coutRedirect_)
122 coutRedirect_ = std::make_unique<OstreamRedirect>(std::cout, "main", Level::Debug);
123 if (!cerrRedirect_)
124 cerrRedirect_ = std::make_unique<OstreamRedirect>(std::cerr, "main", Level::Error);
125 ensureSeed_();
126 }
128 {
129 std::lock_guard<std::mutex> lk(m_);
130 cerrRedirect_.reset();
131 coutRedirect_.reset();
132 }
133
134 // Helper to format ts once in UI (called from DebugConsole)
135 static std::string formatTs(uint64_t ms)
136 {
137 using namespace std::chrono;
138 auto tp = time_point<system_clock, milliseconds>(milliseconds(ms));
139 auto t = system_clock::to_time_t(tp);
140 auto ms_part = (int)(ms % 1000);
141 struct tm bt;
142#if defined(_WIN32)
143 localtime_s(&bt, &t);
144#else
145 localtime_r(&t, &bt);
146#endif
147 char buf[32];
148 std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d.%03d", bt.tm_hour, bt.tm_min, bt.tm_sec, ms_part);
149 return std::string(buf);
150 }
151
152private:
153 Logger() = default;
154 // inside Logger (same file), replace LineToLoggerBuf with this version:
155 class LineToLoggerBuf : public std::streambuf
156 {
157 public:
158 // pass an optional forced level (when set, it overrides auto-detect)
159 explicit LineToLoggerBuf(std::string channel, std::optional<Level> forced = std::nullopt) :
160 channel_(std::move(channel)), forcedLevel_(forced) {}
161
162 protected:
163 int overflow(int ch) override
164 {
165 if (ch == traits_type::eof())
166 return sync();
167 char c = static_cast<char>(ch);
168 buf_.push_back(c);
169 if (c == '\n')
170 flushLine_();
171 return ch;
172 }
173 std::streamsize xsputn(const char* s, std::streamsize count) override
174 {
175 std::streamsize w = 0;
176 for (; w < count; ++w)
177 {
178 char c = s[w];
179 buf_.push_back(c);
180 if (c == '\n')
181 flushLine_();
182 }
183 return w;
184 }
185 int sync() override
186 {
187 if (!buf_.empty())
188 {
189 if (buf_.back() == '\n')
190 buf_.pop_back();
191 if (forcedLevel_)
193 else
194 Logger::instance().log(channel_, buf_); // auto-detect path
195 buf_.clear();
196 }
197 return 0;
198 }
199
200 private:
202 {
203 if (!buf_.empty() && buf_.back() == '\n')
204 buf_.pop_back();
205 if (forcedLevel_)
207 else
209 buf_.clear();
210 }
211
212 std::string channel_;
213 std::string buf_;
214 std::optional<Level> forcedLevel_;
215 };
216
218 {
219 public:
220 // add Level override parameter
221 OstreamRedirect(std::ostream& os, const std::string& channel, std::optional<Level> forced = std::nullopt) :
222 os_(os), original_(os.rdbuf()), buf_(channel, forced)
223 {
224 os_.rdbuf(&buf_);
225 }
227 {
228 if (original_)
229 {
230 try
231 {
232 os_.rdbuf(original_);
233 }
234 catch (...)
235 {
236 }
237 }
238 }
239
244
245 private:
246 std::ostream& os_;
247 std::streambuf* original_;
249 };
250
251
253 {
254 if (!seeded_)
255 {
256 channels_["main"];
257 channels_["localtunnel"];
258 seeded_ = true;
259 }
260 }
261
262 static uint64_t nowMs_()
263 {
264 using namespace std::chrono;
265 return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
266 }
267
268 static Level autoDetect_(const std::string& s)
269 {
270 // very small heuristics (case-insensitive): [error], error:, fail, warn, etc.
271 auto has = [&](const char* needle)
272 {
273 auto it = std::search(
274 s.begin(), s.end(),
275 needle, needle + std::strlen(needle),
276 [](char a, char b)
277 { return std::tolower((unsigned char)a) == std::tolower((unsigned char)b); });
278 return it != s.end();
279 };
280 if (has("[error]") || has(" error") || has("failed") || has("exception"))
281 return Level::Error;
282 if (has("[warn]") || has(" warning"))
283 return Level::Warn;
284 // If you wish, detect [debug] → Debug, [trace] → Trace
285 if (has("[debug]"))
286 return Level::Debug;
287 if (has("[trace]"))
288 return Level::Trace;
289 return Level::Info;
290 }
291
292 void commit_(const std::string& channel, LogEntry e)
293 {
294 std::lock_guard<std::mutex> lk(m_);
295 auto& q = channels_[channel];
296 q.emplace_back(std::move(e));
297 while (q.size() > capacity_)
298 q.pop_front();
299 // fan-out sinks
300 auto& back = q.back();
301 for (auto& [id, s] : sinks_)
302 s(channel, back);
303 }
304
305 // data
306 mutable std::mutex m_;
307 size_t capacity_ = 4000;
308 std::unordered_map<std::string, std::deque<LogEntry>> channels_;
309 int nextSinkId_ = 0;
310 std::vector<std::pair<int, Sink>> sinks_;
311 bool seeded_ = false;
312 std::unique_ptr<OstreamRedirect> coutRedirect_;
313 std::unique_ptr<OstreamRedirect> cerrRedirect_;
314};
315
316//#pragma once
317//#include <mutex>
318//#include <unordered_map>
319//#include <deque>
320//#include <vector>
321//#include <string>
322//#include <functional>
323//#include <algorithm>
324//#include <memory>
325//#include <ostream>
326//
328//class Logger
329//{
330//public:
331// using Sink = std::function<void(const std::string& channel, const std::string& line)>;
332//
333// static Logger& instance()
334// {
335// static Logger L;
336// return L;
337// }
338//
339// // ---- Core API -----------------------------------------------------------
340//
341// // Append a line to a channel.
342// void log(const std::string& channel, const std::string& line)
343// {
344// std::lock_guard<std::mutex> lk(m_);
345// auto& q = channels_[channel];
346// q.emplace_back(line);
347// while (q.size() > capacity_)
348// q.pop_front();
349// // fan-out to sinks (e.g., file, external)
350// for (auto& [id, s] : sinks_)
351// s(channel, line);
352// }
353//
354// // Get snapshot of a channel lines.
355// std::vector<std::string> getChannel(const std::string& channel)
356// {
357// std::lock_guard<std::mutex> lk(m_);
358// auto it = channels_.find(channel);
359// if (it == channels_.end())
360// return {};
361// return {it->second.begin(), it->second.end()};
362// }
363//
364// // List channel names (unordered).
365// std::vector<std::string> channels() const
366// {
367// std::lock_guard<std::mutex> lk(m_);
368// std::vector<std::string> names;
369// names.reserve(channels_.size());
370// for (auto& kv : channels_)
371// names.push_back(kv.first);
372// return names;
373// }
374//
375// // Clear a channel’s buffer.
376// void clearChannel(const std::string& channel)
377// {
378// std::lock_guard<std::mutex> lk(m_);
379// channels_[channel].clear();
380// }
381//
382// // Set ring buffer capacity for all channels.
383// void setChannelCapacity(size_t cap)
384// {
385// std::lock_guard<std::mutex> lk(m_);
386// capacity_ = cap;
387// }
388//
389// // ---- Optional sinks (fan-out new lines) --------------------------------
390// int addSink(const Sink& s)
391// {
392// std::lock_guard<std::mutex> lk(m_);
393// int id = ++nextSinkId_;
394// sinks_.emplace_back(id, s);
395// return id;
396// }
397// void removeSink(int id)
398// {
399// std::lock_guard<std::mutex> lk(m_);
400// sinks_.erase(std::remove_if(sinks_.begin(), sinks_.end(),
401// [id](auto& p)
402// { return p.first == id; }),
403// sinks_.end());
404// }
405// void clearSinks()
406// {
407// std::lock_guard<std::mutex> lk(m_);
408// sinks_.clear();
409// }
410//
411// // ---- std::cout / std::cerr capture (so you don't have to call log()) ----
412// // Call once at startup: Logger::instance().installStdCapture();
413// void installStdCapture()
414// {
415// std::lock_guard<std::mutex> lk(m_);
416// if (!coutRedirect_)
417// coutRedirect_ = std::make_unique<OstreamRedirect>(std::cout, "main");
418// if (!cerrRedirect_)
419// cerrRedirect_ = std::make_unique<OstreamRedirect>(std::cerr, "main");
420// // seed channels so they show up immediately
421// ensureSeed_();
422// }
423// void uninstallStdCapture()
424// {
425// std::lock_guard<std::mutex> lk(m_);
426// cerrRedirect_.reset();
427// coutRedirect_.reset();
428// }
429//
430//private:
431// Logger() = default;
432//
433// // --- line-buffering streambuf feeding Logger ----------------------------
434// class LineToLoggerBuf : public std::streambuf
435// {
436// public:
437// explicit LineToLoggerBuf(std::string channel) :
438// channel_(std::move(channel)) {}
439//
440// protected:
441// int overflow(int ch) override
442// {
443// if (ch == traits_type::eof())
444// return sync();
445// char c = static_cast<char>(ch);
446// buf_.push_back(c);
447// if (c == '\n')
448// flushLine_();
449// return ch;
450// }
451// std::streamsize xsputn(const char* s, std::streamsize count) override
452// {
453// std::streamsize w = 0;
454// for (; w < count; ++w)
455// {
456// char c = s[w];
457// buf_.push_back(c);
458// if (c == '\n')
459// flushLine_();
460// }
461// return w;
462// }
463// int sync() override
464// {
465// if (!buf_.empty())
466// {
467// if (buf_.back() == '\n')
468// buf_.pop_back();
469// Logger::instance().log(channel_, buf_);
470// buf_.clear();
471// }
472// return 0;
473// }
474//
475// private:
476// void flushLine_()
477// {
478// if (!buf_.empty() && buf_.back() == '\n')
479// buf_.pop_back();
480// Logger::instance().log(channel_, buf_);
481// buf_.clear();
482// }
483// std::string buf_;
484// std::string channel_;
485// };
486//
487// // RAII redirect an ostream to LineToLoggerBuf
488// class OstreamRedirect
489// {
490// public:
491// OstreamRedirect(std::ostream& os, const std::string& channel) :
492// os_(os), original_(os.rdbuf()), buf_(channel)
493// {
494// os_.rdbuf(&buf_);
495// }
496//
497// ~OstreamRedirect()
498// {
499// if (original_)
500// {
501// try
502// {
503// os_.rdbuf(original_);
504// }
505// catch (...)
506// {
507// }
508// }
509// }
510//
511// // non-copyable, non-movable
512// OstreamRedirect(const OstreamRedirect&) = delete;
513// OstreamRedirect& operator=(const OstreamRedirect&) = delete;
514// OstreamRedirect(OstreamRedirect&&) = delete;
515// OstreamRedirect& operator=(OstreamRedirect&&) = delete;
516//
517// private:
518// std::ostream& os_;
519// std::streambuf* original_; // original streambuf to restore
520// LineToLoggerBuf buf_; // our line-buffering streambuf
521// };
522//
523// void ensureSeed_()
524// {
525// if (!seeded_)
526// {
527// channels_["main"]; // ensure exists
528// channels_["localtunnel"]; // ensure exists
529// seeded_ = true;
530// }
531// }
532//
533// // data
534// mutable std::mutex m_;
535// size_t capacity_ = 4000;
536// std::unordered_map<std::string, std::deque<std::string>> channels_;
537// int nextSinkId_ = 0;
538// std::vector<std::pair<int, Sink>> sinks_;
539//
540// // std capture
541// bool seeded_ = false;
542// std::unique_ptr<OstreamRedirect> coutRedirect_;
543// std::unique_ptr<OstreamRedirect> cerrRedirect_;
544//};
std::string channel_
Definition Logger.h:212
std::streamsize xsputn(const char *s, std::streamsize count) override
Definition Logger.h:173
std::optional< Level > forcedLevel_
Definition Logger.h:214
int overflow(int ch) override
Definition Logger.h:163
int sync() override
Definition Logger.h:185
LineToLoggerBuf(std::string channel, std::optional< Level > forced=std::nullopt)
Definition Logger.h:159
OstreamRedirect & operator=(OstreamRedirect &&)=delete
std::streambuf * original_
Definition Logger.h:247
OstreamRedirect & operator=(const OstreamRedirect &)=delete
LineToLoggerBuf buf_
Definition Logger.h:248
OstreamRedirect(const OstreamRedirect &)=delete
OstreamRedirect(std::ostream &os, const std::string &channel, std::optional< Level > forced=std::nullopt)
Definition Logger.h:221
std::ostream & os_
Definition Logger.h:246
OstreamRedirect(OstreamRedirect &&)=delete
std::unique_ptr< OstreamRedirect > cerrRedirect_
Definition Logger.h:313
void log(const std::string &channel, Level lvl, const std::string &line)
Definition Logger.h:54
std::vector< std::pair< int, Sink > > sinks_
Definition Logger.h:310
std::vector< LogEntry > getChannel(const std::string &channel)
Definition Logger.h:64
std::function< void(const std::string &channel, const LogEntry &e)> Sink
Definition Logger.h:37
void ensureSeed_()
Definition Logger.h:252
std::unique_ptr< OstreamRedirect > coutRedirect_
Definition Logger.h:312
void commit_(const std::string &channel, LogEntry e)
Definition Logger.h:292
int addSink(const Sink &s)
Definition Logger.h:96
bool seeded_
Definition Logger.h:311
void uninstallStdCapture()
Definition Logger.h:127
static std::string formatTs(uint64_t ms)
Definition Logger.h:135
void setChannelCapacity(size_t cap)
Definition Logger.h:89
std::unordered_map< std::string, std::deque< LogEntry > > channels_
Definition Logger.h:308
size_t capacity_
Definition Logger.h:307
std::vector< std::string > channels() const
Definition Logger.h:73
void log(const std::string &channel, const std::string &line)
Definition Logger.h:48
void installStdCapture()
Definition Logger.h:118
void clearSinks()
Definition Logger.h:111
Logger()=default
Level
Definition Logger.h:19
std::mutex m_
Definition Logger.h:306
static Level autoDetect_(const std::string &s)
Definition Logger.h:268
static Logger & instance()
Definition Logger.h:39
static uint64_t nowMs_()
Definition Logger.h:262
void removeSink(int id)
Definition Logger.h:103
int nextSinkId_
Definition Logger.h:309
void clearChannel(const std::string &channel)
Definition Logger.h:83
uint64_t tsMs
Definition Logger.h:32
std::string text
Definition Logger.h:30
std::string tsStr
Definition Logger.h:34