working on ansi
parent
1e30544cff
commit
fcbc6b947d
|
@ -1,6 +1,6 @@
|
|||
cmake_minimum_required(VERSION 3.20)
|
||||
include(cmake/color.cmake)
|
||||
set(BLT_VERSION 5.1.9)
|
||||
set(BLT_VERSION 5.1.10)
|
||||
|
||||
set(BLT_TARGET BLT)
|
||||
|
||||
|
@ -78,7 +78,7 @@ if (${BUILD_LOGGING})
|
|||
message(STATUS "Building ${Yellow}logging${ColourReset} cxx files")
|
||||
file(GLOB_RECURSE LOGGING_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/blt/logging/*.cpp")
|
||||
else ()
|
||||
set(PARSE_FILES "")
|
||||
set(LOGGING_FILES "")
|
||||
endif ()
|
||||
|
||||
if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/libraries/parallel-hashmap)
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
#pragma once
|
||||
/*
|
||||
* Copyright (C) 2024 Brett Terpstra
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BLT_LOGGING_COLORS_H
|
||||
#define BLT_LOGGING_COLORS_H
|
||||
|
||||
#include <string>
|
||||
#include <blt/std/types.h>
|
||||
|
||||
#define BLT_ANSI_ESCAPE "\x1B"
|
||||
#define BLT_ANSI_CSI BLT_ANSI_ESCAPE "["
|
||||
#define BLT_ANSI_DSC BLT_ANSI_ESCAPE "P"
|
||||
#define BLT_ANSI_OSC BLT_ANSI_ESCAPE "]"
|
||||
|
||||
// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
||||
namespace blt::logging::ansi
|
||||
{
|
||||
namespace color
|
||||
{
|
||||
enum class color_mode : i32
|
||||
{
|
||||
RESET_ALL = 0,
|
||||
BOLD = 1,
|
||||
DIM = 2,
|
||||
ITALIC = 3,
|
||||
UNDERLINE = 4,
|
||||
BLINK = 5,
|
||||
REVERSE = 7,
|
||||
HIDDEN = 8,
|
||||
STRIKE_THROUGH = 9,
|
||||
};
|
||||
|
||||
enum class color8 : i32
|
||||
{
|
||||
BLACK = 0,
|
||||
RED = 1,
|
||||
GREEN = 2,
|
||||
YELLOW = 3,
|
||||
BLUE = 4,
|
||||
MAGENTA = 5,
|
||||
CYAN = 6,
|
||||
WHITE = 7,
|
||||
DEFAULT = 9
|
||||
};
|
||||
|
||||
enum class color8_bright : i32
|
||||
{
|
||||
BLACK = 0,
|
||||
RED = 1,
|
||||
GREEN = 2,
|
||||
YELLOW = 3,
|
||||
BLUE = 4,
|
||||
MAGENTA = 5,
|
||||
CYAN = 6,
|
||||
WHITE = 7,
|
||||
};
|
||||
|
||||
namespace detail
|
||||
{
|
||||
template <typename T>
|
||||
struct color_converter
|
||||
{
|
||||
};
|
||||
|
||||
template <>
|
||||
struct color_converter<color8>
|
||||
{
|
||||
static std::string to_string(color8 color, const bool background)
|
||||
{
|
||||
return (background ? "4" : "3") + std::to_string(static_cast<i32>(color));
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct color_converter<color8_bright>
|
||||
{
|
||||
static std::string to_string(color8_bright color, const bool background)
|
||||
{
|
||||
return (background ? "10" : "9") + std::to_string(static_cast<i32>(color));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace general
|
||||
{
|
||||
inline const std::string bell = "\x07";
|
||||
inline const std::string bs = "\x08";
|
||||
inline const std::string horizontal_tab = "\x09";
|
||||
inline const std::string linefeed = "\x0A";
|
||||
inline const std::string vertical_tab = "\x0B";
|
||||
inline const std::string form_feed = "\x0C";
|
||||
inline const std::string carriage_return = "\x0D";
|
||||
inline const std::string escape = BLT_ANSI_ESCAPE;
|
||||
inline const std::string del = "\0x7F";
|
||||
inline const std::string csi = BLT_ANSI_CSI;
|
||||
inline const std::string dsc = BLT_ANSI_DSC;
|
||||
inline const std::string osc = BLT_ANSI_OSC;
|
||||
}
|
||||
|
||||
namespace cursor
|
||||
{
|
||||
inline const std::string home = BLT_ANSI_CSI "H";
|
||||
|
||||
template <bool UseH>
|
||||
inline std::string move_to(i64 line, i64 column)
|
||||
{
|
||||
if constexpr (UseH)
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(line) + ";" + std::to_string(column) + "H";
|
||||
else
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(line) + ";" + std::to_string(column) + "f";
|
||||
}
|
||||
|
||||
inline std::string move_up(i64 lines)
|
||||
{
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(lines) + "A";
|
||||
}
|
||||
|
||||
inline std::string move_down(i64 lines)
|
||||
{
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(lines) + "B";
|
||||
}
|
||||
|
||||
inline std::string move_right(i64 columns)
|
||||
{
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(columns) + "C";
|
||||
}
|
||||
|
||||
inline std::string move_left(i64 columns)
|
||||
{
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(columns) + "D";
|
||||
}
|
||||
|
||||
inline std::string move_begin_down(i64 lines)
|
||||
{
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(lines) + "E";
|
||||
}
|
||||
|
||||
inline std::string move_begin_up(i64 lines)
|
||||
{
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(lines) + "F";
|
||||
}
|
||||
|
||||
inline std::string move_to(i64 column)
|
||||
{
|
||||
return std::string(BLT_ANSI_CSI) + std::to_string(column) + "G";
|
||||
}
|
||||
|
||||
inline const std::string request_cursor_position = BLT_ANSI_CSI "6n";
|
||||
inline const std::string move_up_one_line = BLT_ANSI_ESCAPE " M";
|
||||
inline const std::string save_cursor_position_dec = BLT_ANSI_ESCAPE " 7";
|
||||
inline const std::string resotre_cursor_position_dec = BLT_ANSI_ESCAPE " 8";
|
||||
inline const std::string save_cursor_position_sco = BLT_ANSI_CSI "s";
|
||||
inline const std::string resotre_cursor_position_sco = BLT_ANSI_CSI "u";
|
||||
}
|
||||
|
||||
namespace erase
|
||||
{
|
||||
inline const std::string to_end_of_screen = BLT_ANSI_CSI "0J";
|
||||
inline const std::string from_begin_of_screen = BLT_ANSI_CSI "1J";
|
||||
inline const std::string entire_screen = BLT_ANSI_CSI "2J";
|
||||
inline const std::string saved_lines = BLT_ANSI_CSI "3J";
|
||||
inline const std::string to_end_of_line = BLT_ANSI_CSI "0K";
|
||||
inline const std::string from_begin_of_line = BLT_ANSI_CSI "1K";
|
||||
inline const std::string entire_line = BLT_ANSI_CSI "2K";
|
||||
}
|
||||
}
|
||||
|
||||
#endif //BLT_LOGGING_COLORS_H
|
|
@ -23,6 +23,7 @@
|
|||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <blt/std/types.h>
|
||||
#include <functional>
|
||||
|
||||
namespace blt::logging
|
||||
{
|
||||
|
@ -38,6 +39,8 @@ namespace blt::logging
|
|||
POUND,
|
||||
LEFT_CHEVRON,
|
||||
RIGHT_CHEVRON,
|
||||
OPEN_BRACKET,
|
||||
CLOSE_BRACKET,
|
||||
CARET
|
||||
};
|
||||
|
||||
|
@ -102,14 +105,16 @@ namespace blt::logging
|
|||
|
||||
private:
|
||||
size_t m_pos = 0;
|
||||
std::string_view m_fmt;
|
||||
std::string_view m_fmt{};
|
||||
};
|
||||
|
||||
|
||||
class fmt_parser_t
|
||||
{
|
||||
public:
|
||||
explicit fmt_parser_t() = default;
|
||||
explicit fmt_parser_t(std::vector<std::function<void(std::ostream&, const fmt_spec_t&)>>& handlers): m_handlers(handlers)
|
||||
{
|
||||
}
|
||||
|
||||
fmt_token_t& peek(const size_t offset)
|
||||
{
|
||||
|
@ -153,14 +158,17 @@ namespace blt::logging
|
|||
|
||||
void parse_fmt_field();
|
||||
void parse_arg_id();
|
||||
std::string parse_arg_or_number();
|
||||
|
||||
void parse_fmt_spec();
|
||||
void parse_fmt_spec_fill();
|
||||
void parse_fmt_spec_align();
|
||||
void parse_fmt_spec_sign();
|
||||
void parse_fmt_spec_form();
|
||||
void parse_fmt_spec_width();
|
||||
void parse_fmt_spec_precision();
|
||||
|
||||
void parse_fill();
|
||||
void parse_align();
|
||||
void parse_sign();
|
||||
void parse_form();
|
||||
|
@ -172,6 +180,8 @@ namespace blt::logging
|
|||
std::vector<fmt_token_t> m_tokens;
|
||||
fmt_tokenizer_t m_tokenizer;
|
||||
fmt_spec_t m_spec;
|
||||
|
||||
std::vector<std::function<void(std::ostream&, const fmt_spec_t&)>>& m_handlers;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -43,11 +43,11 @@ namespace blt::logging
|
|||
template <typename... Args>
|
||||
std::string log(std::string fmt, Args&&... args)
|
||||
{
|
||||
compile(std::move(fmt));
|
||||
auto sequence = std::make_integer_sequence<size_t, sizeof...(Args)>{};
|
||||
m_arg_print_funcs.clear();
|
||||
m_arg_print_funcs.resize(sizeof...(Args));
|
||||
create_conv_funcs(sequence, std::forward<Args>(args)...);
|
||||
compile(std::move(fmt));
|
||||
process_strings();
|
||||
return to_string();
|
||||
}
|
||||
|
@ -137,6 +137,7 @@ namespace blt::logging
|
|||
};
|
||||
}
|
||||
|
||||
[[nodiscard]] size_t find_ending_brace(size_t begin) const;
|
||||
void setup_stream(const fmt_spec_t& spec) const;
|
||||
void process_strings();
|
||||
static void handle_type(std::ostream& stream, const fmt_spec_t& spec);
|
||||
|
@ -150,7 +151,7 @@ namespace blt::logging
|
|||
|
||||
std::string m_fmt;
|
||||
std::ostream& m_stream;
|
||||
fmt_parser_t m_parser;
|
||||
fmt_parser_t m_parser{m_arg_print_funcs};
|
||||
// normal sections of string
|
||||
std::vector<std::string_view> m_string_sections;
|
||||
// processed format specs
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
/*
|
||||
* Copyright (C) 2024 Brett Terpstra
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BLT_LOGGING_LOGGING_CONFIG_H
|
||||
#define BLT_LOGGING_LOGGING_CONFIG_H
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <blt/std/types.h>
|
||||
#include <blt/logging/ansi.h>
|
||||
#include <array>
|
||||
|
||||
namespace blt::logging
|
||||
{
|
||||
enum class log_level_t
|
||||
{
|
||||
TRACE,
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR,
|
||||
FATAL
|
||||
};
|
||||
|
||||
inline constexpr size_t LOG_LEVEL_COUNT = 6;
|
||||
|
||||
class logging_config_t
|
||||
{
|
||||
public:
|
||||
std::optional<std::string> log_file_path;
|
||||
std::string log_format = "";
|
||||
std::array<std::string, LOG_LEVEL_COUNT> log_level_colors = {
|
||||
|
||||
};
|
||||
log_level_t level = log_level_t::TRACE;
|
||||
bool use_color = true;
|
||||
bool log_to_console = true;
|
||||
|
||||
private:
|
||||
};
|
||||
}
|
||||
|
||||
#endif //BLT_LOGGING_LOGGING_CONFIG_H
|
|
@ -344,494 +344,6 @@ namespace blt::logging
|
|||
void setMaxFileSize(size_t fileSize);
|
||||
}
|
||||
|
||||
//#define BLT_LOGGING_IMPLEMENTATION
|
||||
#ifdef BLT_LOGGING_IMPLEMENTATION
|
||||
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <unordered_map>
|
||||
#include <thread>
|
||||
#include <cstdarg>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#if defined(CXX17_FILESYSTEM) || defined (CXX17_FILESYSTEM_LIBFS)
|
||||
#include <filesystem>
|
||||
#elif defined(CXX11_EXP_FILESYSTEM) || defined (CXX11_EXP_FILESYSTEM_LIBFS)
|
||||
#include <experimental/filesystem>
|
||||
#else
|
||||
#include <filesystem>
|
||||
#endif
|
||||
#include <ios>
|
||||
#include <fstream>
|
||||
|
||||
template<typename K, typename V>
|
||||
using hashmap = std::unordered_map<K, V>;
|
||||
|
||||
namespace blt::logging {
|
||||
|
||||
/**
|
||||
* Used to store fast associations between built in tags and their respective values
|
||||
*/
|
||||
class tag_map {
|
||||
private:
|
||||
tag* tags;
|
||||
size_t size;
|
||||
|
||||
[[nodiscard]] static inline size_t hash(const tag& t) {
|
||||
size_t h = t.tag[1] * 3 - t.tag[0];
|
||||
return h - 100;
|
||||
}
|
||||
|
||||
// TODO: fix
|
||||
void expand() {
|
||||
auto newSize = size * 2;
|
||||
auto newTags = new tag[newSize];
|
||||
for (size_t i = 0; i < size; i++)
|
||||
newTags[i] = tags[i];
|
||||
delete[] tags;
|
||||
tags = newTags;
|
||||
size = newSize;
|
||||
}
|
||||
public:
|
||||
tag_map(std::initializer_list<tag> initial_tags){
|
||||
size_t max = 0;
|
||||
for (const auto& t : initial_tags)
|
||||
max = std::max(max, hash(t));
|
||||
tags = new tag[(size = max+1)];
|
||||
for (const auto& t : initial_tags)
|
||||
insert(t);
|
||||
}
|
||||
tag_map(const tag_map& copy) {
|
||||
tags = new tag[(size = copy.size)];
|
||||
for (size_t i = 0; i < size; i++)
|
||||
tags[i] = copy.tags[i];
|
||||
}
|
||||
|
||||
void insert(const tag& t) {
|
||||
auto h = hash(t);
|
||||
//if (h > size)
|
||||
// expand();
|
||||
if (!tags[h].tag.empty())
|
||||
std::cerr << "Tag not empty! " << tags[h].tag << "!!!\n";
|
||||
tags[h] = t;
|
||||
}
|
||||
|
||||
tag& operator[](const std::string& name) const {
|
||||
auto h = hash(tag{name, nullptr});
|
||||
if (h > size)
|
||||
std::cerr << "Tag out of bounds";
|
||||
return tags[h];
|
||||
}
|
||||
|
||||
~tag_map(){
|
||||
delete[] tags;
|
||||
tags = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class LogFileWriter {
|
||||
private:
|
||||
std::string m_path;
|
||||
std::fstream* output = nullptr;
|
||||
public:
|
||||
explicit LogFileWriter() = default;
|
||||
|
||||
void writeLine(const std::string& path, const std::string& line){
|
||||
if (path != m_path || output == nullptr){
|
||||
clear();
|
||||
delete output;
|
||||
output = new std::fstream(path, std::ios::out | std::ios::app);
|
||||
if (!output->good()){
|
||||
throw std::runtime_error("Unable to open console filestream!\n");
|
||||
}
|
||||
}
|
||||
if (!output->good()){
|
||||
std::cerr << "There has been an error in the logging file stream!\n";
|
||||
output->clear();
|
||||
}
|
||||
*output << line;
|
||||
}
|
||||
|
||||
void clear(){
|
||||
if (output != nullptr) {
|
||||
try {
|
||||
output->flush();
|
||||
output->close();
|
||||
} catch (std::exception& e){
|
||||
std::cerr << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~LogFileWriter() {
|
||||
clear();
|
||||
delete(output);
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef WIN32
|
||||
#define BLT_NOW() auto t = std::time(nullptr); tm now{}; localtime_s(&now, &t)
|
||||
#else
|
||||
#define BLT_NOW() auto t = std::time(nullptr); auto now_ptr = std::localtime(&t); auto& now = *now_ptr
|
||||
#endif
|
||||
|
||||
//#define BLT_NOW() auto t = std::time(nullptr); tm now; localtime_s(&now, &t); //auto now = std::localtime(&t)
|
||||
#define BLT_ISO_YEAR(S) auto S = std::to_string(now.tm_year + 1900); \
|
||||
S += '-'; \
|
||||
S += ensureHasDigits(now.tm_mon+1, 2); \
|
||||
S += '-'; \
|
||||
S += ensureHasDigits(now.tm_mday, 2);
|
||||
#define BLT_CUR_TIME(S) auto S = ensureHasDigits(now.tm_hour, 2); \
|
||||
S += ':'; \
|
||||
S += ensureHasDigits(now.tm_min, 2); \
|
||||
S += ':'; \
|
||||
S += ensureHasDigits(now.tm_sec, 2);
|
||||
|
||||
static inline std::string ensureHasDigits(int current, int digits) {
|
||||
std::string asString = std::to_string(current);
|
||||
auto length = digits - asString.length();
|
||||
if (length <= 0)
|
||||
return asString;
|
||||
std::string zeros;
|
||||
zeros.reserve(length);
|
||||
for (unsigned int i = 0; i < length; i++){
|
||||
zeros += '0';
|
||||
}
|
||||
return zeros + asString;
|
||||
}
|
||||
|
||||
log_format loggingFormat {};
|
||||
hashmap<std::thread::id, std::string> loggingThreadNames;
|
||||
hashmap<std::thread::id, hashmap<blt::logging::log_level, std::string>> loggingStreamLines;
|
||||
LogFileWriter writer;
|
||||
|
||||
const std::unique_ptr<tag_map> tagMap = std::make_unique<tag_map>(tag_map{
|
||||
{"YEAR", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
return std::to_string(now.tm_year);
|
||||
}},
|
||||
{"MONTH", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_mon+1, 2);
|
||||
}},
|
||||
{"DAY", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_mday, 2);
|
||||
}},
|
||||
{"HOUR", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_hour, 2);
|
||||
}},
|
||||
{"MINUTE", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_min, 2);
|
||||
}},
|
||||
{"SECOND", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_sec, 2);
|
||||
}},
|
||||
{"MS", [](const tag_func_param&) -> std::string {
|
||||
return std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::high_resolution_clock::now().time_since_epoch()).count()
|
||||
);
|
||||
}},
|
||||
{"NS", [](const tag_func_param&) -> std::string {
|
||||
return std::to_string(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::high_resolution_clock::now().time_since_epoch()).count()
|
||||
);
|
||||
}},
|
||||
{"ISO_YEAR", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
BLT_ISO_YEAR(returnStr);
|
||||
return returnStr;
|
||||
}},
|
||||
{"TIME", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
BLT_CUR_TIME(returnStr);
|
||||
return returnStr;
|
||||
}},
|
||||
{"FULL_TIME", [](const tag_func_param&) -> std::string {
|
||||
BLT_NOW();
|
||||
BLT_ISO_YEAR(ISO);
|
||||
BLT_CUR_TIME(TIME);
|
||||
ISO += ' ';
|
||||
ISO += TIME;
|
||||
return ISO;
|
||||
}},
|
||||
{"LF", [](const tag_func_param& f) -> std::string {
|
||||
return loggingFormat.levelColors[(int)f.level];
|
||||
}},
|
||||
{"ER", [](const tag_func_param&) -> std::string {
|
||||
return loggingFormat.levelColors[(int)log_level::ERROR];
|
||||
}},
|
||||
{"CNR", [](const tag_func_param& f) -> std::string {
|
||||
return f.level >= log_level::ERROR ? loggingFormat.levelColors[(int)log_level::ERROR] : "";
|
||||
}},
|
||||
{"RC", [](const tag_func_param&) -> std::string {
|
||||
return "\033[0m";
|
||||
}},
|
||||
{"LOG_LEVEL", [](const tag_func_param& f) -> std::string {
|
||||
return loggingFormat.levelNames[(int)f.level];
|
||||
}},
|
||||
{"THREAD_NAME", [](const tag_func_param&) -> std::string {
|
||||
if (loggingThreadNames.find(std::this_thread::get_id()) == loggingThreadNames.end())
|
||||
return "UNKNOWN";
|
||||
return loggingThreadNames[std::this_thread::get_id()];
|
||||
}},
|
||||
{"FILE", [](const tag_func_param& f) -> std::string {
|
||||
return f.file;
|
||||
}},
|
||||
{"LINE", [](const tag_func_param& f) -> std::string {
|
||||
return f.line;
|
||||
}},
|
||||
{"RAW_STR", [](const tag_func_param& f) -> std::string {
|
||||
return f.raw_string;
|
||||
}},
|
||||
{"STR", [](const tag_func_param& f) -> std::string {
|
||||
return f.formatted_string;
|
||||
}}
|
||||
});
|
||||
|
||||
static inline std::vector<std::string> split(std::string s, const std::string& delim) {
|
||||
size_t pos = 0;
|
||||
std::vector<std::string> tokens;
|
||||
while ((pos = s.find(delim)) != std::string::npos) {
|
||||
auto token = s.substr(0, pos);
|
||||
tokens.push_back(token);
|
||||
s.erase(0, pos + delim.length());
|
||||
}
|
||||
tokens.push_back(s);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
inline std::string filename(const std::string& path){
|
||||
if (loggingFormat.printFullFileName)
|
||||
return path;
|
||||
auto paths = split(path, "/");
|
||||
auto final = paths[paths.size()-1];
|
||||
if (final == "/")
|
||||
return paths[paths.size()-2];
|
||||
return final;
|
||||
}
|
||||
|
||||
class string_parser {
|
||||
private:
|
||||
std::string _str;
|
||||
size_t _pos;
|
||||
public:
|
||||
explicit string_parser(std::string str): _str(std::move(str)), _pos(0) {}
|
||||
|
||||
inline char next(){
|
||||
return _str[_pos++];
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool has_next() const {
|
||||
return _pos < _str.size();
|
||||
}
|
||||
};
|
||||
|
||||
std::string stripANSI(const std::string& str){
|
||||
string_parser parser(str);
|
||||
std::string out;
|
||||
while (parser.has_next()){
|
||||
char c = parser.next();
|
||||
if (c == '\033'){
|
||||
while (parser.has_next() && parser.next() != 'm');
|
||||
} else
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void applyCFormatting(const std::string& format, std::string& output, std::va_list& args){
|
||||
// args must be copied because they will be consumed by the first vsnprintf
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
|
||||
auto buffer_size = std::vsnprintf(nullptr, 0, format.c_str(), args_copy) + 1;
|
||||
auto* buffer = new char[static_cast<unsigned long>(buffer_size)];
|
||||
|
||||
vsnprintf(buffer, buffer_size, format.c_str(), args);
|
||||
output = std::string(buffer);
|
||||
|
||||
delete[] buffer;
|
||||
|
||||
va_end(args_copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the next character in the parser is a tag opening, if not output the buffer to the out string
|
||||
*/
|
||||
inline bool tagOpening(string_parser& parser, std::string& out){
|
||||
char c = ' ';
|
||||
if (parser.has_next() && (c = parser.next()) == '{')
|
||||
if (parser.has_next() && (c = parser.next()) == '{')
|
||||
return true;
|
||||
else
|
||||
out += c;
|
||||
else
|
||||
out += c;
|
||||
return false;
|
||||
}
|
||||
|
||||
void parseString(string_parser& parser, std::string& out, const std::string& userStr, log_level level, const char* file, int line){
|
||||
while (parser.has_next()){
|
||||
char c = parser.next();
|
||||
std::string nonTag;
|
||||
if (c == '$' && tagOpening(parser, nonTag)){
|
||||
std::string tag;
|
||||
while (parser.has_next()){
|
||||
c = parser.next();
|
||||
if (c == '}')
|
||||
break;
|
||||
tag += c;
|
||||
}
|
||||
c = parser.next();
|
||||
if (parser.has_next() && c != '}') {
|
||||
std::cerr << "Error processing tag, is not closed with two '}'!\n";
|
||||
break;
|
||||
}
|
||||
if (loggingFormat.ensureAlignment && tag == "STR") {
|
||||
auto currentOutputWidth = out.size();
|
||||
auto& longestWidth = loggingFormat.currentWidth;
|
||||
longestWidth = std::max(longestWidth, currentOutputWidth);
|
||||
// pad with spaces
|
||||
if (currentOutputWidth != longestWidth){
|
||||
for (size_t i = currentOutputWidth; i < longestWidth; i++)
|
||||
out += ' ';
|
||||
}
|
||||
}
|
||||
tag_func_param param{
|
||||
level, filename({file}), std::to_string(line), userStr, userStr
|
||||
};
|
||||
out += (*tagMap)[tag].func(param);
|
||||
} else {
|
||||
out += c;
|
||||
out += nonTag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string applyFormatString(const std::string& str, log_level level, const char* file, int line){
|
||||
// this can be speedup by preprocessing the string into an easily callable class
|
||||
// where all the variables are ready to be substituted in one step
|
||||
// and all static information already entered
|
||||
string_parser parser(loggingFormat.logOutputFormat);
|
||||
std::string out;
|
||||
parseString(parser, out, str, level, file, line);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void log_internal(const std::string& format, log_level level, const char* file, int line, std::va_list& args) {
|
||||
std::string withoutLn = format;
|
||||
auto len = withoutLn.length();
|
||||
|
||||
if (len > 0 && withoutLn[len - 1] == '\n')
|
||||
withoutLn = withoutLn.substr(0, len-1);
|
||||
|
||||
std::string out;
|
||||
|
||||
applyCFormatting(withoutLn, out, args);
|
||||
|
||||
if (level == log_level::NONE){
|
||||
std::cout << out << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string finalFormattedOutput = applyFormatString(out, level, file, line);
|
||||
|
||||
if (loggingFormat.logToConsole)
|
||||
std::cout << finalFormattedOutput;
|
||||
|
||||
|
||||
if (loggingFormat.logToFile){
|
||||
string_parser parser(loggingFormat.logFileName);
|
||||
std::string fileName;
|
||||
parseString(parser, fileName, withoutLn, level, file, line);
|
||||
|
||||
auto path = loggingFormat.logFilePath;
|
||||
if (!path.empty() && path[path.length()-1] != '/')
|
||||
path += '/';
|
||||
|
||||
// if the file has changed (new day in default case) we should reset the rollover count
|
||||
if (loggingFormat.lastFile != fileName){
|
||||
loggingFormat.currentRollover = 0;
|
||||
loggingFormat.lastFile = fileName;
|
||||
}
|
||||
|
||||
path += fileName;
|
||||
path += '-';
|
||||
path += std::to_string(loggingFormat.currentRollover);
|
||||
path += ".log";
|
||||
|
||||
if (std::filesystem::exists(path)) {
|
||||
auto fileSize = std::filesystem::file_size(path);
|
||||
|
||||
// will start on next file
|
||||
if (fileSize > loggingFormat.logMaxFileSize)
|
||||
loggingFormat.currentRollover++;
|
||||
}
|
||||
|
||||
writer.writeLine(path, stripANSI(finalFormattedOutput));
|
||||
}
|
||||
//std::cout.flush();
|
||||
}
|
||||
|
||||
void log_stream_internal(const std::string& str, const logger& logger) {
|
||||
auto& s = loggingStreamLines[std::this_thread::get_id()][logger.level];
|
||||
// s += str;
|
||||
for (char c : str){
|
||||
s += c;
|
||||
if (c == '\n'){
|
||||
log(s, logger.level, logger.file, logger.line);
|
||||
s = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setThreadName(const std::string& name) {
|
||||
loggingThreadNames[std::this_thread::get_id()] = name;
|
||||
}
|
||||
|
||||
void setLogFormat(const log_format& format){
|
||||
loggingFormat = format;
|
||||
}
|
||||
void setLogColor(log_level level, const std::string& newFormat){
|
||||
loggingFormat.levelColors[(int)level] = newFormat;
|
||||
}
|
||||
void setLogName(log_level level, const std::string& newFormat){
|
||||
loggingFormat.levelNames[(int)level] = newFormat;
|
||||
}
|
||||
void setLogOutputFormat(const std::string& newFormat){
|
||||
loggingFormat.logOutputFormat = newFormat;
|
||||
}
|
||||
void setLogToFile(bool shouldLogToFile){
|
||||
loggingFormat.logToFile = shouldLogToFile;
|
||||
}
|
||||
void setLogToConsole(bool shouldLogToConsole){
|
||||
loggingFormat.logToConsole = shouldLogToConsole;
|
||||
}
|
||||
void setLogPath(const std::string& path){
|
||||
loggingFormat.logFilePath = path;
|
||||
}
|
||||
void setLogFileName(const std::string& fileName){
|
||||
loggingFormat.logFileName = fileName;
|
||||
}
|
||||
void setMaxFileSize(size_t fileSize) {
|
||||
loggingFormat.logMaxFileSize = fileSize;
|
||||
}
|
||||
|
||||
void flush() {
|
||||
std::cerr.flush();
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(__clang__) || defined(__llvm__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments"
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* <Short Description>
|
||||
* Copyright (C) 2025 Brett Terpstra
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <blt/logging/ansi.h>
|
|
@ -55,6 +55,10 @@ namespace blt::logging
|
|||
return fmt_token_type::RIGHT_CHEVRON;
|
||||
case '^':
|
||||
return fmt_token_type::CARET;
|
||||
case '{':
|
||||
return fmt_token_type::OPEN_BRACKET;
|
||||
case '}':
|
||||
return fmt_token_type::CLOSE_BRACKET;
|
||||
default:
|
||||
return fmt_token_type::STRING;
|
||||
}
|
||||
|
@ -64,6 +68,12 @@ namespace blt::logging
|
|||
{
|
||||
if (m_pos >= m_fmt.size())
|
||||
return {};
|
||||
bool is_escaped = false;
|
||||
if (m_fmt[m_pos] == '\\')
|
||||
{
|
||||
is_escaped = true;
|
||||
++m_pos;
|
||||
}
|
||||
switch (const auto base_type = get_type(m_fmt[m_pos]))
|
||||
{
|
||||
case fmt_token_type::SPACE:
|
||||
|
@ -75,6 +85,10 @@ namespace blt::logging
|
|||
case fmt_token_type::LEFT_CHEVRON:
|
||||
case fmt_token_type::RIGHT_CHEVRON:
|
||||
case fmt_token_type::CARET:
|
||||
case fmt_token_type::OPEN_BRACKET:
|
||||
case fmt_token_type::CLOSE_BRACKET:
|
||||
if (is_escaped)
|
||||
return fmt_token_t{fmt_token_type::STRING, std::string_view{m_fmt.data() + m_pos++, 1}};
|
||||
return fmt_token_t{base_type, std::string_view{m_fmt.data() + m_pos++, 1}};
|
||||
default:
|
||||
{
|
||||
|
@ -164,6 +178,29 @@ namespace blt::logging
|
|||
m_spec.arg_id = std::stoll(std::string(value));
|
||||
}
|
||||
|
||||
std::string fmt_parser_t::parse_arg_or_number()
|
||||
{
|
||||
auto [type, value] = next();
|
||||
if (type == fmt_token_type::NUMBER)
|
||||
return std::string(value);
|
||||
if (type == fmt_token_type::OPEN_BRACKET)
|
||||
{
|
||||
auto [next_type, next_value] = next();
|
||||
if (next_type != fmt_token_type::NUMBER)
|
||||
throw std::runtime_error("Expected number when parsing arg or number, unexpected value '" + std::string(next_value) + '\'');
|
||||
if (next().type != fmt_token_type::CLOSE_BRACKET)
|
||||
throw std::runtime_error("Expected closing bracket when parsing arg or number, unexpected value '" + std::string(next_value) + '\'');
|
||||
// TODO: this feels like an evil hack.
|
||||
const auto arg_id = std::stoul(std::string(next_value));
|
||||
if (arg_id >= m_handlers.size())
|
||||
throw std::runtime_error("Invalid arg id " + std::to_string(arg_id) + ", max arg supported: " + std::to_string(m_handlers.size()));
|
||||
std::stringstream ss;
|
||||
m_handlers[arg_id](ss, fmt_spec_t{});
|
||||
return ss.str();
|
||||
}
|
||||
throw std::runtime_error("Expected number when parsing arg or number, unexpected value '" + std::string(value) + '\'');
|
||||
}
|
||||
|
||||
void fmt_parser_t::parse_fmt_spec()
|
||||
{
|
||||
// consume :
|
||||
|
@ -172,12 +209,9 @@ namespace blt::logging
|
|||
switch (type)
|
||||
{
|
||||
case fmt_token_type::STRING:
|
||||
// if there is a token beyond this string, it is not a type string
|
||||
if (has_next(1))
|
||||
{
|
||||
const auto [next_type, next_value] = peek(1);
|
||||
if (is_align_t(next_type))
|
||||
parse_fmt_spec_align();
|
||||
}
|
||||
parse_fmt_spec_fill();
|
||||
return;
|
||||
case fmt_token_type::RIGHT_CHEVRON:
|
||||
case fmt_token_type::LEFT_CHEVRON:
|
||||
|
@ -185,12 +219,14 @@ namespace blt::logging
|
|||
parse_fmt_spec_align();
|
||||
break;
|
||||
case fmt_token_type::COLON:
|
||||
case fmt_token_type::CLOSE_BRACKET:
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "(Stage (Begin)) Invalid token type " << static_cast<u8>(type) << " value " << value;
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
case fmt_token_type::NUMBER:
|
||||
case fmt_token_type::OPEN_BRACKET:
|
||||
parse_fmt_spec_width();
|
||||
break;
|
||||
case fmt_token_type::DOT:
|
||||
|
@ -207,6 +243,46 @@ namespace blt::logging
|
|||
}
|
||||
}
|
||||
|
||||
void fmt_parser_t::parse_fmt_spec_fill()
|
||||
{
|
||||
parse_fill();
|
||||
if (!has_next())
|
||||
return;
|
||||
auto [type, value] = peek();
|
||||
switch (type)
|
||||
{
|
||||
case fmt_token_type::RIGHT_CHEVRON:
|
||||
case fmt_token_type::LEFT_CHEVRON:
|
||||
case fmt_token_type::CARET:
|
||||
parse_fmt_spec_align();
|
||||
break;
|
||||
case fmt_token_type::NUMBER:
|
||||
case fmt_token_type::OPEN_BRACKET:
|
||||
parse_fmt_spec_width();
|
||||
break;
|
||||
case fmt_token_type::DOT:
|
||||
parse_fmt_spec_precision();
|
||||
break;
|
||||
case fmt_token_type::SPACE:
|
||||
case fmt_token_type::MINUS:
|
||||
case fmt_token_type::PLUS:
|
||||
parse_fmt_spec_sign();
|
||||
break;
|
||||
case fmt_token_type::POUND:
|
||||
parse_fmt_spec_form();
|
||||
break;
|
||||
case fmt_token_type::STRING:
|
||||
return;
|
||||
case fmt_token_type::COLON:
|
||||
case fmt_token_type::CLOSE_BRACKET:
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "(Stage (Fill)) Invalid token type " << static_cast<u8>(type) << " value " << value;
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fmt_parser_t::parse_fmt_spec_align()
|
||||
{
|
||||
parse_align();
|
||||
|
@ -218,6 +294,7 @@ namespace blt::logging
|
|||
case fmt_token_type::STRING:
|
||||
return;
|
||||
case fmt_token_type::NUMBER:
|
||||
case fmt_token_type::OPEN_BRACKET:
|
||||
parse_fmt_spec_width();
|
||||
break;
|
||||
case fmt_token_type::DOT:
|
||||
|
@ -235,9 +312,10 @@ namespace blt::logging
|
|||
case fmt_token_type::COLON:
|
||||
case fmt_token_type::LEFT_CHEVRON:
|
||||
case fmt_token_type::RIGHT_CHEVRON:
|
||||
case fmt_token_type::CLOSE_BRACKET:
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "(Stage (Begin)) Invalid token type " << static_cast<u8>(type) << " value " << value;
|
||||
ss << "(Stage (Align)) Invalid token type " << static_cast<u8>(type) << " value " << value;
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
}
|
||||
|
@ -261,12 +339,14 @@ namespace blt::logging
|
|||
case fmt_token_type::CARET:
|
||||
case fmt_token_type::LEFT_CHEVRON:
|
||||
case fmt_token_type::RIGHT_CHEVRON:
|
||||
case fmt_token_type::CLOSE_BRACKET:
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "(Stage (Sign)) Invalid token type " << static_cast<u8>(type) << " value " << value;
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
case fmt_token_type::NUMBER:
|
||||
case fmt_token_type::OPEN_BRACKET:
|
||||
parse_fmt_spec_width();
|
||||
break;
|
||||
case fmt_token_type::DOT:
|
||||
|
@ -296,12 +376,14 @@ namespace blt::logging
|
|||
case fmt_token_type::CARET:
|
||||
case fmt_token_type::LEFT_CHEVRON:
|
||||
case fmt_token_type::RIGHT_CHEVRON:
|
||||
case fmt_token_type::CLOSE_BRACKET:
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "(Stage (Form)) Invalid token type " << static_cast<u8>(type) << " value " << value;
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
case fmt_token_type::NUMBER:
|
||||
case fmt_token_type::OPEN_BRACKET:
|
||||
parse_fmt_spec_width();
|
||||
break;
|
||||
case fmt_token_type::DOT:
|
||||
|
@ -330,6 +412,8 @@ namespace blt::logging
|
|||
case fmt_token_type::CARET:
|
||||
case fmt_token_type::LEFT_CHEVRON:
|
||||
case fmt_token_type::RIGHT_CHEVRON:
|
||||
case fmt_token_type::OPEN_BRACKET:
|
||||
case fmt_token_type::CLOSE_BRACKET:
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "(Stage (Width)) Invalid token type " << static_cast<u8>(type) << " value " << value;
|
||||
|
@ -348,17 +432,22 @@ namespace blt::logging
|
|||
parse_precision();
|
||||
}
|
||||
|
||||
void fmt_parser_t::parse_fill()
|
||||
{
|
||||
auto [type, value] = next();
|
||||
if (type != fmt_token_type::STRING)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Expected string when parsing fill, got " << static_cast<u8>(type) << " value " << value;
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
m_spec.prefix_char = value.front();
|
||||
}
|
||||
|
||||
void fmt_parser_t::parse_align()
|
||||
{
|
||||
auto [type, value] = next();
|
||||
fmt_token_type process_type = type;
|
||||
if (type == fmt_token_type::STRING)
|
||||
{
|
||||
auto [next_type, next_value] = next();
|
||||
process_type = next_type;
|
||||
m_spec.prefix_char = value.front();
|
||||
}
|
||||
switch (process_type)
|
||||
switch (type)
|
||||
{
|
||||
case fmt_token_type::LEFT_CHEVRON:
|
||||
m_spec.alignment = fmt_align_t::LEFT;
|
||||
|
@ -372,7 +461,7 @@ namespace blt::logging
|
|||
default:
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Invalid align type " << static_cast<u8>(process_type) << " value " << value;
|
||||
ss << "Invalid align type " << static_cast<u8>(type) << " value " << value;
|
||||
throw std::runtime_error(ss.str());
|
||||
}
|
||||
}
|
||||
|
@ -415,19 +504,17 @@ namespace blt::logging
|
|||
|
||||
void fmt_parser_t::parse_width()
|
||||
{
|
||||
auto [_, value] = next();
|
||||
const auto value = parse_arg_or_number();
|
||||
if (value.front() == '0' && !m_spec.prefix_char.has_value())
|
||||
m_spec.prefix_char = '0';
|
||||
m_spec.width = std::stoll(std::string(value));
|
||||
m_spec.width = std::stoll(value);
|
||||
}
|
||||
|
||||
void fmt_parser_t::parse_precision()
|
||||
{
|
||||
if (!has_next())
|
||||
throw std::runtime_error("Missing token when parsing precision");
|
||||
auto [type, value] = next();
|
||||
if (type != fmt_token_type::NUMBER)
|
||||
throw std::runtime_error("Expected number when parsing precision");
|
||||
auto value = parse_arg_or_number();
|
||||
m_spec.precision = std::stoll(std::string(value));
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,21 @@ namespace blt::logging
|
|||
return dynamic_cast<std::stringstream&>(m_stream).str();
|
||||
}
|
||||
|
||||
size_t logger_t::find_ending_brace(size_t begin) const
|
||||
{
|
||||
size_t braces = 0;
|
||||
for (; begin < m_fmt.size(); ++begin)
|
||||
{
|
||||
if (m_fmt[begin] == '{')
|
||||
++braces;
|
||||
else if (m_fmt[begin] == '}')
|
||||
--braces;
|
||||
if (braces == 0)
|
||||
return begin;
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
void logger_t::setup_stream(const fmt_spec_t& spec) const
|
||||
{
|
||||
if (spec.prefix_char)
|
||||
|
@ -167,9 +182,8 @@ namespace blt::logging
|
|||
const auto begin = m_fmt.find('{', m_last_fmt_pos);
|
||||
if (begin == std::string::npos)
|
||||
return {};
|
||||
const auto next_begin = m_fmt.find('{', begin + 1);
|
||||
const auto end = m_fmt.find('}', begin);
|
||||
if (end == std::string::npos || (next_begin != std::string::npos && next_begin < end))
|
||||
const auto end = find_ending_brace(begin);
|
||||
if (end == std::string::npos)
|
||||
{
|
||||
std::stringstream ss;
|
||||
ss << "Invalid format string, missing closing '}' near " << m_fmt.substr(std::min(static_cast<i64>(begin) - 5, 0l));
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* <Short Description>
|
||||
* Copyright (C) 2025 Brett Terpstra
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <blt/logging/logging_config.h>
|
||||
|
||||
namespace blt::logging
|
||||
{
|
||||
|
||||
}
|
|
@ -3,5 +3,625 @@
|
|||
* Licensed under GNU General Public License V3.0
|
||||
* See LICENSE file for license detail
|
||||
*/
|
||||
#define BLT_LOGGING_IMPLEMENTATION
|
||||
#include <blt/std/logging.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <unordered_map>
|
||||
#include <thread>
|
||||
#include <cstdarg>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#if defined(CXX17_FILESYSTEM) || defined (CXX17_FILESYSTEM_LIBFS)
|
||||
#include <filesystem>
|
||||
#elif defined(CXX11_EXP_FILESYSTEM) || defined (CXX11_EXP_FILESYSTEM_LIBFS)
|
||||
#include <experimental/filesystem>
|
||||
#else
|
||||
#include <filesystem>
|
||||
#endif
|
||||
#include <ios>
|
||||
#include <fstream>
|
||||
|
||||
template <typename K, typename V>
|
||||
using hashmap = std::unordered_map<K, V>;
|
||||
|
||||
namespace blt::logging
|
||||
{
|
||||
/**
|
||||
* Used to store fast associations between built in tags and their respective values
|
||||
*/
|
||||
class tag_map
|
||||
{
|
||||
private:
|
||||
tag* tags;
|
||||
size_t size;
|
||||
|
||||
[[nodiscard]] static inline size_t hash(const tag& t)
|
||||
{
|
||||
size_t h = t.tag[1] * 3 - t.tag[0];
|
||||
return h - 100;
|
||||
}
|
||||
|
||||
// TODO: fix
|
||||
void expand()
|
||||
{
|
||||
auto newSize = size * 2;
|
||||
auto newTags = new tag[newSize];
|
||||
for (size_t i = 0; i < size; i++)
|
||||
newTags[i] = tags[i];
|
||||
delete[] tags;
|
||||
tags = newTags;
|
||||
size = newSize;
|
||||
}
|
||||
|
||||
public:
|
||||
tag_map(std::initializer_list<tag> initial_tags)
|
||||
{
|
||||
size_t max = 0;
|
||||
for (const auto& t : initial_tags)
|
||||
max = std::max(max, hash(t));
|
||||
tags = new tag[(size = max + 1)];
|
||||
for (const auto& t : initial_tags)
|
||||
insert(t);
|
||||
}
|
||||
|
||||
tag_map(const tag_map& copy)
|
||||
{
|
||||
tags = new tag[(size = copy.size)];
|
||||
for (size_t i = 0; i < size; i++)
|
||||
tags[i] = copy.tags[i];
|
||||
}
|
||||
|
||||
void insert(const tag& t)
|
||||
{
|
||||
auto h = hash(t);
|
||||
//if (h > size)
|
||||
// expand();
|
||||
if (!tags[h].tag.empty())
|
||||
std::cerr << "Tag not empty! " << tags[h].tag << "!!!\n";
|
||||
tags[h] = t;
|
||||
}
|
||||
|
||||
tag& operator[](const std::string& name) const
|
||||
{
|
||||
auto h = hash(tag{name, nullptr});
|
||||
if (h > size)
|
||||
std::cerr << "Tag out of bounds";
|
||||
return tags[h];
|
||||
}
|
||||
|
||||
~tag_map()
|
||||
{
|
||||
delete[] tags;
|
||||
tags = nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
class LogFileWriter
|
||||
{
|
||||
private:
|
||||
std::string m_path;
|
||||
std::fstream* output = nullptr;
|
||||
|
||||
public:
|
||||
explicit LogFileWriter() = default;
|
||||
|
||||
void writeLine(const std::string& path, const std::string& line)
|
||||
{
|
||||
if (path != m_path || output == nullptr)
|
||||
{
|
||||
clear();
|
||||
delete output;
|
||||
output = new std::fstream(path, std::ios::out | std::ios::app);
|
||||
if (!output->good())
|
||||
{
|
||||
throw std::runtime_error("Unable to open console filestream!\n");
|
||||
}
|
||||
}
|
||||
if (!output->good())
|
||||
{
|
||||
std::cerr << "There has been an error in the logging file stream!\n";
|
||||
output->clear();
|
||||
}
|
||||
*output << line;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
if (output != nullptr)
|
||||
{
|
||||
try
|
||||
{
|
||||
output->flush();
|
||||
output->close();
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
std::cerr << e.what() << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~LogFileWriter()
|
||||
{
|
||||
clear();
|
||||
delete(output);
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef WIN32
|
||||
#define BLT_NOW() auto t = std::time(nullptr); tm now{}; localtime_s(&now, &t)
|
||||
#else
|
||||
#define BLT_NOW() auto t = std::time(nullptr); auto now_ptr = std::localtime(&t); auto& now = *now_ptr
|
||||
#endif
|
||||
|
||||
//#define BLT_NOW() auto t = std::time(nullptr); tm now; localtime_s(&now, &t); //auto now = std::localtime(&t)
|
||||
#define BLT_ISO_YEAR(S) auto S = std::to_string(now.tm_year + 1900); \
|
||||
S += '-'; \
|
||||
S += ensureHasDigits(now.tm_mon+1, 2); \
|
||||
S += '-'; \
|
||||
S += ensureHasDigits(now.tm_mday, 2);
|
||||
#define BLT_CUR_TIME(S) auto S = ensureHasDigits(now.tm_hour, 2); \
|
||||
S += ':'; \
|
||||
S += ensureHasDigits(now.tm_min, 2); \
|
||||
S += ':'; \
|
||||
S += ensureHasDigits(now.tm_sec, 2);
|
||||
|
||||
static inline std::string ensureHasDigits(int current, int digits)
|
||||
{
|
||||
std::string asString = std::to_string(current);
|
||||
auto length = digits - asString.length();
|
||||
if (length <= 0)
|
||||
return asString;
|
||||
std::string zeros;
|
||||
zeros.reserve(length);
|
||||
for (unsigned int i = 0; i < length; i++)
|
||||
{
|
||||
zeros += '0';
|
||||
}
|
||||
return zeros + asString;
|
||||
}
|
||||
|
||||
log_format loggingFormat{};
|
||||
hashmap<std::thread::id, std::string> loggingThreadNames;
|
||||
hashmap<std::thread::id, hashmap<blt::logging::log_level, std::string>> loggingStreamLines;
|
||||
LogFileWriter writer;
|
||||
|
||||
const std::unique_ptr<tag_map> tagMap = std::make_unique<tag_map>(tag_map{
|
||||
{
|
||||
"YEAR", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
return std::to_string(now.tm_year);
|
||||
}
|
||||
},
|
||||
{
|
||||
"MONTH", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_mon + 1, 2);
|
||||
}
|
||||
},
|
||||
{
|
||||
"DAY", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_mday, 2);
|
||||
}
|
||||
},
|
||||
{
|
||||
"HOUR", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_hour, 2);
|
||||
}
|
||||
},
|
||||
{
|
||||
"MINUTE", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_min, 2);
|
||||
}
|
||||
},
|
||||
{
|
||||
"SECOND", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
return ensureHasDigits(now.tm_sec, 2);
|
||||
}
|
||||
},
|
||||
{
|
||||
"MS", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
return std::to_string(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::high_resolution_clock::now().time_since_epoch()).count()
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
"NS", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
return std::to_string(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::high_resolution_clock::now().time_since_epoch()).count()
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
"ISO_YEAR", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
BLT_ISO_YEAR(returnStr);
|
||||
return returnStr;
|
||||
}
|
||||
},
|
||||
{
|
||||
"TIME", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
BLT_CUR_TIME(returnStr);
|
||||
return returnStr;
|
||||
}
|
||||
},
|
||||
{
|
||||
"FULL_TIME", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
BLT_NOW();
|
||||
BLT_ISO_YEAR(ISO);
|
||||
BLT_CUR_TIME(TIME);
|
||||
ISO += ' ';
|
||||
ISO += TIME;
|
||||
return ISO;
|
||||
}
|
||||
},
|
||||
{
|
||||
"LF", [](const tag_func_param& f) -> std::string
|
||||
{
|
||||
return loggingFormat.levelColors[(int)f.level];
|
||||
}
|
||||
},
|
||||
{
|
||||
"ER", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
return loggingFormat.levelColors[(int)log_level::ERROR];
|
||||
}
|
||||
},
|
||||
{
|
||||
"CNR", [](const tag_func_param& f) -> std::string
|
||||
{
|
||||
return f.level >= log_level::ERROR ? loggingFormat.levelColors[(int)log_level::ERROR] : "";
|
||||
}
|
||||
},
|
||||
{
|
||||
"RC", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
return "\033[0m";
|
||||
}
|
||||
},
|
||||
{
|
||||
"LOG_LEVEL", [](const tag_func_param& f) -> std::string
|
||||
{
|
||||
return loggingFormat.levelNames[(int)f.level];
|
||||
}
|
||||
},
|
||||
{
|
||||
"THREAD_NAME", [](const tag_func_param&) -> std::string
|
||||
{
|
||||
if (loggingThreadNames.find(std::this_thread::get_id()) == loggingThreadNames.end())
|
||||
return "UNKNOWN";
|
||||
return loggingThreadNames[std::this_thread::get_id()];
|
||||
}
|
||||
},
|
||||
{
|
||||
"FILE", [](const tag_func_param& f) -> std::string
|
||||
{
|
||||
return f.file;
|
||||
}
|
||||
},
|
||||
{
|
||||
"LINE", [](const tag_func_param& f) -> std::string
|
||||
{
|
||||
return f.line;
|
||||
}
|
||||
},
|
||||
{
|
||||
"RAW_STR", [](const tag_func_param& f) -> std::string
|
||||
{
|
||||
return f.raw_string;
|
||||
}
|
||||
},
|
||||
{
|
||||
"STR", [](const tag_func_param& f) -> std::string
|
||||
{
|
||||
return f.formatted_string;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
static inline std::vector<std::string> split(std::string s, const std::string& delim)
|
||||
{
|
||||
size_t pos = 0;
|
||||
std::vector<std::string> tokens;
|
||||
while ((pos = s.find(delim)) != std::string::npos)
|
||||
{
|
||||
auto token = s.substr(0, pos);
|
||||
tokens.push_back(token);
|
||||
s.erase(0, pos + delim.length());
|
||||
}
|
||||
tokens.push_back(s);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
inline std::string filename(const std::string& path)
|
||||
{
|
||||
if (loggingFormat.printFullFileName)
|
||||
return path;
|
||||
auto paths = split(path, "/");
|
||||
auto final = paths[paths.size() - 1];
|
||||
if (final == "/")
|
||||
return paths[paths.size() - 2];
|
||||
return final;
|
||||
}
|
||||
|
||||
class string_parser
|
||||
{
|
||||
private:
|
||||
std::string _str;
|
||||
size_t _pos;
|
||||
|
||||
public:
|
||||
explicit string_parser(std::string str): _str(std::move(str)), _pos(0)
|
||||
{
|
||||
}
|
||||
|
||||
inline char next()
|
||||
{
|
||||
return _str[_pos++];
|
||||
}
|
||||
|
||||
[[nodiscard]] inline bool has_next() const
|
||||
{
|
||||
return _pos < _str.size();
|
||||
}
|
||||
};
|
||||
|
||||
std::string stripANSI(const std::string& str)
|
||||
{
|
||||
string_parser parser(str);
|
||||
std::string out;
|
||||
while (parser.has_next())
|
||||
{
|
||||
char c = parser.next();
|
||||
if (c == '\033')
|
||||
{
|
||||
while (parser.has_next() && parser.next() != 'm');
|
||||
}
|
||||
else
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void applyCFormatting(const std::string& format, std::string& output, std::va_list& args)
|
||||
{
|
||||
// args must be copied because they will be consumed by the first vsnprintf
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
|
||||
auto buffer_size = std::vsnprintf(nullptr, 0, format.c_str(), args_copy) + 1;
|
||||
auto* buffer = new char[static_cast<unsigned long>(buffer_size)];
|
||||
|
||||
vsnprintf(buffer, buffer_size, format.c_str(), args);
|
||||
output = std::string(buffer);
|
||||
|
||||
delete[] buffer;
|
||||
|
||||
va_end(args_copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the next character in the parser is a tag opening, if not output the buffer to the out string
|
||||
*/
|
||||
inline bool tagOpening(string_parser& parser, std::string& out)
|
||||
{
|
||||
char c = ' ';
|
||||
if (parser.has_next() && (c = parser.next()) == '{')
|
||||
if (parser.has_next() && (c = parser.next()) == '{')
|
||||
return true;
|
||||
else
|
||||
out += c;
|
||||
else
|
||||
out += c;
|
||||
return false;
|
||||
}
|
||||
|
||||
void parseString(string_parser& parser, std::string& out, const std::string& userStr, log_level level, const char* file, int line)
|
||||
{
|
||||
while (parser.has_next())
|
||||
{
|
||||
char c = parser.next();
|
||||
std::string nonTag;
|
||||
if (c == '$' && tagOpening(parser, nonTag))
|
||||
{
|
||||
std::string tag;
|
||||
while (parser.has_next())
|
||||
{
|
||||
c = parser.next();
|
||||
if (c == '}')
|
||||
break;
|
||||
tag += c;
|
||||
}
|
||||
c = parser.next();
|
||||
if (parser.has_next() && c != '}')
|
||||
{
|
||||
std::cerr << "Error processing tag, is not closed with two '}'!\n";
|
||||
break;
|
||||
}
|
||||
if (loggingFormat.ensureAlignment && tag == "STR")
|
||||
{
|
||||
auto currentOutputWidth = out.size();
|
||||
auto& longestWidth = loggingFormat.currentWidth;
|
||||
longestWidth = std::max(longestWidth, currentOutputWidth);
|
||||
// pad with spaces
|
||||
if (currentOutputWidth != longestWidth)
|
||||
{
|
||||
for (size_t i = currentOutputWidth; i < longestWidth; i++)
|
||||
out += ' ';
|
||||
}
|
||||
}
|
||||
tag_func_param param{
|
||||
level, filename({file}), std::to_string(line), userStr, userStr
|
||||
};
|
||||
out += (*tagMap)[tag].func(param);
|
||||
}
|
||||
else
|
||||
{
|
||||
out += c;
|
||||
out += nonTag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string applyFormatString(const std::string& str, log_level level, const char* file, int line)
|
||||
{
|
||||
// this can be speedup by preprocessing the string into an easily callable class
|
||||
// where all the variables are ready to be substituted in one step
|
||||
// and all static information already entered
|
||||
string_parser parser(loggingFormat.logOutputFormat);
|
||||
std::string out;
|
||||
parseString(parser, out, str, level, file, line);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void log_internal(const std::string& format, log_level level, const char* file, int line, std::va_list& args)
|
||||
{
|
||||
std::string withoutLn = format;
|
||||
auto len = withoutLn.length();
|
||||
|
||||
if (len > 0 && withoutLn[len - 1] == '\n')
|
||||
withoutLn = withoutLn.substr(0, len - 1);
|
||||
|
||||
std::string out;
|
||||
|
||||
applyCFormatting(withoutLn, out, args);
|
||||
|
||||
if (level == log_level::NONE)
|
||||
{
|
||||
std::cout << out << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::string finalFormattedOutput = applyFormatString(out, level, file, line);
|
||||
|
||||
if (loggingFormat.logToConsole)
|
||||
std::cout << finalFormattedOutput;
|
||||
|
||||
|
||||
if (loggingFormat.logToFile)
|
||||
{
|
||||
string_parser parser(loggingFormat.logFileName);
|
||||
std::string fileName;
|
||||
parseString(parser, fileName, withoutLn, level, file, line);
|
||||
|
||||
auto path = loggingFormat.logFilePath;
|
||||
if (!path.empty() && path[path.length() - 1] != '/')
|
||||
path += '/';
|
||||
|
||||
// if the file has changed (new day in default case) we should reset the rollover count
|
||||
if (loggingFormat.lastFile != fileName)
|
||||
{
|
||||
loggingFormat.currentRollover = 0;
|
||||
loggingFormat.lastFile = fileName;
|
||||
}
|
||||
|
||||
path += fileName;
|
||||
path += '-';
|
||||
path += std::to_string(loggingFormat.currentRollover);
|
||||
path += ".log";
|
||||
|
||||
if (std::filesystem::exists(path))
|
||||
{
|
||||
auto fileSize = std::filesystem::file_size(path);
|
||||
|
||||
// will start on next file
|
||||
if (fileSize > loggingFormat.logMaxFileSize)
|
||||
loggingFormat.currentRollover++;
|
||||
}
|
||||
|
||||
writer.writeLine(path, stripANSI(finalFormattedOutput));
|
||||
}
|
||||
//std::cout.flush();
|
||||
}
|
||||
|
||||
void log_stream_internal(const std::string& str, const logger& logger)
|
||||
{
|
||||
auto& s = loggingStreamLines[std::this_thread::get_id()][logger.level];
|
||||
// s += str;
|
||||
for (char c : str)
|
||||
{
|
||||
s += c;
|
||||
if (c == '\n')
|
||||
{
|
||||
log(s, logger.level, logger.file, logger.line);
|
||||
s = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setThreadName(const std::string& name)
|
||||
{
|
||||
loggingThreadNames[std::this_thread::get_id()] = name;
|
||||
}
|
||||
|
||||
void setLogFormat(const log_format& format)
|
||||
{
|
||||
loggingFormat = format;
|
||||
}
|
||||
|
||||
void setLogColor(log_level level, const std::string& newFormat)
|
||||
{
|
||||
loggingFormat.levelColors[(int)level] = newFormat;
|
||||
}
|
||||
|
||||
void setLogName(log_level level, const std::string& newFormat)
|
||||
{
|
||||
loggingFormat.levelNames[(int)level] = newFormat;
|
||||
}
|
||||
|
||||
void setLogOutputFormat(const std::string& newFormat)
|
||||
{
|
||||
loggingFormat.logOutputFormat = newFormat;
|
||||
}
|
||||
|
||||
void setLogToFile(bool shouldLogToFile)
|
||||
{
|
||||
loggingFormat.logToFile = shouldLogToFile;
|
||||
}
|
||||
|
||||
void setLogToConsole(bool shouldLogToConsole)
|
||||
{
|
||||
loggingFormat.logToConsole = shouldLogToConsole;
|
||||
}
|
||||
|
||||
void setLogPath(const std::string& path)
|
||||
{
|
||||
loggingFormat.logFilePath = path;
|
||||
}
|
||||
|
||||
void setLogFileName(const std::string& fileName)
|
||||
{
|
||||
loggingFormat.logFileName = fileName;
|
||||
}
|
||||
|
||||
void setMaxFileSize(const size_t fileSize)
|
||||
{
|
||||
loggingFormat.logMaxFileSize = fileSize;
|
||||
}
|
||||
|
||||
void flush()
|
||||
{
|
||||
std::cerr.flush();
|
||||
std::cout.flush();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,27 +57,32 @@ This is a println with boolean as hex 0x1
|
|||
This is a println with boolean as octal 1
|
||||
This is a println with alignment left 64 end value
|
||||
This is a println with alignment right 46 end value
|
||||
This is a println with alignment left (fill) 46******** end value
|
||||
This is a println with alignment left (fill) 46******** end value
|
||||
This is a println with alignment right (fill) ********46 end value
|
||||
This is a println with alignment right (fill with reserved character) ^^^^^^^^46 end value
|
||||
This is a println with fill no alignment %%%%%%%%%%%%%%%%%%46 end value
|
||||
This is a println with arg reference 46.02
|
||||
This is a println with arg reference &&&&&&&&&&&&&&&&&&&&
|
||||
)");
|
||||
|
||||
std::pair<bool, std::string> compare_strings(const std::string& s1, const std::string& s2)
|
||||
{
|
||||
if (s1.size() != s2.size())
|
||||
return {false, "Strings size do not match '" + std::to_string(s1.size()) + "' vs '" + std::to_string(s2.size()) + "'"};
|
||||
const auto size = std::min(s1.size(), s2.size());
|
||||
size_t index = 0;
|
||||
for (; index < s1.size(); ++index)
|
||||
for (; index < size; ++index)
|
||||
{
|
||||
if (s1[index] != s2[index])
|
||||
{
|
||||
std::stringstream ss;
|
||||
const auto i1 = std::max(static_cast<blt::i64>(index) - 32, 0l);
|
||||
const auto l1 = std::min(static_cast<blt::i64>(s1.size()) - i1, 65l);
|
||||
const auto l1 = std::min(static_cast<blt::i64>(size) - i1, 65l);
|
||||
ss << "Strings differ at index " << index << "!\n";
|
||||
ss << "'" << s1.substr(i1, l1) << "' vs '" << s2.substr(i1, l1) << "'" << std::endl;
|
||||
return {false, ss.str()};
|
||||
}
|
||||
}
|
||||
if (s1.size() != s2.size())
|
||||
return {false, "Strings size do not match '" + std::to_string(s1.size()) + "' vs '" + std::to_string(s2.size()) + "'"};
|
||||
return {true, ""};
|
||||
}
|
||||
|
||||
|
@ -116,8 +121,12 @@ int main()
|
|||
blt::logging::println(ss, "This is a println with boolean as octal {:o}", true);
|
||||
blt::logging::println(ss, "This is a println with alignment left {:<10} end value", 64);
|
||||
blt::logging::println(ss, "This is a println with alignment right {:>10} end value", 46);
|
||||
blt::logging::println(ss, "This is a println with alignment left (fill) {:*<10} end value", 46);
|
||||
blt::logging::println(ss, "This is a println with alignment left (fill) {:*<10} end value", 46);
|
||||
blt::logging::println(ss, "This is a println with alignment right (fill) {:*>10} end value", 46);
|
||||
blt::logging::println(ss, "This is a println with alignment right (fill with reserved character) {:\\^>10} end value", 46);
|
||||
blt::logging::println(ss, "This is a println with fill no alignment {:%20} end value", 46);
|
||||
blt::logging::println(ss, "This is a println with arg reference {0:{1}.{2}f}", 46.0232, 20, 2);
|
||||
blt::logging::println(ss, "This is a println with arg reference {0:&{1}}", "", 20);
|
||||
blt::logging::print(ss.str());
|
||||
auto [passed, error_msg] = compare_strings(expected_str, ss.str());
|
||||
BLT_ASSERT_MSG(passed && "Logger logged string doesn't match precomputed expected string!", error_msg.c_str());
|
||||
|
|
Loading…
Reference in New Issue