Line data Source code
1 : // Webthing-CPP
2 : // SPDX-FileCopyrightText: 2023-present Benno Waldhauer
3 : // SPDX-License-Identifier: MIT
4 :
5 : #pragma once
6 :
7 : #include <chrono>
8 : #include <iomanip>
9 : #include <iostream>
10 : #include <mutex>
11 : #include <optional>
12 : #include <random>
13 : #include <regex>
14 : #include <sstream>
15 : #include <thread>
16 : #include <time.h>
17 :
18 : namespace bw::webthing
19 : {
20 :
21 : namespace details
22 : {
23 : struct global
24 : {
25 : static inline std::optional<std::string> fixed_time;
26 : static inline std::optional<std::string> fixed_uuid;
27 : };
28 :
29 : // Generate a ISO8601-formatted local time timestamp
30 : // and return as std::string
31 2000630 : inline std::string current_ISO8601_time_local(const std::optional<std::string>& fixed_time = std::nullopt)
32 : {
33 2000630 : if(fixed_time)
34 15 : return *fixed_time;
35 :
36 2000615 : auto now = std::chrono::system_clock::now();
37 2000615 : std::time_t now_time = std::chrono::system_clock::to_time_t(now);
38 :
39 : // Get the milliseconds part of the current time
40 2000615 : auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count() % 1000;
41 :
42 : // Get the local time
43 : std::tm local_time;
44 :
45 : // Get the timezone offset
46 : #ifdef _WIN32
47 : localtime_s(&local_time, &now_time);
48 : long timezone_offset = 0;
49 : int err = _get_timezone(&timezone_offset);
50 : if(err != 0)
51 : timezone_offset = 0;
52 : timezone_offset = -timezone_offset;
53 :
54 : if(local_time.tm_isdst)
55 : timezone_offset += 3600;
56 : #else
57 2000615 : localtime_r(&now_time, &local_time);
58 2000615 : long timezone_offset = local_time.tm_gmtoff;
59 : #endif
60 :
61 : // Calculate the timezone offset manually
62 2000615 : int offset_hours = timezone_offset / 3600;
63 2000615 : int offset_minutes = (std::abs(timezone_offset) % 3600) / 60;
64 :
65 : // Format the time in ISO 8601 format, including milliseconds
66 2000615 : std::ostringstream oss;
67 : oss << std::put_time(&local_time, "%Y-%m-%dT%H:%M:%S")
68 2000615 : << "." << std::setw(3) << std::setfill('0') << milliseconds;
69 2000615 : if (timezone_offset >= 0)
70 2000615 : oss << "+";
71 : else
72 0 : oss << "-";
73 2000615 : oss << std::setw(2) << std::setfill('0') << std::abs(offset_hours)
74 2000615 : << ":" << std::setw(2) << std::setfill('0') << offset_minutes;
75 :
76 2000615 : return oss.str();
77 2000615 : }
78 : } // bw::webthing::details
79 :
80 2000068 : inline std::string timestamp()
81 : {
82 2000068 : return details::current_ISO8601_time_local(details::global::fixed_time);
83 : }
84 :
85 : enum log_level
86 : {
87 : error = 5000,
88 : warn = 4000,
89 : info = 3000,
90 : debug = 2000,
91 : trace = 1000
92 : };
93 :
94 : struct logger
95 : {
96 : typedef std::function<void (log_level, const std::string&)> log_impl;
97 : typedef std::function<const char* (log_level)> log_color_mapper;
98 :
99 3 : static void error(const std::string& msg)
100 : {
101 3 : log(log_level::error, msg);
102 3 : }
103 :
104 56 : static void warn(const std::string& msg)
105 : {
106 56 : log(log_level::warn, msg);
107 56 : }
108 :
109 196 : static void info(const std::string& msg)
110 : {
111 196 : log(log_level::info, msg);
112 196 : }
113 :
114 80 : static void debug(const std::string& msg)
115 : {
116 80 : log(log_level::debug, msg);
117 80 : }
118 :
119 238 : static void trace(const std::string& msg)
120 : {
121 238 : log(log_level::trace, msg);
122 238 : }
123 :
124 573 : static void log(log_level level, const std::string& msg)
125 : {
126 573 : if(level < custom_log_level)
127 4 : return;
128 :
129 569 : if(custom_log_impl)
130 7 : return custom_log_impl(level, msg);
131 :
132 562 : default_log_impl(level, msg);
133 : }
134 :
135 5 : static void register_implementation(log_impl log_impl)
136 : {
137 5 : custom_log_impl = log_impl;
138 5 : }
139 :
140 20 : static void set_level(log_level level)
141 : {
142 20 : custom_log_level = level;
143 20 : }
144 :
145 153 : static log_level get_level()
146 : {
147 153 : return custom_log_level;
148 : }
149 :
150 : static void use_color(bool use_color)
151 : {
152 : log_use_color = use_color;
153 : }
154 :
155 : private:
156 562 : static void default_log_impl(log_level level, const std::string& msg)
157 : {
158 562 : auto timestamp = details::current_ISO8601_time_local();
159 562 : auto level_str = level == log_level::error ? "ERROR" :
160 561 : level == log_level::warn ? "WARN " :
161 507 : level == log_level::info ? "INFO " :
162 313 : level == log_level::debug ? "DEBUG" :
163 235 : level == log_level::trace ? "TRACE" :
164 1686 : "L:" + std::to_string(level);
165 :
166 1124 : std::string color = log_use_color ? log_level_to_color(level) : "";
167 1124 : std::string color_clear = log_use_color ? "\033[0m" : "";
168 1124 : std::string dim = log_use_color ? "\x1B[2m" : "";
169 1124 : std::string dim_off = log_use_color ? "\x1B[22m" : "";
170 562 : std::string italic = log_use_color ? "\x1B[1;3m" : "";
171 :
172 562 : timestamp = std::regex_replace(timestamp, std::regex(R"([T])"), " " + dim_off);
173 562 : timestamp = dim + std::regex_replace(timestamp, std::regex{R"([\+])"}, " " + dim + "+");
174 :
175 : // regex for time, ip 4/6, MAC, urls (with ports)
176 562 : auto string_colored = log_use_color ? (color + std::regex_replace(msg, std::regex{R"("[^"]*"|'[^']*')"}, italic + "$&" + color_clear + color)) : msg;
177 :
178 : // TODO: make log level configurable for THING (notify e, p, a), WEBSOCKET (in, open, close, broadcast), MDNS, Https (REQ (with/without body), RES)
179 :
180 562 : std::ostringstream ss;
181 : ss << color << timestamp << " [" << "" << std::this_thread::get_id() << "" << "] " <<
182 562 : dim_off << level_str << dim << " -- " << dim_off << string_colored << color_clear;
183 :
184 562 : std::lock_guard<std::mutex> lg(logger::log_mutex);
185 562 : switch (level)
186 : {
187 55 : case log_level::error:
188 : case log_level::warn:
189 55 : std::cerr << ss.str() << std::endl;
190 55 : break;
191 507 : default:
192 507 : std::cout << ss.str() << std::endl;
193 : }
194 562 : }
195 :
196 0 : static const char* log_level_to_color(log_level level)
197 : {
198 0 : return level == log_level::error ? "\x1B[91m" : // light red
199 0 : level == log_level::warn ? "\x1B[33m" : // yellow
200 0 : level == log_level::info ? "\x1B[32m" : // green
201 0 : level == log_level::debug ? "\x1B[34m" : // blue
202 0 : level == log_level::trace ? "\x1B[90m" : //light gray
203 0 : "";
204 : }
205 :
206 : static inline std::mutex log_mutex;
207 : static inline log_impl custom_log_impl;
208 : static inline log_level custom_log_level = log_level::debug;
209 : static inline bool log_use_color = false;
210 : };
211 :
212 : // set a fixed time for timestamp generation
213 : // this is useful for tests
214 : // timestamp must follow ISO8601 format
215 : // e.g. "2023-02-08T01:23:45"
216 6 : inline void fix_time(std::string timestamp)
217 : {
218 6 : details::global::fixed_time = timestamp;
219 6 : logger::warn("time fixed to " + bw::webthing::timestamp());
220 6 : }
221 :
222 : // unfix the time for timestamp generation
223 6 : inline void unfix_time()
224 : {
225 6 : details::global::fixed_time = std::nullopt;
226 6 : logger::warn("time unfixed");
227 6 : }
228 :
229 : // fix the time for the current scope. Not thread safe!!!
230 : struct fix_time_scoped
231 : {
232 6 : fix_time_scoped(std::string timestamp)
233 : {
234 6 : fix_time(timestamp);
235 6 : }
236 :
237 6 : ~fix_time_scoped()
238 : {
239 6 : unfix_time();
240 6 : }
241 : };
242 :
243 : #define FIXED_TIME_SCOPED(timestamp) auto fixed_time_scope_guard = fix_time_scoped(timestamp);
244 :
245 100041 : inline std::string generate_uuid()
246 : {
247 100041 : if(details::global::fixed_uuid)
248 13 : return *details::global::fixed_uuid;
249 :
250 : // https://stackoverflow.com/a/58467162
251 : // TODO: include real uuid generator lib?
252 :
253 100028 : static std::random_device dev;
254 100028 : static std::mt19937 rng(dev());
255 :
256 100028 : std::uniform_int_distribution<int> dist(0, 15);
257 :
258 100028 : const char *v = "0123456789abcdef";
259 100028 : const bool dash[] = { 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0 };
260 :
261 100028 : std::string uuid;
262 1700476 : for (int i = 0; i < 16; i++) {
263 1600448 : if (dash[i]) uuid += "-";
264 1600448 : uuid += v[dist(rng)];
265 1600448 : uuid += v[dist(rng)];
266 : }
267 :
268 100028 : return uuid;
269 100028 : }
270 :
271 : // set a fixed uuid for uuid generation
272 : // this is useful for tests
273 5 : inline void fix_uuid(std::string uuid)
274 : {
275 5 : details::global::fixed_uuid = uuid;
276 5 : logger::warn("uuid generation fixed to " + bw::webthing::generate_uuid());
277 5 : }
278 :
279 : // unfix the time for timestamp generation
280 5 : inline void unfix_uuid()
281 : {
282 5 : details::global::fixed_uuid = std::nullopt;
283 5 : logger::warn("uuid generation unfixed");
284 5 : }
285 :
286 : // fix the uuid generation for the current scope. Not thread safe!!!
287 : struct fix_uuid_scoped
288 : {
289 5 : fix_uuid_scoped(std::string uuid)
290 : {
291 5 : fix_uuid(uuid);
292 5 : }
293 :
294 5 : ~fix_uuid_scoped()
295 : {
296 5 : unfix_uuid();
297 5 : }
298 : };
299 :
300 : #define FIXED_UUID_SCOPED(uuid) auto fixed_uuid_scope_guard = fix_uuid_scoped(uuid);
301 :
302 : // try to static_cast from a type to another, throws std::bad_cast on error
303 : template<class To, class From>
304 1 : inline To try_static_cast(const From& value)
305 : {
306 : try
307 : {
308 : if constexpr (std::is_convertible_v<From, To>)
309 1 : return static_cast<To>(value);
310 : }
311 0 : catch(...)
312 : {}
313 :
314 0 : throw std::bad_cast();
315 : }
316 :
317 : } // bw::webthing
|